¿Cuándo debería utilizar una estructura en lugar de una clase en C#?
¿Cuándo deberías usar struct y no class en C#? Mi modelo conceptual es que las estructuras se utilizan en momentos en que el elemento es simplemente una colección de tipos de valores . Una forma de mantenerlos a todos juntos lógicamente en un todo cohesivo.
Encontré estas reglas aquí :
- Una estructura debe representar un valor único.
- Una estructura debe tener una huella de memoria inferior a 16 bytes.
- Una estructura no debe modificarse después de su creación.
¿Funcionan estas reglas? ¿Qué significa semánticamente una estructura?
La fuente a la que hace referencia el OP tiene cierta credibilidad... pero ¿qué pasa con Microsoft? ¿Cuál es la postura sobre el uso de estructuras? Busqué aprendizaje adicional de Microsoft y esto es lo que encontré:
Considere definir una estructura en lugar de una clase si las instancias del tipo son pequeñas y comúnmente de corta duración o comúnmente están incrustadas en otros objetos.
No defina una estructura a menos que el tipo tenga todas las características siguientes:
- Lógicamente representa un valor único, similar a los tipos primitivos (entero, doble, etc.).
- Tiene un tamaño de instancia inferior a 16 bytes.
- Es inmutable.
- No será necesario encajonarlo con frecuencia.
Microsoft viola constantemente esas reglas
Bien, los números 2 y 3 de todos modos. Nuestro querido diccionario tiene 2 estructuras internas:
[StructLayout(LayoutKind.Sequential)] // default for structs
private struct Entry //<Tkey, TValue>
{
// View code at *Reference Source
}
[Serializable, StructLayout(LayoutKind.Sequential)]
public struct Enumerator :
IEnumerator<KeyValuePair<TKey, TValue>>, IDisposable,
IDictionaryEnumerator, IEnumerator
{
// View code at *Reference Source
}
* Fuente de referencia
La fuente 'JonnyCantCode.com' obtuvo 3 de 4, algo bastante perdonable ya que el puesto 4 probablemente no sería un problema. Si se encuentra encajonando una estructura, reconsidere su arquitectura.
Veamos por qué Microsoft usaría estas estructuras:
- Cada estructura
Entry
yEnumerator
representan valores únicos. - Velocidad
Entry
nunca se pasa como parámetro fuera de la clase Diccionario. Una investigación más profunda muestra que para satisfacer la implementación de IEnumerable, Dictionary usa laEnumerator
estructura que copia cada vez que se solicita un enumerador... tiene sentido.- Interno a la clase Diccionario.
Enumerator
es público porque el Diccionario es enumerable y debe tener igual accesibilidad a la implementación de la interfaz IEnumerator, por ejemplo, el getter IEnumerator.
Actualización : además, tenga en cuenta que cuando una estructura implementa una interfaz (como lo hace Enumerator) y se convierte en ese tipo implementado, la estructura se convierte en un tipo de referencia y se mueve al montón. Interno de la clase Diccionario, Enumerador sigue siendo un tipo de valor. Sin embargo, tan pronto como un método llama , se devuelve GetEnumerator()
un tipo de referencia .IEnumerator
Lo que no vemos aquí es ningún intento o prueba de requisito para mantener las estructuras inmutables o mantener un tamaño de instancia de solo 16 bytes o menos:
- No se declara nada en las estructuras anteriores
readonly
, no es inmutable - El tamaño de estas estructuras podría superar los 16 bytes
Entry
tiene una vida útil indeterminada (desdeAdd()
, hastaRemove()
,Clear()
o recolección de basura);
Y... 4. Ambas estructuras almacenan TKey y TValue, que todos sabemos que son bastante capaces de ser tipos de referencia (información adicional adicional)
A pesar de las claves hash, los diccionarios son rápidos en parte porque crear instancias de una estructura es más rápido que un tipo de referencia. Aquí, tengo un Dictionary<int, int>
que almacena 300.000 enteros aleatorios con claves incrementadas secuencialmente.
Capacidad: 312874
MemSize: 2660827 bytes
Cambio de tamaño completado: 5 ms
Tiempo total para llenar: 889 ms
Capacidad : número de elementos disponibles antes de que se deba cambiar el tamaño de la matriz interna.
MemSize : se determina serializando el diccionario en un MemoryStream y obteniendo una longitud en bytes (lo suficientemente precisa para nuestros propósitos).
Cambio de tamaño completado : el tiempo que lleva cambiar el tamaño de la matriz interna de 150862 elementos a 312874 elementos. Cuando calculas que cada elemento se copia secuencialmente a través de Array.CopyTo()
, no está nada mal.
Tiempo total para completar : ciertamente sesgado debido al registro y a un OnResize
evento que agregué a la fuente; sin embargo, sigue siendo impresionante llenar 300.000 números enteros mientras se cambia el tamaño 15 veces durante la operación. Solo por curiosidad, ¿cuál sería el tiempo total para llenar si ya supiera la capacidad? 13ms
Entonces, ¿y si Entry
fuéramos una clase? ¿Estos tiempos o métricas realmente diferirían tanto?
Capacidad: 312874
MemSize: 2660827 bytes
Cambio de tamaño completado: 26 ms
Tiempo total para llenar: 964 ms
Evidentemente, la gran diferencia está en el cambio de tamaño. ¿Alguna diferencia si el Diccionario se inicializa con la Capacidad? No es suficiente preocuparse por... 12 ms .
Lo que sucede es que, como Entry
es una estructura, no requiere inicialización como un tipo de referencia. Ésta es a la vez la belleza y la perdición del tipo de valor. Para usarlo Entry
como tipo de referencia, tuve que insertar el siguiente código:
/*
* Added to satisfy initialization of entry elements --
* this is where the extra time is spent resizing the Entry array
* **/
for (int i = 0 ; i < prime ; i++)
{
destinationArray[i] = new Entry( );
}
/* *********************************************** */
La razón por la que tuve que inicializar cada elemento de la matriz Entry
como tipo de referencia se puede encontrar en MSDN: Diseño de estructuras . En breve:
No proporcione un constructor predeterminado para una estructura.
Si una estructura define un constructor predeterminado, cuando se crean matrices de la estructura, Common Language Runtime ejecuta automáticamente el constructor predeterminado en cada elemento de la matriz.
Algunos compiladores, como el compilador de C#, no permiten que las estructuras tengan constructores predeterminados.
En realidad, es bastante simple y tomaremos prestado las Tres Leyes de la Robótica de Asimov :
- La estructura debe ser segura de usar.
- La estructura debe realizar su función de manera eficiente, a menos que esto viole la regla n.° 1
- La estructura debe permanecer intacta durante su uso a menos que se requiera su destrucción para satisfacer la regla n.° 1.
... qué nos llevamos de esto : en resumen, ser responsable con el uso de tipos de valor. Son rápidos y eficientes, pero tienen la capacidad de provocar muchos comportamientos inesperados si no se mantienen adecuadamente (es decir, copias no intencionadas).
Cuando usted:
- no necesita polimorfismo,
- quiero semántica de valor, y
- desea evitar la asignación del montón y la sobrecarga asociada de recolección de basura.
La advertencia, sin embargo, es que las estructuras (arbitrariamente grandes) son más costosas de transmitir que las referencias de clase (generalmente una palabra de máquina), por lo que las clases podrían terminar siendo más rápidas en la práctica.
No estoy de acuerdo con las reglas dadas en la publicación original. Aquí están mis reglas:
Utiliza estructuras para el rendimiento cuando se almacenan en matrices. (ver también ¿Cuándo son las estructuras la respuesta? )
Los necesita en el código que pasa datos estructurados hacia/desde C/C++
No utilice estructuras a menos que las necesite:
- Se comportan de manera diferente a los "objetos normales" ( tipos de referencia ) bajo asignación y cuando se pasan como argumentos, lo que puede provocar un comportamiento inesperado; Esto es particularmente peligroso si la persona que mira el código no sabe que está tratando con una estructura.
- No se pueden heredar.
- Pasar estructuras como argumentos es más caro que clases.
Utilice una estructura cuando desee semántica de valores en lugar de semántica de referencia.
Si necesita semántica de referencia, necesita una clase, no una estructura.