Pregunta Cuándo usar struct?


¿Cuándo debería usar struct y no clase en C #? Mi modelo conceptual es que las estructuras se usan en momentos en que el artículo es simplemente una colección de tipos de valores. Una forma de mantenerlos lógicamente todos juntos en un todo cohesivo.

Me encontré con estas reglas aquí:

  • Una estructura debería representar un solo valor.
  • Una estructura debería tener memoria huella menos de 16 bytes.
  • Una estructura no debería ser cambiada después creación.

¿Funcionan estas reglas? ¿Qué significa una estructura semánticamente?


1201
2018-02-06 17:37


origen


Respuestas:


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 las estructuras? Busqué un poco más aprendiendo de Microsoft, y aquí está lo que encontré:

Considere definir una estructura en lugar de una clase si las instancias de   tipo son pequeños y comúnmente de corta duración o están comúnmente integrados en   otros objetos

No defina una estructura a menos que el tipo tenga todas las características siguientes: 

  1. Lógicamente representa un único valor, 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 encasillarlo frecuentemente.

Microsoft viola sistemáticamente esas reglas

De acuerdo, # 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, bastante perdonable ya que el # 4 probablemente no sería un problema. Si te encuentras boxeando una estructura, reconsidera tu arquitectura.

Veamos por qué Microsoft usaría estas estructuras:

  1. Cada estructura, Entry y Enumerator, representan valores individuales
  2. Velocidad
  3. Entry nunca pasa como un parámetro fuera de la clase de diccionario. La investigación adicional muestra que para satisfacer la implementación de IEnumerable, Dictionary usa Enumerator estructura que copia cada vez que se solicita un enumerador ... tiene sentido.
  4. Interna a la clase de diccionario. Enumerator es público porque Dictionary es enumerable y debe tener igual accesibilidad a la implementación de la interfaz IEnumerator - p. IEnumerator getter.

Actualizar - 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 a la clase de diccionario, enumerador es sigue siendo un tipo de valor Sin embargo, tan pronto como un método llama GetEnumerator(), un tipo de referencia IEnumerator es regresado.

Lo que no vemos aquí es cualquier intento o prueba de requisito para mantener las estructuras inmutables o mantener un tamaño de instancia de solo 16 bytes o menos:

  1. Nada en las estructuras anteriores está declarado readonly - no inmutable
  2. El tamaño de estas estructuras podría ser más de 16 bytes
  3. Entry tiene una vida indeterminada (desde Add(), a 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 añadida)

A pesar de las claves hash, los diccionarios son rápidos en parte porque instaurar 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
  Completed Resize: 5ms
  Tiempo total para llenar: 889ms

Capacidad: número de elementos disponibles antes de que la matriz interna deba cambiar de tamaño.

MemSize: determinado serializando el diccionario en un MemoryStream y obteniendo un byte de longitud (lo suficientemente preciso para nuestros propósitos).

Completar el tamaño: el tiempo que lleva cambiar el tamaño de la matriz interna de 150862 elementos a 312874 elementos. Cuando imaginas que cada elemento se copia secuencialmente a través de Array.CopyTo(), eso no es muy viejo.

Tiempo total para llenar: ciertamente sesgado debido a la tala y una OnResize evento que agregué a la fuente; sin embargo, sigue siendo impresionante llenar 300k de enteros mientras se redimensiona 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, ahora, ¿qué pasa si Entry eran una clase? ¿Estos tiempos o métricas realmente difieren tanto?

Capacidad: 312874
  MemSize: 2660827 bytes
  Completed Resize: 26 ms
  Tiempo total para llenar: 964ms

Obviamente, la gran diferencia está en cambiar el tamaño. ¿Alguna diferencia si el diccionario se inicializa con la Capacidad? No es suficiente para preocuparse por ... 12ms.

Lo que sucede es, porque Entry es una estructura, no requiere inicialización como un tipo de referencia. Esto es a la vez la belleza y la ruina del tipo de valor. Para usar 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 matriz de Entry como un tipo de referencia se puede encontrar en MSDN: diseño de estructura. En breve:

No proporcione un constructor predeterminado para una estructura.

Si una estructura define un constructor predeterminado, cuando las matrices de   estructura se crean, el tiempo de ejecución de lenguaje común de forma automática   ejecuta el constructor por defecto en cada elemento de la matriz.

Algunos compiladores, como el compilador de C #, no permiten que las estructuras   tener constructores por defecto

En realidad es bastante simple y tomaremos prestado de Asimov Tres leyes de la robótica:

  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 cumplir con la regla n. ° 1

...¿Qué nos quitamos de esto?: en resumen, ser responsable con el uso de tipos de valores. Son rápidos y eficientes, pero tienen la capacidad de causar muchos comportamientos inesperados si no se mantienen adecuadamente (es decir, copias no intencionales).


541
2017-08-07 13:44



Siempre que no necesite polimorfismo, quiere semántica de valores y desea evitar la asignación de montón y la sobrecarga de recolección de basura asociada. La advertencia, sin embargo, es que las estructuras (arbitrariamente grandes) son más caras de pasar 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.


142
2018-02-06 17:40



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 almacena en matrices. (ver también ¿Cuándo se estructura la respuesta?)

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

3) No use estructuras a menos que las necesite:

  • Se comportan de forma diferente a los "objetos normales" (tipos de referencia) bajo asignación y al pasar como argumentos, lo que puede conducir a un comportamiento inesperado; esto es particularmente peligroso si la persona que mira el código lo hace No sé que están tratando con una estructura.
  • No pueden ser heredados
  • Pasar estructuras como argumentos es más caro que las clases.

133
2018-02-28 16:33



Usa una estructura cuando quieras semántica de valores en lugar de semántica de referencia.

Editar

No estoy seguro de por qué la gente está bajando la votación, pero este es un punto válido, y se hizo antes de que el OP aclarara su pregunta, y es la razón básica más fundamental para una estructura.

Si necesita semántica de referencia, necesita una clase, no una estructura.


82
2018-02-06 17:40



Además de la respuesta "es un valor", un escenario específico para usar estructuras es cuando saber que tiene un conjunto de datos que está causando problemas de recolección de basura, y tiene muchos objetos. Por ejemplo, una gran lista / matriz de instancias de Persona. La metáfora natural aquí es una clase, pero si tienes una gran cantidad de instancias Person de larga duración, pueden obstruir GEN-2 y causar bloqueos de GC. Si el escenario lo justifica, un posible enfoque aquí es usar una matriz (no lista) de Persona estructuras, es decir Person[]. Ahora, en lugar de tener millones de objetos en GEN-2, tiene una sola porción en el LOH (supongo que aquí no hay cadenas, es decir, un valor puro sin referencias). Esto tiene muy poco impacto de GC.

Trabajar con estos datos es incómodo, ya que los datos son probablemente demasiado grandes para una estructura, y no desea copiar valores de grasa todo el tiempo. Sin embargo, acceder a él directamente en una matriz no copia la estructura; está en su lugar (contraste con un indizador de lista, que sí lo hace). Esto significa mucho trabajo con índices:

int index = ...
int id = peopleArray[index].Id;

Tenga en cuenta que mantener los valores en sí mismos inmutables ayudará aquí. Para una lógica más compleja, use un método con un parámetro by-ref:

void Foo(ref Person person) {...}
...
Foo(ref peopleArray[index]);

De nuevo, esto está en su lugar, no hemos copiado el valor.

En escenarios muy específicos, esta táctica puede ser muy exitosa; sin embargo, es un scernario bastante avanzado que debe intentarse solo si usted sabe lo que está haciendo y por qué. El valor predeterminado aquí sería una clase.


54
2017-10-22 12:14



Desde el Especificación del lenguaje C #:

1.7 Estructuras 

Al igual que las clases, las estructuras son estructuras de datos que pueden contener miembros de datos y miembros de funciones, pero a diferencia de las clases, las estructuras son   tipos de valor y no requieren asignación de montón. Una variable de una estructura   tipo almacena directamente los datos de la estructura, mientras que una variable de   class type almacena una referencia a un objeto dinámicamente asignado.   Los tipos de estructura no son compatibles con la herencia especificada por el usuario, y todas las estructuras   tipos heredan implícitamente de tipo objeto.

Las estructuras son particularmente útiles para estructuras de datos pequeñas que tienen   semántica de valores. Números complejos, puntos en un sistema de coordenadas, o   pares clave-valor en un diccionario son todos buenos ejemplos de estructuras. los   el uso de estructuras en lugar de clases para estructuras de datos pequeñas puede hacer   una gran diferencia en el número de asignaciones de memoria de una aplicación   realiza. Por ejemplo, el siguiente programa crea e inicializa   una matriz de 100 puntos. Con Point implementado como una clase, 101   Se crean instancias de objetos separados: uno para la matriz y uno para cada uno.   los 100 elementos

class Point
{
   public int x, y;

   public Point(int x, int y) {
      this.x = x;
      this.y = y;
   }
}

class Test
{
   static void Main() {
      Point[] points = new Point[100];
      for (int i = 0; i < 100; i++) points[i] = new Point(i, i);
   }
}

Una alternativa es hacer que Point sea una estructura.

struct Point
{
   public int x, y;

   public Point(int x, int y) {
      this.x = x;
      this.y = y;
   }
}

Ahora, solo se crea una instancia de un objeto, el de la matriz, y las instancias de Point se almacenan en línea en la matriz.

Los constructores de Struct se invocan con el nuevo operador, pero eso no implica que se esté asignando memoria. En lugar de asignar dinámicamente un objeto y devolverle una referencia, un constructor de estructura simplemente devuelve el valor de la estructura en sí (generalmente en una ubicación temporal en la pila), y este valor se copia cuando es necesario.

Con las clases, es posible que dos variables hagan referencia al mismo objeto y, por lo tanto, sea posible que las operaciones en una variable afecten al objeto al que hace referencia la otra variable. Con structs, las variables tienen cada una su propia copia de los datos, y no es posible que las operaciones en una afecten a la otra. Por ejemplo, la salida producida por el siguiente fragmento de código depende de si Point es una clase o una estructura.

Point a = new Point(10, 10);
Point b = a;
a.x = 20;
Console.WriteLine(b.x);

Si Point es una clase, la salida es 20 porque a y b hacen referencia al mismo objeto. Si Point es una estructura, el resultado es 10 porque la asignación de aab crea una copia del valor, y esta copia no se ve afectada por la asignación posterior a a.x.

El ejemplo anterior resalta dos de las limitaciones de las estructuras. En primer lugar, copiar una estructura completa suele ser menos eficiente que copiar una referencia de objeto, por lo que la asignación y el paso de parámetros de valor pueden ser más costosos con las estructuras que con los tipos de referencia. En segundo lugar, a excepción de los parámetros ref y out, no es posible crear referencias a structs, lo que excluye su uso en una serie de situaciones.


36
2017-09-17 15:42



Las estructuras son buenas para la representación atómica de datos, donde dichos datos pueden ser copiados varias veces por el código. La clonación de un objeto es, en general, más costosa que copiar una estructura, ya que implica asignar la memoria, ejecutar el constructor y desasignar / recopilar la basura cuando se hace con ella.


31
2018-02-06 17:58



Aquí hay una regla básica.

  • Si todos los campos de miembros son tipos de valores, cree un estructura.

  • Si alguno de los campos de un miembro es un tipo de referencia, cree un clase. Esto se debe a que el campo del tipo de referencia necesitará la asignación del montón de todos modos.

Exmaples

public struct MyPoint 
{
    public int X; // Value Type
    public int Y; // Value Type
}

public class MyPointWithName 
{
    public int X; // Value Type
    public int Y; // Value Type
    public string Name; // Reference Type
}

24
2018-01-22 10:17



Primero: escenarios de interoperabilidad o cuando necesita especificar el diseño de la memoria

Segundo: cuando los datos son casi del mismo tamaño que un puntero de referencia de todos modos.


17
2018-02-06 18:12



Necesita utilizar una "estructura" en situaciones en las que desea especificar explícitamente el diseño de la memoria utilizando el StructLayoutAttribute - típicamente para PInvoke.

Editar: Comentario indica que puede usar clase o estructura con StructLayoutAttribute y eso es cierto. En la práctica, normalmente usaría una estructura: se asigna en la pila frente al montón, lo que tiene sentido si solo está pasando un argumento a una llamada de método no administrada.


16
2018-02-06 18:09



Uso estructuras para empaquetar o desempaquetar cualquier tipo de formato de comunicación binario. Eso incluye leer o escribir en el disco, listas de vértices DirectX, protocolos de red o tratar con datos encriptados / comprimidos.

Las tres pautas que enumera no me han sido útiles en este contexto. Cuando necesito escribir cuatrocientos bytes de cosas en un Pedido particular, voy a definir una estructura de cuatrocientos bytes, y la voy a llenar con los valores no relacionados que se supone que tiene, y voy a para configurarlo de la forma que sea más sensata en ese momento. (De acuerdo, cuatrocientos bytes serían bastante extraños, pero cuando estaba escribiendo archivos de Excel para ganarse la vida, estaba lidiando con estructuras de hasta unos cuarenta bytes, porque así de grandes son algunos de los registros BIFF).


15
2018-02-06 18:25