¿Cuándo debo usar una estructura en lugar de una clase?


302

MSDN dice que debe usar estructuras cuando necesite objetos livianos. ¿Hay otros escenarios cuando una estructura es preferible a una clase?

Algunas personas podrían haber olvidado que:

  1. Las estructuras pueden tener métodos.
  2. Las estructuras no pueden ser heredadas.

Entiendo las diferencias técnicas entre estructuras y clases, simplemente no tengo una buena idea de cuándo usar una estructura.


Solo un recordatorio: lo que la mayoría de la gente tiende a olvidar en este contexto es que en C # structs también pueden tener métodos.
petr k.

Respuestas:


295

MSDN tiene la respuesta: Elegir entre clases y estructuras .

Básicamente, esa página le brinda una lista de verificación de 4 elementos y le dice que use una clase a menos que su tipo cumpla con todos los criterios.

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 menor de 16 bytes.
  • Es inmutable.
  • No tendrá que estar en caja con frecuencia.

1
Tal vez me estoy perdiendo algo obvio, pero no entiendo el razonamiento detrás de la parte "inmutable". ¿Por qué es esto necesario? ¿Alguien podría explicarlo?
Tamas Czinege

3
Probablemente lo hayan recomendado porque si la estructura es inmutable, no importará que tenga una semántica de valor en lugar de una semántica de referencia. La distinción solo importa si mutas el objeto / estructura después de hacer una copia.
Stephen C. Steel

@DrJokepu: en algunas situaciones, el sistema realizará una copia temporal de una estructura y luego permitirá que esa copia se pase por referencia al código que la modifica; Dado que la copia temporal será desechada, el cambio se perderá. Este problema es especialmente grave si una estructura tiene métodos que lo mutan. Estoy totalmente en desacuerdo con la idea de que la mutabilidad es una razón para hacer que una clase sea una clase, ya que, a pesar de algunas deficiencias en c # y vb.net, las estructuras mutables proporcionan una semántica útil que no se puede lograr de otra manera; no hay razón semántica para preferir una estructura inmutable a una clase.
supercat

44
@Chuu: Al diseñar el compilador JIT, Microsoft decidió optimizar el código para copiar estructuras de 16 bytes o menos; Esto significa que copiar una estructura de 17 bytes puede ser significativamente más lento que copiar una estructura de 16 bytes. No veo ninguna razón particular para esperar que Microsoft extienda tales optimizaciones a estructuras más grandes, pero es importante tener en cuenta que si bien las estructuras de 17 bytes pueden ser más lentas para copiar que las estructuras de 16 bytes, hay muchos casos en los que las estructuras grandes pueden ser más eficientes que objetos de clase grandes, y donde la ventaja relativa de las estructuras crece con el tamaño de la estructura.
supercat

3
@Chuu: la aplicación de los mismos patrones de uso a las estructuras grandes como lo haría con las clases puede dar como resultado un código ineficiente, pero la solución adecuada a menudo no es reemplazar las estructuras con clases, sino utilizar estructuras de manera más eficiente; más notablemente, uno debe evitar pasar o devolver estructuras por valor. Pásalos como refparámetros siempre que sea razonable hacerlo. Pasar una estructura con 4.000 campos como parámetro de referencia a un método que cambie uno será más barato que pasar una estructura con 4 campos por valor a un método que devuelve una versión modificada.
supercat

53

Me sorprende no haber leído en ninguna de las respuestas anteriores esto, que considero el aspecto más crucial:

Yo uso estructuras cuando quiero un tipo sin identidad. Por ejemplo un punto 3D:

public struct ThreeDimensionalPoint
{
    public readonly int X, Y, Z;
    public ThreeDimensionalPoint(int x, int y, int z)
    {
        this.X = x;
        this.Y = y;
        this.Z = z;
    }

    public override string ToString()
    {
        return "(X=" + this.X + ", Y=" + this.Y + ", Z=" + this.Z + ")";
    }

    public override int GetHashCode()
    {
        return (this.X + 2) ^ (this.Y + 2) ^ (this.Z + 2);
    }

    public override bool Equals(object obj)
    {
        if (!(obj is ThreeDimensionalPoint))
            return false;
        ThreeDimensionalPoint other = (ThreeDimensionalPoint)obj;
        return this == other;
    }

    public static bool operator ==(ThreeDimensionalPoint p1, ThreeDimensionalPoint p2)
    {
        return p1.X == p2.X && p1.Y == p2.Y && p1.Z == p2.Z;
    }

    public static bool operator !=(ThreeDimensionalPoint p1, ThreeDimensionalPoint p2)
    {
        return !(p1 == p2);
    }
}

Si tiene dos instancias de esta estructura, no le importa si son una sola pieza de datos en la memoria o dos. Solo le importan los valores que tienen.


44
Un poco fuera de tema, pero ¿por qué lanzas una excepción ArgumentException cuando obj no es ThreeDimensionalPoint? ¿No deberías simplemente devolver falso en ese caso?
Svish

44
Eso es correcto, estaba demasiado ansioso. return falsees lo que debería haber estado allí, corrigiendo ahora.
Andrei Rînea

Una razón interesante para usar una estructura. He realizado clases con GetHashCode y Equals definidos de manera similar a lo que muestra aquí, pero siempre tuve que tener cuidado de no mutar esas instancias si las usaba como claves de diccionario. Probablemente habría sido más seguro si los hubiera definido como estructuras. (Porque entonces la clave sería una copia de los campos en el momento en que la estructura se convirtió en una clave de diccionario , por lo clave se mantendría sin cambios si más tarde cambié el original.)
ToolmakerSteve

En su ejemplo, está bien porque solo tiene 12 Bytes, pero tenga en cuenta que si tiene muchos campos en esa estructura que superan los 16 Bytes, debe considerar usar una clase y anular los métodos GetHashCode e Equals.
Daniel Botero Correa

Un tipo de valor en DDD no significa que necesariamente deba usar un tipo de valor en C #
Darragh

26

Bill Wagner tiene un capítulo sobre esto en su libro "efectivo C #" ( http://www.amazon.com/Effective-Specific-Ways-Improve-Your/dp/0321245660 ). Concluye utilizando el siguiente principio:

  1. ¿Es la responsabilidad principal del tipo de almacenamiento de datos?
  2. ¿Su interfaz pública está definida completamente por propiedades que acceden o modifican sus miembros de datos?
  3. ¿Estás seguro de que tu tipo nunca tendrá subclases?
  4. ¿Estás seguro de que tu tipo nunca será tratado polimórficamente?

Si responde 'sí' a las 4 preguntas: use una estructura. De lo contrario, use una clase.


1
entonces ... ¿los objetos de transferencia de datos (DTO) deberían ser estructuras?
Cruiser

1
Yo diría que sí, si cumple con los 4 criterios descritos anteriormente. ¿Por qué un objeto de transferencia de datos necesita ser tratado de una manera específica?
Bart Gijssens

2
@cruizer depende de tu situación. En un proyecto, teníamos campos de auditoría comunes en nuestros DTO y, por lo tanto, escribimos un DTO base del que otros heredaron.
Andrew Grothe

1
Todos menos (2) parecen excelentes principios. Necesitaría ver su razonamiento para saber exactamente qué quiere decir con (2) y por qué.
ToolmakerSteve

1
@ToolmakerSteve: Tendrá que leer el libro para eso. No piense que es justo copiar / pegar grandes porciones de un libro.
Bart Gijssens


12

Usaría estructuras cuando:

  1. se supone que un objeto es de solo lectura (cada vez que pasa / asigna una estructura se copia). Los objetos de solo lectura son excelentes cuando se trata de procesamiento multiproceso, ya que no requieren bloqueo en la mayoría de los casos.

  2. un objeto es pequeño y de corta vida. En tal caso, existe una buena posibilidad de que el objeto se asigne en la pila, lo que es mucho más eficiente que colocarlo en el montón administrado. Además, la memoria asignada por el objeto se liberará tan pronto como salga de su alcance. En otras palabras, es menos trabajo para Garbage Collector y la memoria se usa de manera más eficiente.


10

Use una clase si:

  • Su identidad es importante. Las estructuras se copian implícitamente cuando se pasan por valor a un método.
  • Tendrá una gran huella de memoria.
  • Sus campos necesitan inicializadores.
  • Necesita heredar de una clase base.
  • Necesitas comportamiento polimórfico;

Use una estructura si:

  • Actuará como un tipo primitivo (int, long, byte, etc.).
  • Debe tener una pequeña huella de memoria.
  • Está llamando a un método P / Invoke que requiere que se pase una estructura por valor.
  • Debe reducir el impacto de la recolección de basura en el rendimiento de la aplicación.
  • Sus campos deben inicializarse solo a sus valores predeterminados. Este valor sería cero para los tipos numéricos, falso para los tipos booleanos y nulo para los tipos de referencia.
    • Tenga en cuenta que en C # 6.0 las estructuras pueden tener un constructor predeterminado que se puede usar para inicializar los campos de la estructura a valores no predeterminados.
  • No necesita heredar de una clase base (que no sea ValueType, de la que heredan todas las estructuras).
  • No necesitas un comportamiento polimórfico.

5

Siempre he usado una estructura cuando quería agrupar algunos valores para pasar cosas de una llamada a método, pero no necesitaré usarla para nada después de haber leído esos valores. Solo como una forma de mantener las cosas limpias. Tiendo a ver las cosas en una estructura como "desechables" y las cosas en una clase como más útiles y "funcionales"


4

Si una entidad va a ser inmutable, la cuestión de si usar una estructura o una clase generalmente será de rendimiento en lugar de semántica. En un sistema de 32/64 bits, las referencias de clase requieren 4/8 bytes para almacenarse, independientemente de la cantidad de información en la clase; copiar una referencia de clase requerirá copiar 4/8 bytes. Por otro lado, cada distintivoLa instancia de clase tendrá 8/16 bytes de sobrecarga además de la información que contiene y el costo de memoria de las referencias a ella. Supongamos que uno quiere una matriz de 500 entidades, cada una con cuatro enteros de 32 bits. Si la entidad es un tipo de estructura, la matriz requerirá 8,000 bytes independientemente de si las 500 entidades son todas idénticas, todas diferentes o en algún punto intermedio. Si la entidad es un tipo de clase, la matriz de 500 referencias tomará 4,000 bytes. Si todas esas referencias apuntan a objetos diferentes, los objetos requerirían 24 bytes adicionales cada uno (12,000 bytes para los 500), un total de 16,000 bytes, el doble del costo de almacenamiento de un tipo de estructura. Por otro lado, del código creó una instancia de objeto y luego copió una referencia a las 500 ranuras de matriz, el costo total sería de 24 bytes para esa instancia y 4, 000 para la matriz: un total de 4.024 bytes. Un gran ahorro. Pocas situaciones funcionarían tan bien como la última, pero en algunos casos puede ser posible copiar algunas referencias a suficientes ranuras de matriz para que valga la pena compartirlas.

Si se supone que la entidad es mutable, la cuestión de si se debe usar una clase o estructura es de alguna manera más fácil. Suponga que "Thing" es una estructura o clase que tiene un campo entero llamado x, y uno hace el siguiente código:

  Cosa t1, t2;
  ...
  t2 = t1;
  t2.x = 5;

¿Se quiere que la última declaración afecte a t1.x?

Si Thing es un tipo de clase, t1 y t2 serán equivalentes, lo que significa que t1.xy t2.x también serán equivalentes. Por lo tanto, la segunda declaración afectará a t1.x. Si Thing es un tipo de estructura, t1 y t2 serán instancias diferentes, lo que significa que t1.xy t2.x se referirán a enteros diferentes. Por lo tanto, la segunda declaración no afectará a t1.x.

Las estructuras mutables y las clases mutables tienen comportamientos fundamentalmente diferentes, aunque .net tiene algunas peculiaridades en su manejo de mutaciones de estructura. Si uno quiere un comportamiento de tipo de valor (lo que significa que "t2 = t1" copiará los datos de t1 a t2 mientras deja t1 y t2 como instancias distintas), y si uno puede vivir con las peculiaridades en el manejo de tipos de valor de .net, use una estructura. Si uno quiere una semántica de tipo de valor pero las peculiaridades de .net causarían una semántica de tipo de valor rota en la aplicación, use una clase y murmure.


3

Además, las excelentes respuestas anteriores:

Las estructuras son tipos de valor.

Nunca se pueden establecer en Nothing .

Establecer una estructura = Nada, establecerá todos sus tipos de valores a sus valores predeterminados.


2

cuando realmente no necesita comportamiento, pero necesita más estructura que una simple matriz o diccionario.

Seguimiento Así es como pienso en las estructuras en general. Sé que pueden tener métodos, pero me gusta mantener esa distinción mental general.


¿Por qué dices eso? Las estructuras pueden tener métodos.
Esteban Araya

2

Como dijo @Simon, las estructuras proporcionan una semántica de "tipo de valor", por lo que si necesita un comportamiento similar a un tipo de datos incorporado, use una estructura. Como las estructuras se pasan por copia, debe asegurarse de que sean pequeñas, de unos 16 bytes.


2

Es un tema antiguo, pero quería proporcionar una prueba de referencia simple.

He creado dos archivos .cs:

public class TestClass
{
    public long ID { get; set; }
    public string FirstName { get; set; }
    public string LastName { get; set; }
}

y

public struct TestStruct
{
    public long ID { get; set; }
    public string FirstName { get; set; }
    public string LastName { get; set; }
}

Ejecutar punto de referencia:

  • Crear 1 clase de prueba
  • Crear 1 TestStruct
  • Crear 100 TestClass
  • Crear 100 TestStruct
  • Crear 10000 TestClass
  • Crear 10000 TestStruct

Resultados:

BenchmarkDotNet=v0.12.0, OS=Windows 10.0.18362
Intel Core i5-8250U CPU 1.60GHz (Kaby Lake R), 1 CPU, 8 logical and 4 physical cores
.NET Core SDK=3.1.101
[Host]     : .NET Core 3.1.1 (CoreCLR 4.700.19.60701, CoreFX 4.700.19.60801), X64 RyuJIT  [AttachedDebugger]
DefaultJob : .NET Core 3.1.1 (CoreCLR 4.700.19.60701, CoreFX 4.700.19.60801), X64 RyuJIT


|         Method |           Mean |         Error |        StdDev |     Ratio | RatioSD | Rank |    Gen 0 | Gen 1 | Gen 2 | Allocated |
|--------------- |---------------:|--------------:|--------------:|----------:|--------:|-----:|---------:|------:|------:|----------:|

|      UseStruct |      0.0000 ns |     0.0000 ns |     0.0000 ns |     0.000 |    0.00 |    1 |        - |     - |     - |         - |
|       UseClass |      8.1425 ns |     0.1873 ns |     0.1839 ns |     1.000 |    0.00 |    2 |   0.0127 |     - |     - |      40 B |
|   Use100Struct |     36.9359 ns |     0.4026 ns |     0.3569 ns |     4.548 |    0.12 |    3 |        - |     - |     - |         - |
|    Use100Class |    759.3495 ns |    14.8029 ns |    17.0471 ns |    93.144 |    3.24 |    4 |   1.2751 |     - |     - |    4000 B |
| Use10000Struct |  3,002.1976 ns |    25.4853 ns |    22.5920 ns |   369.664 |    8.91 |    5 |        - |     - |     - |         - |
|  Use10000Class | 76,529.2751 ns | 1,570.9425 ns | 2,667.5795 ns | 9,440.182 |  346.76 |    6 | 127.4414 |     - |     - |  400000 B |

1

Hmm ...

No usaría la recolección de basura como argumento a favor / en contra del uso de estructuras vs clases. El montón administrado funciona de manera muy similar a una pila: crear un objeto simplemente lo coloca en la parte superior del montón, que es casi tan rápido como asignarlo en la pila. Además, si un objeto es de corta duración y no sobrevive a un ciclo de GC, la desasignación es gratuita, ya que el GC solo funciona con memoria que aún es accesible. (Busque MSDN, hay una serie de artículos sobre administración de memoria .NET, soy demasiado vago para ir a buscarlos).

La mayoría de las veces uso una estructura, termino pateándome por hacerlo, porque luego descubro que tener una semántica de referencia habría hecho las cosas un poco más simples.

De todos modos, esos cuatro puntos en el artículo de MSDN publicado anteriormente parecen una buena guía.


1
Si a veces necesita semántica de referencia con una estructura, simplemente declare class MutableHolder<T> { public T Value; MutableHolder(T value) {Value = value;} }, y luego MutableHolder<T>será un objeto con semántica de clase mutable (esto funciona igual de bien si Tes una estructura o un tipo de clase inmutable).
supercat

1

Las estructuras están en la pila, no en el montón, por lo tanto, son seguras para subprocesos y deben usarse al implementar el patrón de objetos de transferencia, nunca querrá usar objetos en el montón, son volátiles, en este caso desea usar la pila de llamadas, Este es un caso básico para usar una estructura. Estoy sorprendido por las respuestas completas aquí,


-3

Creo que la mejor respuesta es simplemente usar struct cuando lo que necesita es una colección de propiedades, clase cuando es una colección de propiedades Y comportamientos.


Las estructuras también pueden tener métodos
Nithin Chandran

por supuesto, pero si necesita métodos, las posibilidades son del 99%, está utilizando incorrectamente struct en lugar de class. Las únicas excepciones que encontré cuando está bien tener métodos en struct son devoluciones de llamada
Lucian Gabriel Popescu
Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.