TSet es similar a TMap y TMultiMap, pero con una diferencia importante: en lugar de asociar valores de datos con claves independientes, TSet usa el valor de datos en sí mismo como la clave a través de una función anulable que evalúa el elemento. TSet es muy rápida (tiempo constante) para añadir, buscar y eliminar elementos. De forma predeterminada, TSet no admite claves duplicadas, pero este comportamiento puede activarse con un parámetro de plantilla.
TSet
TSet es una clase contenedora rápida para almacenar elementos únicos en un contexto donde el orden es irrelevante. En la mayoría de los casos, solo se necesita un parámetro: el tipo de elemento. Sin embargo, TSet se puede configurar con distintos parámetros de plantilla para cambiar su comportamiento y hacerla más versátil. Puedes especificar una estructura derivada basada en DefaultKeyFuncs para proporcionar funcionalidad de hash, además de permitir que haya varias claves con el mismo valor en el conjunto. Por último, al igual que el resto de clases contenedoras, puedes proporcionar un asignador de memoria personalizado para el almacenamiento de tus datos.
Al igual que TArray, TSet es un contenedor homogéneo, lo que significa que todos sus elementos son estrictamente del mismo tipo. TSet también es un tipo de valor y admite las operaciones habituales de copia, asignación y destructor, así como una propiedad sólida de sus elementos, que se destruyen cuando lo hace el TSet. El tipo de clave también debe ser un tipo de valor.
TSet usa hashes, lo que significa que el parámetro de plantilla KeyFuncs, si se proporciona, te indica cómo determinar la clave de un elemento, cómo comparar dos claves para determinar si son iguales, cómo aplicar la función hash a la clave y si se permite o no la duplicación de claves. Estos tienen valores por defecto que devolverán referencias a la clave, usa el operator== para la igualdad, y la función GetTypeHash que no sea miembro para el hash. De forma predeterminada, el conjunto no permitirá claves duplicadas. Si tu tipo de clave es compatible con estas funciones, es utilizable como clave de conjunto sin necesidad de proporcionar un KeyFuncs personalizado. Para crear un KeyFuncs personalizado, amplía la estructura DefaultKeyFuncs.
Por último, TSet puede usar un asignador opcional para controlar el comportamiento de la asignación de memoria. Los asignadores estándar de Unreal Engine 4 (UE4) (como FHeapAllocator y TInlineAllocator) no se pueden usar como asignadores para TSet. En cambio, TSet usa asignadores de conjuntos, que definen cuántos depósitos de hash debe usar el conjunto y qué asignadores estándar UE4 usar para el almacenamiento de elementos. Consulta TSetAllocator para obtener más información.
A diferencia de TArray, el orden relativo de los elementos de TSet en la memoria no es fiable ni estable y al iterar sobre los elementos es probable que se devuelvan en un orden distinto al que tenían cuando se añadieron. Tampoco es probable que los elementos se dispongan de forma contigua en la memoria. La estructura de datos de respaldo de un conjunto es una matriz dispersa, es decir, una matriz que admite de forma eficiente los espacios entre sus elementos. A medida que se vayan eliminando elementos del conjunto, aparecerán espacios en la matriz dispersa. Esos espacios se pueden rellenar añadiendo nuevos elementos a la matriz. Sin embargo, aunque TSet no aleatoriza los elementos para rellenar los espacios, los punteros a los elementos del conjunto pueden quedar invalidados, ya que todo el almacenamiento puede reasignarse cuando está lleno y se añaden nuevos elementos.
TSet (al igual que muchos contenedores de Unreal Engine) asume que el tipo de elemento es reubicable de forma trivial, lo que significa que los elementos se pueden con seguridad de una posición a otra en la memoria copiando directamente bytes sin procesar.
Cómo crear y rellenar un conjunto
Puedes crear un TSet como este:
TSet<FString> FruitSet;Esto crea un TSet vacío que contendrá datos de FString. El TSet comparará los elementos directamente con operator==, los cifrará mediante GetTypeHash y usará el asignador de montón estándar. Aún no se ha asignado memoria.
La forma estándar de rellenar un conjunto es usar la función Add y proporcionar una clave (elemento):
FruitSet.Add(TEXT("Banana"));
FruitSet.Add(TEXT("Grapefruit"));
FruitSet.Add(TEXT("Pineapple"));
// FruitSet == [ "Banana", "Grapefruit", "Pineapple" ]Aunque los elementos se enumeran aquí en el orden en que se insertaron, no hay garantía de cuál será tu orden real en la memoria. Para un nuevo conjunto, es probable que estén en orden de inserción, pero a medida que se producen más inserciones y eliminaciones, es cada vez menos probable que aparezcan nuevos elementos al final.
Dado que este conjunto usa el asignador predeterminado, se garantiza que las claves son únicas. El resultado de añadir una clave duplicada es el siguiente:
FruitSet.Add(TEXT("Pear"));
FruitSet.Add(TEXT("Banana"));
// FruitSet == [ "Banana", "Grapefruit", "Pineapple", "Pear" ]
// Note: Only one banana entry.El conjunto ahora contiene cuatro elementos. «Pear» elevó el recuento de tres a cuatro, pero el nuevo «Banana» no modificó el número de elementos del conjunto, ya que reemplazó la antigua entrada «Banana».
Al igual que TArray, también podemos usar Emplace en lugar de Add para evitar la creación de temporales al insertar en el conjunto:
FruitSet.Emplace(TEXT("Orange"));
// FruitSet == [ "Banana", "Grapefruit", "Pineapple", "Pear", "Orange" ]Aquí, el argumento se pasa directamente al constructor del tipo de clave. Así se evita la creación de un FString temporal para el valor. A diferencia de TArray, solo es posible insertar elementos en un conjunto con constructores de un solo argumento.
También es posible insertar todos los elementos de otro conjunto usando la función Append para combinarlos:
TSet<FString> FruitSet2;
FruitSet2.Emplace(TEXT("Kiwi"));
FruitSet2.Emplace(TEXT("Melon"));
FruitSet2.Emplace(TEXT("Mango"));
FruitSet2.Emplace(TEXT("Orange"));
FruitSet.Append(FruitSet2);
// FruitSet == [ "Banana", "Grapefruit", "Pineapple", "Pear", "Orange", "Kiwi", "Melon", "Mango" ]En el ejemplo anterior, el conjunto resultante equivale a usar Add o Emplace para añadir los elementos individualmente. Las claves duplicadas del conjunto de origen reemplazarán a sus equivalentes en el destino.
Cómo editar TSets de UPROPERTY
Si marcas el TSet con la macro UPROPERTY y una de las palabras clave «editables» (EditAnywhere, EditDefaultsOnly o EditInstanceOnly), puedes añadir y editar elementos en Unreal Editor.
UPROPERTY(Category = SetExample, EditAnywhere)
TSet<FString> FruitSet;Iteración
La iteración sobre TSets es similar a TArrays. Puedes usar la función de rango de C++:
for (auto& Elem : FruitSet)
{
FPlatformMisc::LocalPrint(
*FString::Printf(
TEXT(" \"%s\"\n"),
*Elem
)
);
}
// Output:
También puedes crear iteradores con las funciones CreateIterator y CreateConstIterators. CreateIterator devuelve un iterador con permisos de lectura y escritura, mientras que CreateConstIterator devuelve un iterador de solo lectura. En cualquier caso, puedes usar las funciones Key y Value de estos iteradores para examinar los elementos. Si imprimiéramos el contenido de nuestro ejemplo de «fruta» usando iteradores, quedaría algo así:
for (auto It = FruitSet.CreateConstIterator(); It; ++It)
{
FPlatformMisc::LocalPrint(
*FString::Printf(
TEXT("(%s)\n"),
*It
)
);
}Consultas
Para saber cuántos elementos hay en el conjunto, llama a la función Num.
int32 Count = FruitSet.Num();
// Count == 8Para determinar si un conjunto contiene o no un elemento específico, llama a la función Contains, tal y como se indica a continuación:
bool bHasBanana = FruitSet.Contains(TEXT("Banana"));
bool bHasLemon = FruitSet.Contains(TEXT("Lemon"));
// bHasBanana == true
// bHasLemon == falsePuedes usar la estructura FSetElementId para encontrar el índice de una clave dentro del conjunto. Luego, puedes usar ese índice con operator[] para recuperar el elemento. Llamar a operator[] en un conjunto no constante devolverá una referencia no constante, y llamarlo en un conjunto constante devolverá una referencia constante.
FSetElementId BananaIndex = FruitSet.Index(TEXT("Banana"));
// BananaIndex is a value between 0 and (FruitSet.Num() - 1)
FPlatformMisc::LocalPrint(
*FString::Printf(
TEXT(" \"%s\"\n"),
*FruitSet[BananaIndex]
)
);
// Prints "Banana"
Si no estás seguro de si tu conjunto contiene una clave o no, puedes consultar con la función Contains y luego usar operator[]. Sin embargo, esto no es lo mejor, ya que una recuperación correcta implica dos búsquedas en la misma clave. La función Find combina estos comportamientos con una sola búsqueda. Find devuelve un puntero al valor del elemento si el conjunto contiene la clave, o un puntero nulo en caso contrario. Llamar a Find en un conjunto constante provocará que el puntero que devuelve también sea constante.
FString* PtrBanana = FruitSet.Find(TEXT("Banana"));
FString* PtrLemon = FruitSet.Find(TEXT("Lemon"));
// *PtrBanana == "Banana"
// PtrLemon == nullptrLa función Array devuelve un TArray con una copia de todos los elementos del TSet. La matriz que pases se vaciará al principio de la operación, por lo que el número de elementos resultante siempre será igual al número de elementos del conjunto:
TArray<FString> FruitArray = FruitSet.Array();
// FruitArray == [ "Banana","Grapefruit","Pineapple","Pear","Orange","Kiwi","Melon","Mango" ] (order may vary)Eliminación
Los elementos se pueden eliminar por índice con la función Remove, aunque se recomienda usarla solo al iterar por los elementos. La función Remove devuelve el número de elementos eliminados, y será 0 si la clave proporcionada no estaba en el conjunto. Si un TSet admite claves duplicadas, Remove eliminará todos los elementos coincidentes.
FruitSet.Remove(0);
// FruitSet == [ "Grapefruit","Pineapple","Pear","Orange","Kiwi","Melon","Mango" ]La eliminación de elementos puede dejar espacios en la estructura de datos, que puedes ver al visualizar el conjunto en la ventana de inspección de Visual Studio, pero se han omitido aquí para mayor claridad.
int32 RemovedAmountPineapple = FruitSet.Remove(TEXT("Pineapple"));
// RemovedAmountPineapple == 1
// FruitSet == [ "Grapefruit","Pear","Orange","Kiwi","Melon","Mango" ]
FString RemovedAmountLemon = FruitSet.Remove(TEXT("Lemon"));
// RemovedAmountLemon == 0Por último, puedes eliminar todos los elementos del conjunto con las funciones Empty o Reset.
TSet<FString> FruitSetCopy = FruitSet;
// FruitSetCopy == [ "Grapefruit","Pear","Orange","Kiwi","Melon","Mango" ]
FruitSetCopy.Empty();
// FruitSetCopy == []Empty y Reset son similares, pero Empty puede tomar un parámetro para indicar cuánta holgura dejar en el conjunto, mientras que Reset siempre deja la mayor holgura posible.
Ordenación
Un TSet puede ordenarse. Después de ordenar, la iteración sobre el conjunto presentará los elementos en orden, pero este comportamiento solo está garantizado hasta la próxima vez que modifiques el conjunto. La clasificación es inestable, por lo que los elementos equivalentes de un conjunto que admite claves duplicadas pueden aparecer en cualquier orden.
La función Sort utiliza un predicado binario que especifica el criterio de ordenación, tal y como se indica a continuación:
FruitSet.Sort([](const FString& A, const FString& B) {
return A > B; // sort by reverse-alphabetical order
});
// FruitSet == [ "Pear", "Orange", "Melon", "Mango", "Kiwi", "Grapefruit" ] (order is temporarily guaranteed)
FruitSet.Sort([](const FString& A, const FString& B) {
return A.Len() < B.Len(); // sort strings by length, shortest to longest
});
// FruitSet == [ "Pear", "Kiwi", "Melon", "Mango", "Orange", "Grapefruit" ] (order is temporarily guaranteed)Operadores
Al igual que TArray, TSet es un tipo de valor normal y, como tal, se puede copiar con el constructor de copia estándar o con el operador de asignación. Los conjuntos son propietarios estrictos de sus elementos, por lo que copiar un conjunto es una tarea compleja; el nuevo conjunto tendrá su propia copia de los elementos.
TSet<int32, FString> NewSet = FruitSet;
NewSet.Add(TEXT("Apple"));
NewSet.Remove(TEXT("Pear"));
// FruitSet == [ "Pear", "Kiwi", "Melon", "Mango", "Orange", "Grapefruit" ]
// NewSet == [ "Kiwi", "Melon", "Mango", "Orange", "Grapefruit", "Apple" ]Holgura
La holgura es memoria asignada que no contiene ningún elemento. Puedes asignar memoria sin añadir elementos haciendo una llamada a Reserve y puedes eliminar elementos sin desasignar la memoria que estaban usando haciendo una llamada a Reset o a Empty con un parámetro de holgura distinto de cero. La holgura optimiza el proceso de añadir nuevos elementos al conjunto usando memoria preasignada en lugar de tener que asignar nueva memoria. También puede ayudar con la eliminación de elementos, ya que el sistema no necesita desasignar memoria. Esto es especialmente eficaz a la hora de vaciar un conjunto que esperas repoblar inmediatamente con la misma cantidad de elementos o menos.
TSet no proporciona una forma de comprobar cuántos elementos están preasignados, como sí lo hace la función Max de TArray.
El siguiente código elimina todos los elementos del conjunto sin desasignar memoria, lo que genera holgura:
FruitSet.Reset();
// FruitSet == [ <invalid>, <invalid>, <invalid>, <invalid>, <invalid>, <invalid> ]Para crear holgura directamente (p. ej., para preasignar memoria antes de añadir elementos), usa la función Reserve.
FruitSet.Reserve(10);
for (int32 i = 0; i < 10; ++i)
{
FruitSet.Add(FString::Printf(TEXT("Fruit%d"), i));
}
// FruitSet == [ "Fruit9", "Fruit8", "Fruit7" ... "Fruit2", "Fruit1", "Fruit0" ]La preasignación de holgura ha provocado que los nuevos elementos se añadan en orden inverso. A diferencia de las matrices, los conjuntos no intentan mantener el orden de los elementos, y el código que trata con conjuntos no debería esperar que el orden de los elementos sea estable o predecible.
Para eliminar toda la holgura de un TSet, usa las funciones Collapse y Shrink. Shrink elimina toda la holgura del final del contenedor, pero dejará los elementos vacíos en el medio o al principio.
// Remove every other element from the set.
for (int32 i = 0; i < 10; i += 2)
{
FruitSet.Remove(FSetElementId::FromInteger(i));
}
// FruitSet == ["Fruit8", <invalid>, "Fruit6", <invalid>, "Fruit4", <invalid>, "Fruit2", <invalid>, "Fruit0", <invalid> ]
FruitSet.Shrink();
// FruitSet == ["Fruit8", <invalid>, "Fruit6", <invalid>, "Fruit4", <invalid>, "Fruit2", <invalid>, "Fruit0" ]Shrink solo eliminó un elemento no válido en el código anterior porque solo había un elemento vacío al final. Para eliminar todo el margen de holgura, primero debes llamar a la función Compact o CompactStable, de modo que los espacios vacíos se agrupen en preparación para Shrink. Como su nombre indica, CompactStable mantiene el orden de los elementos mientras consolida los elementos vacíos.
FruitSet.CompactStable();
// FruitSet == ["Fruit8", "Fruit6", "Fruit4", "Fruit2", "Fruit0", <invalid>, <invalid>, <invalid>, <invalid> ]
FruitSet.Shrink();
// FruitSet == ["Fruit8", "Fruit6", "Fruit4", "Fruit2", "Fruit0" ]DefaultKeyFuncs
Siempre que un tipo tenga un operator== y una sobrecarga GetTypeHash que no sea miembro, TSet puede usarlos, ya que el tipo es tanto el elemento como la clave. Sin embargo, puede ser útil usar tipos como claves cuando no se deseen sobrecargar esas funciones. En estos casos, puedes proporcionar tus propios DefaultKeyFuncs personalizados. Para crear KeyFuncs para tu tipo de clave, debes definir dos definiciones de tipo y tres funciones estáticas, tal y como se indica a continuación:
KeyInitType: el tipo usado para pasar claves. Suele extraerse del parámetro de plantilla ElementType.ElementInitType: el tipo usado para pasar elementos. También suele extraerse del parámetro de plantilla ElementType y, por lo tanto, es idéntico a KeyInitType.KeyInitType GetSetKey(ElementInitType Element): devuelve la clave de un elemento. En el caso de los conjuntos, suele ser el propio elemento.bool Matches(KeyInitType A, KeyInitType B): devuelvetruesiAyBson equivalentes,falseen caso contrario.uint32 GetKeyHash(KeyInitType Key): devuelve el valor hash deKey.
KeyInitType y ElementInitType son definiciones de tipo de la convención normal de paso del tipo clave/elemento. Por lo general, serán un valor para los tipos triviales y una referencia constante para los tipos no triviales. Recuerda que el tipo de elemento de un conjunto es también el tipo de clave, razón por la cual DefaultKeyFuncs usa solo un parámetro de plantilla, ElementType, para definir ambos.
TSet da por hecho que dos elementos que se comparen con Matches (en DefaultKeyFuncs) también devolverán el mismo valor con GetKeyHash (en KeyFuncs).
No modifiques la clave de un elemento existente de forma que cambie los resultados de cualquiera de estas funciones, ya que esto invalidará el hash interno del conjunto. Estas reglas también se aplican a las sobrecargas del operator== y GetKeyHash al usar la implementación por defecto de DefaultKeyFuncs.
Varios
Las funciones CountBytes y GetAllocatedSize calculan cuánta memoria está utilizando actualmente la matriz interna. CountBytes usa un parámetro FArchive, mientras que GetAllocatedSize no. Estas funciones se suelen usar para generar informes de estadísticas.
La función Dump toma un FOutputDevice y escribe información de implementación sobre el contenido del conjunto. También existe una función DumpHashElements que enumera todos los elementos de todas las entradas de hash. Estas funciones se suelen utilizar para la depuración.