¿Cuándo debería utilizar una estructura en lugar de una clase en C#?

Resuelto Alex Baranosky asked hace 15 años • 0 respuestas

¿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?

Alex Baranosky avatar Feb 07 '09 00:02 Alex Baranosky
Aceptado

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:

  1. Lógicamente representa un valor único, similar a los tipos primitivos (entero, doble, etc.).
  2. Tiene un tamaño de instancia inferior a 16 bytes.
  3. Es inmutable.
  4. 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:

  1. Cada estructura Entryy Enumeratorrepresentan valores únicos.
  2. Velocidad
  3. Entrynunca 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 la Enumeratorestructura que copia cada vez que se solicita un enumerador... tiene sentido.
  4. Interno a la clase Diccionario. Enumeratores 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:

  1. No se declara nada en las estructuras anteriores readonly, no es inmutable
  2. El tamaño de estas estructuras podría superar los 16 bytes
  3. Entrytiene una vida útil indeterminada (desde Add(), hasta Remove(), 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 OnResizeevento 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 Entryfué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 Entryes 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 Entrycomo 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 Entrycomo 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 :

  1. La estructura debe ser segura de usar.
  2. La estructura debe realizar su función de manera eficiente, a menos que esto viole la regla n.° 1
  3. 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).

IAbstract avatar Aug 07 '2011 13:08 IAbstract

Cuando usted:

  1. no necesita polimorfismo,
  2. quiero semántica de valor, y
  3. 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.

dsimcha avatar Feb 06 '2009 17:02 dsimcha

No estoy de acuerdo con las reglas dadas en la publicación original. Aquí están mis reglas:

  1. Utiliza estructuras para el rendimiento cuando se almacenan en matrices. (ver también ¿Cuándo son las estructuras la respuesta? )

  2. Los necesita en el código que pasa datos estructurados hacia/desde C/C++

  3. 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.
ILoveFortran avatar Feb 28 '2009 16:02 ILoveFortran

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.

JoshBerke avatar Feb 06 '2009 17:02 JoshBerke