Primero, hablemos sobre los Tipos de referencia y los Tipos de valor. Creo que la gente realmente no entiende las diferencias y los beneficios de ambos. Por lo general, dicen que los tipos de referencia almacenan contenido en el montón y los tipos de valor almacenan contenido en la pila, lo cual es incorrecto.
Discutamos las diferencias reales:
- Un tipo de valor : su valor es una estructura completa . El valor de un tipo de referencia es una referencia a un objeto. - Una estructura en la memoria: los tipos de valores contienen solo los datos que indicó. Los tipos de referencia también contienen dos campos del sistema. El primero almacena 'SyncBlockIndex', el segundo almacena la información sobre un tipo, incluida la información sobre una Tabla de métodos virtuales (VMT).
- Los tipos de referencia pueden tener métodos que se anulan cuando se heredan. Los tipos de valor no se pueden heredar.
- Debe asignar espacio en el montón para una instancia de un tipo de referencia. Se puede asignar un tipo de valor en la pila, o se convierte en parte de un tipo de referencia. Esto aumenta suficientemente el rendimiento de algunos algoritmos.
Sin embargo, hay características comunes:
- Ambas subclases pueden heredar el tipo de objeto y convertirse en sus representantes.
Miremos más de cerca cada característica.
Este capítulo fue traducido del ruso conjuntamente por el autor y por traductores profesionales . Puede ayudarnos con la traducción del ruso o el inglés a cualquier otro idioma, principalmente al chino o al alemán.
Además, si quieres agradecernos, la mejor manera de hacerlo es darnos una estrella en Github o bifurcar el repositorio
github / sidristij / dotnetbook .

Miremos más de cerca cada característica.
Copiando
La principal diferencia entre los dos tipos es la siguiente:
- Cada campo de variable, clase o estructura o parámetros de método que toman un tipo de referencia almacenan una referencia a un valor;
- Pero cada variable, clase o campo de estructura o parámetros de método que toman un tipo de valor almacenan un valor exactamente, es decir, una estructura completa.
Esto significa que asignar o pasar un parámetro a un método copiará el valor. Incluso si cambia la copia, el original permanecerá igual. Sin embargo, si cambia los campos de tipo de referencia, esto "afectará" todas las partes con una referencia a una instancia de un tipo. Veamos el
ejemplo:
DateTime dt = DateTime.Now;
Parece que esta propiedad produce construcciones de código ambiguas como el
cambio de código en colecciones:
// Let's declare a structure struct ValueHolder { public int Data; } // Let's create an array of such structures and initialize the Data field = 5 var array = new [] { new ValueHolder { Data = 5 } }; // Let's use an index to get the structure and put 4 in the Data field array[0].Data = 4; // Let's check the value Console.WriteLine(array[0].Data);
Hay un pequeño truco en este código. Parece que primero obtenemos la instancia de estructura y luego asignamos un nuevo valor al campo Datos de la copia. Esto significa que deberíamos obtener 5
nuevamente al verificar el valor. Sin embargo, esto no sucede. MSIL tiene una instrucción separada para establecer los valores de los campos en las estructuras de una matriz, lo que aumenta el rendimiento. El código funcionará según lo previsto: el programa
salida 4
a una consola.
Veamos qué sucederá si cambiamos este código:
// Let's declare a structure struct ValueHolder { public int Data; } // Let's create a list of such structures and initialize the Data field = 5 var list = new List<ValueHolder> { new ValueHolder { Data = 5 } }; // Let's use an index to get the structure and put 4 in the Data field list[0].Data = 4; // Let's check the value Console.WriteLine(list[0].Data);
La compilación de este código fallará, porque cuando escribe list[0].Data = 4
obtiene primero la copia de la estructura. De hecho, está llamando a un método de instancia del tipo List<T>
que subyace al acceso mediante un índice. Toma la copia de una estructura de una matriz interna ( List<T>
almacena datos en matrices) y le devuelve esta copia del método de acceso utilizando un índice. A continuación, intenta modificar la copia, que no se usa más adelante. Este código no tiene sentido. Un compilador prohíbe tal comportamiento, sabiendo que las personas usan mal los tipos de valor. Deberíamos reescribir este ejemplo de la siguiente manera:
// Let's declare a structure struct ValueHolder { public int Data; } // Let's create a list of such structures and initialize the Data field = 5 var list = new List<ValueHolder> { new ValueHolder { Data = 5 } }; // Let's use an index to get the structure and put 4 in the Data field. Then, let's save it again. var copy = list[0]; copy.Data = 4; list[0] = copy; // Let's check the value Console.WriteLine(list[0].Data);
Este código es correcto a pesar de su aparente redundancia. El programa
salida 4
a una consola.
El siguiente ejemplo muestra lo que quiero decir con "el valor de una estructura es un
estructura completa "
// Variant 1 struct PersonInfo { public int Height; public int Width; public int HairColor; } int x = 5; PersonInfo person; int y = 6; // Variant 2 int x = 5; int Height; int Width; int HairColor; int y = 6;
Ambos ejemplos son similares en términos de la ubicación de los datos en la memoria, ya que el valor de la estructura es la estructura completa. Se asigna la memoria para sí mismo donde está.
// Variant 1 struct PersonInfo { public int Height; public int Width; public int HairColor; } class Employee { public int x; public PersonInfo person; public int y; } // Variant 2 class Employee { public int x; public int Height; public int Width; public int HairColor; public int y; }
Estos ejemplos también son similares en términos de la ubicación de los elementos en la memoria, ya que la estructura ocupa un lugar definido entre los campos de clase. No digo que sean totalmente similares, ya que puede operar campos de estructura utilizando métodos de estructura.
Por supuesto, este no es el caso de los tipos de referencia. Una instancia en sí está en el montón de objetos pequeños inalcanzable (SOH) o en el montón de objetos grandes (LOH). Un campo de clase solo contiene el valor de un puntero a una instancia: un número de 32 o 64 bits.
Veamos el último ejemplo para cerrar el problema.
// Variant 1 struct PersonInfo { public int Height; public int Width; public int HairColor; } void Method(int x, PersonInfo person, int y); // Variant 2 void Method(int x, int HairColor, int Width, int Height, int y);
En términos de memoria, ambas variantes de código funcionarán de manera similar, pero no en términos de arquitectura. No es solo un reemplazo de un número variable de argumentos. El orden cambia porque los parámetros del método se declaran uno tras otro. Se ponen en la pila de la misma manera.
Sin embargo, la pila crece de direcciones más altas a más bajas. Eso significa que el orden de empujar una estructura pieza por pieza será diferente de empujarla como un todo.
Métodos reemplazables y herencia
La siguiente gran diferencia entre los dos tipos es la falta de virtual
tabla de métodos en estructuras. Esto significa que:
- No puede describir y anular métodos virtuales en estructuras.
- Una estructura no puede heredar otra. La única forma de emular la herencia es poner una estructura de tipo base en el primer campo. Los campos de una estructura "heredada" irán después de los campos de una estructura "base" y creará una herencia lógica. Los campos de ambas estructuras coincidirán en función del desplazamiento.
- Puede pasar estructuras a código no administrado. Sin embargo, perderá la información sobre los métodos. Esto se debe a que una estructura es solo espacio en la memoria, llena de datos sin la información sobre un tipo. Puede pasarlo a métodos no administrados, por ejemplo, escritos en C ++, sin cambios.
La falta de una tabla de métodos virtuales resta una cierta parte de la herencia "mágica" de las estructuras, pero les da otras ventajas. La primera es que podemos pasar instancias de dicha estructura a entornos externos (fuera de .NET Framework). Recuerda, esto es solo un recuerdo
rango! También podemos tomar un rango de memoria del código no administrado y emitir un tipo a nuestra estructura para que sus campos sean más accesibles. No puede hacer esto con clases ya que tienen dos campos inaccesibles. Estos son SyncBlockIndex y una dirección de tabla de métodos virtuales. Si esos dos campos pasan al código no administrado, será peligroso. Usando una tabla de métodos virtuales, uno puede acceder a cualquier tipo y cambiarlo para atacar una aplicación.
Vamos a mostrar que es solo un rango de memoria sin lógica adicional.
unsafe void Main() { int secret = 666; HeightHolder hh; hh.Height = 5; WidthHolder wh; unsafe { // This cast wouldn't work if structures had the information about a type. // The CLR would check a hierarchy before casting a type and if it didn't find WidthHolder, // it would output an InvalidCastException exception. But since a structure is a memory range, // you can interpret it as any kind of structure. wh = *(WidthHolder*)&hh; } Console.WriteLine("Width: " + wh.Width); Console.WriteLine("Secret:" + wh.Secret); } struct WidthHolder { public int Width; public int Secret; } struct HeightHolder { public int Height; }
Aquí, realizamos la operación que es imposible en tipeo fuerte. Lanzamos un tipo a otro incompatible que contiene un campo adicional. Introducimos una variable adicional dentro del método Main. En teoría, su valor es secreto. Sin embargo, el código de ejemplo generará el valor de una variable, que no se encuentra en ninguna de las estructuras dentro del método Main()
. Puede considerarlo una violación de la seguridad, pero las cosas no son tan simples. No puede deshacerse del código no administrado en un programa. La razón principal es la estructura de la pila de hilos. Se puede usar para acceder al código no administrado y jugar con variables locales. Puede defender su código de estos ataques aleatorizando el tamaño de un marco de pila. O bien, puede eliminar la información sobre el registro EBP
para complicar el retorno de un marco de pila. Sin embargo, esto no nos importa ahora. Lo que nos interesa en este ejemplo es lo siguiente. La variable "secreta" va antes de la definición de la variable hh y luego en la estructura WidthHolder (en diferentes lugares, en realidad). Entonces, ¿por qué obtuvimos fácilmente su valor? La respuesta es que la pila crece de derecha a izquierda. Las variables declaradas primero tendrán direcciones mucho más altas, y las declaradas después tendrán direcciones más bajas.
El comportamiento al llamar a métodos de instancia
Ambos tipos de datos tienen otra característica que no es fácil de ver y puede explicar la estructura de ambos tipos. Se trata de llamar a los métodos de instancia.
// The example with a reference type class FooClass { private int x; public void ChangeTo(int val) { x = val; } } // The example with a value type struct FooStruct { private int x; public void ChangeTo(int val) { x = val; } } FooClass klass = new FooClass(); FooStruct strukt = new FooStruct(); klass.ChangeTo(10); strukt.ChangeTo(10);
Lógicamente, podemos decidir que el método tiene un cuerpo compilado. En otras palabras, no hay una instancia de un tipo que tenga su propio conjunto compilado de métodos, similar a los conjuntos de otras instancias. Sin embargo, el método llamado sabe a qué instancia pertenece como referencia a la instancia de un tipo que es el primer parámetro. Podemos reescribir nuestro ejemplo y será idéntico a lo que dijimos antes. No estoy usando un ejemplo con métodos virtuales deliberadamente, ya que tienen otro procedimiento.
// An example with a reference type class FooClass { public int x; } // An example with a value type struct FooStruct { public int x; } public void ChangeTo(FooClass klass, int val) { klass.x = val; } public void ChangeTo(ref FooStruct strukt, int val) { strukt.x = val; } FooClass klass = new FooClass(); FooStruct strukt = new FooStruct(); ChangeTo(klass, 10); ChangeTo(ref strukt, 10);
Debería explicar el uso de la palabra clave ref. Si no lo usara, obtendría una copia de la estructura como parámetro del método en lugar del original. Luego lo cambiaría, pero el original se mantendría igual. Tendría que devolver una copia modificada de un método a una persona que llama (otra copia), y la persona que llama guardaría este valor nuevamente en la variable (una copia más). En cambio, un método de instancia obtiene un puntero y lo usa para cambiar el original de inmediato. El uso de un puntero no influye en el rendimiento, ya que cualquier operación a nivel de procesador utiliza punteros. Ref es parte del mundo de C #, no más.
La capacidad de señalar la posición de los elementos.
Tanto las estructuras como las clases tienen otra capacidad para señalar el desplazamiento de un campo en particular con respecto al comienzo de una estructura en la memoria. Esto sirve para varios propósitos:
- para trabajar con API externas en el mundo no administrado sin tener que insertar campos no utilizados antes de uno necesario;
- para indicar a un compilador que ubique un campo justo al comienzo del tipo (
[FieldOffset(0)]
). Hará el trabajo con este tipo más rápido. Si es un campo de uso frecuente, podemos aumentar el rendimiento de la aplicación. Sin embargo, esto es cierto solo para los tipos de valor. En los tipos de referencia, el campo con un desplazamiento cero contiene la dirección de una tabla de métodos virtuales, que toma 1 palabra de máquina. Incluso si aborda el primer campo de una clase, utilizará un direccionamiento complejo (dirección + desplazamiento). Esto se debe a que el campo de clase más utilizado es la dirección de una tabla de métodos virtuales. La tabla es necesaria para llamar a todos los métodos virtuales; - para señalar varios campos usando una dirección. En este caso, el mismo valor se interpreta como diferentes tipos de datos. En C ++, este tipo de datos se denomina unión;
- no molestarse en declarar nada: un compilador asignará los campos de manera óptima. Por lo tanto, el orden final de los campos puede ser diferente.
Observaciones generales
- Automático : el entorno de tiempo de ejecución elige automáticamente una ubicación y un empaque para todos los campos de clase o estructura. Las estructuras definidas que están marcadas por un miembro de esta enumeración no pueden pasar a código no administrado. El intento de hacerlo producirá una excepción;
- Explícito : un programador controla explícitamente la ubicación exacta de cada campo de un tipo con FieldOffsetAttribute;
- Secuencial : los miembros de tipo vienen en un orden secuencial, definido durante el diseño de tipo. El valor StructLayoutAttribute.Pack de un paso de empaquetado indica su ubicación.
Usar FieldOffset para omitir los campos de estructura no utilizados
Las estructuras que provienen del mundo no administrado pueden contener campos reservados. Se pueden usar en una versión futura de una biblioteca. En C / C ++ llenamos estos huecos agregando campos, por ejemplo, reservado1, reservado2, ... Sin embargo, en .NET solo compensamos al comienzo de un campo usando el atributo FieldOffsetAttribute y [StructLayout(LayoutKind.Explicit)]
.
[StructLayout(LayoutKind.Explicit)] public struct SYSTEM_INFO { [FieldOffset(0)] public ulong OemId; // 92 bytes reserved [FieldOffset(100)] public ulong PageSize; [FieldOffset(108)] public ulong ActiveProcessorMask; [FieldOffset(116)] public ulong NumberOfProcessors; [FieldOffset(124)] public ulong ProcessorType; }
Hay un espacio ocupado pero espacio no utilizado. La estructura tendrá un tamaño igual a 132 y no 40 bytes, como puede parecer desde el principio.
Unión
Usando FieldOffsetAttribute puede emular el tipo C / C ++ llamado unión. Permite acceder a los mismos datos que las entidades de
diferentes tipos Veamos el ejemplo:
// If we read the RGBA.Value, we will get an Int32 value accumulating all // other fields. // However, if we try to read the RGBA.R, RGBA.G, RGBA.B, RGBA.Alpha, we // will get separate components of Int32. [StructLayout(LayoutKind.Explicit)] public struct RGBA { [FieldOffset(0)] public uint Value; [FieldOffset(0)] public byte R; [FieldOffset(1)] public byte G; [FieldOffset(2)] public byte B; [FieldOffset(3)] public byte Alpha; }
Se podría decir que tal comportamiento es posible solo para los tipos de valor. Sin embargo, puede simularlo para los tipos de referencia, utilizando una dirección para superponer dos tipos de referencia o un tipo de referencia y un tipo de valor:
class Program { public static void Main() { Union x = new Union(); x.Reference.Value = "Hello!"; Console.WriteLine(x.Value.Value); } [StructLayout(LayoutKind.Explicit)] public class Union { public Union() { Value = new Holder<IntPtr>(); Reference = new Holder<object>(); } [FieldOffset(0)] public Holder<IntPtr> Value; [FieldOffset(0)] public Holder<object> Reference; } public class Holder<T> { public T Value; } }
Usé un tipo genérico para superponer a propósito. Si solia usar
superpuesto, este tipo provocaría la TypeLoadException cuando se carga en un dominio de aplicación. En teoría, puede parecer una violación de seguridad (especialmente cuando se habla de complementos de aplicaciones ), pero si intentamos ejecutar este código usando un dominio protegido, obtendremos la misma TypeLoadException
.
La diferencia en la asignación
Otra característica que diferencia ambos tipos es la asignación de memoria para objetos o estructuras. El CLR debe decidir sobre varias cosas antes de asignar memoria para un objeto. ¿Cuál es el tamaño de un objeto? ¿Es más o menos de 85K? Si es menor, ¿hay suficiente espacio libre en el SOH para asignar este objeto? Si es más, el CLR activa el recolector de basura. Atraviesa un gráfico de objetos, compacta los objetos moviéndolos al espacio despejado. Si todavía no hay espacio en el SOH, se iniciará la asignación de páginas de memoria virtual adicionales. Es solo entonces que un objeto obtiene espacio asignado, despejado de basura. Posteriormente, el CLR presenta SyncBlockIndex y VirtualMethodsTable. Finalmente, la referencia a un objeto vuelve a un usuario.
Si un objeto asignado es mayor que 85K, se dirige al Montón de objetos grandes (LOH). Este es el caso de grandes cadenas y matrices. Aquí, debemos encontrar el espacio más adecuado en la memoria de la lista de rangos desocupados o asignar uno nuevo. No es rápido, pero vamos a tratar con cuidado los objetos de tal tamaño. Además, no vamos a hablar de ellos aquí.
Hay varios escenarios posibles para RefTypes:
- RefType <85K, hay espacio en el SOH: asignación rápida de memoria;
- RefType <85K, el espacio en el SOH se está agotando: asignación de memoria muy lenta;
- RefType> 85K, asignación de memoria lenta.
Tales operaciones son raras y no pueden competir con ValTypes. El algoritmo de asignación de memoria para tipos de valor no existe. La asignación de memoria para los tipos de valor no cuesta nada. Lo único que sucede cuando se asigna memoria para este tipo es establecer campos en nulo. Veamos por qué sucede esto: 1. Cuando se declara una variable en el cuerpo de un método, el tiempo de asignación de memoria para una estructura es cercano a cero. Esto se debe a que el tiempo de asignación para las variables locales no depende de su número; 2. Si ValTypes se asignan como campos, Reftypes aumentará el tamaño de los campos. Un tipo de valor se asigna por completo, convirtiéndose en su parte; 3. Como en el caso de la copia, si ValTypes se pasan como parámetros de método, aparece una diferencia, dependiendo del tamaño y la ubicación de un parámetro.
Sin embargo, eso no lleva más tiempo que copiar una variable en otra.
La elección entre una clase o una estructura.
Analicemos las ventajas y desventajas de ambos tipos y decidamos sus escenarios de uso. Un principio clásico dice que deberíamos elegir un tipo de valor si no es mayor de 16 bytes, permanece sin cambios durante su vida útil y no se hereda. Sin embargo, elegir el tipo correcto significa revisarlo desde diferentes perspectivas basándose en escenarios de uso futuro. Propongo tres grupos de criterios:
- basado en la arquitectura del sistema de tipos, en el que su tipo interactuará;
- basado en su enfoque como programador de sistemas para elegir un tipo con un rendimiento óptimo;
- cuando no hay otra opción
Cada característica diseñada debe reflejar su propósito. Esto no trata solo con su nombre o interfaz de interacción (métodos, propiedades). Se pueden usar consideraciones arquitectónicas para elegir entre valores y tipos de referencia. Pensemos por qué podría elegirse una estructura y no una clase desde el punto de vista del sistema de sistema de tipos.
Si su tipo diseñado es independiente de su estado, esto significará que su estado refleja un proceso o es un valor de algo. En otras palabras, una instancia de un tipo es constante e inmutable por naturaleza. Podemos crear otra instancia de un tipo basada en esta constante indicando algún desplazamiento. O bien, podemos crear una nueva instancia indicando sus propiedades. Sin embargo, no debemos cambiarlo. No quiero decir que la estructura sea de un tipo inmutable. Puede cambiar sus valores de campo. Además, puede pasar una referencia a una estructura a un método utilizando el parámetro ref y obtendrá campos cambiados después de salir del método. De lo que hablo aquí es del sentido arquitectónico. Daré varios ejemplos.
- DateTime es una estructura que encapsula el concepto de un momento en el tiempo. Almacena estos datos como una unidad pero da acceso a características separadas de un momento: año, mes, día, hora, minutos, segundos, milisegundos e incluso tics de procesador. Sin embargo, es inmutable, basándose en lo que encapsula. No podemos cambiar un momento en el tiempo. No puedo vivir el siguiente minuto como si fuera mi mejor cumpleaños en la infancia. Por lo tanto, si elegimos un tipo de datos, podemos elegir una clase con una interfaz de solo lectura, que produce una nueva instancia para cada cambio de propiedades. O bien, podemos elegir una estructura, que puede pero no debe cambiar los campos de sus instancias: su valor es la descripción de un momento en el tiempo, como un número. No puede acceder a la estructura de un número y cambiarlo. Si desea obtener otro momento en el tiempo, que difiere en un día del original, obtendrá una nueva instancia de una estructura.
KeyValuePair<TKey, TValue>
es una estructura que encapsula el concepto de un par clave-valor conectado. Esta estructura es solo para generar el contenido de un diccionario durante la enumeración. Desde el punto de vista arquitectónico, una clave y un valor son conceptos inseparables en el Dictionary<T>
. Sin embargo, en el interior tenemos una estructura compleja, donde una clave se encuentra por separado de un valor. Para un usuario, un par clave-valor es un concepto inseparable en términos de interfaz y el significado de una estructura de datos. Es un valor completo en sí mismo. Si uno asigna otro valor para una clave, todo el par cambiará. Por lo tanto, representan una sola entidad. Esto hace que una estructura sea una variante ideal en este caso.
Si su tipo diseñado es una parte inseparable de un tipo externo pero es estructuralmente integral. Eso significa que es incorrecto decir que el tipo externo se refiere a una instancia de un tipo encapsulado. Sin embargo, es correcto decir que un tipo encapsulado es parte de un externo junto con todas sus propiedades. Esto es útil al diseñar una estructura que es parte de otra estructura.
- Por ejemplo, si tomamos una estructura de un encabezado de archivo, será inapropiado pasar una referencia de un archivo a otro, por ejemplo, un archivo header.txt. Esto sería apropiado al insertar un documento en otro, no incrustando un archivo sino usando una referencia en un sistema de archivos. Un buen ejemplo son los archivos de acceso directo en el sistema operativo Windows. Sin embargo, si hablamos de un encabezado de archivo (por ejemplo, encabezado de archivo JPEG que contiene metadatos sobre el tamaño de una imagen, métodos de compresión, parámetros de fotografía, coordenadas GPS y otros), entonces deberíamos usar estructuras para diseñar tipos para analizar el encabezado. Si describe todos los encabezados en las estructuras, obtendrá la misma posición de los campos en la memoria que en un archivo. Usando la transformación insegura
*(Header *)readedBuffer
insegura *(Header *)readedBuffer
sin deserialización obtendrá estructuras de datos completamente llenas.
- Ningún ejemplo muestra la herencia del comportamiento. Muestran que no hay necesidad de heredar el comportamiento de estas entidades. Son independientes. Sin embargo, si tomamos en cuenta la efectividad del código, veremos la elección desde otro lado:
- Si necesitamos tomar algunos datos estructurados del código no administrado, debemos elegir estructuras. También podemos pasar la estructura de datos a un método inseguro. Un tipo de referencia no es adecuado para esto en absoluto.
- Una estructura es su elección si un tipo pasa los datos en llamadas a métodos (como valores devueltos o como parámetro de método) y no hay necesidad de referirse al mismo valor desde diferentes lugares. El ejemplo perfecto son las tuplas. Si un método devuelve varios valores usando tuplas, devolverá un ValueTuple, declarado como una estructura. El método no asignará espacio en el montón, pero utilizará la pila del subproceso, donde la asignación de memoria no cuesta nada.
- Si diseña un sistema que crea un gran tráfico de instancias que tienen un tamaño y una vida útil pequeños, el uso de tipos de referencia conducirá a un grupo de objetos o, si no es el grupo de objetos, a una acumulación de basura no controlada en el montón. Algunos objetos se convertirán en generaciones anteriores, aumentando la carga en GC. El uso de tipos de valor en dichos lugares (si es posible) aumentará el rendimiento porque nada pasará al SOH. Esto disminuirá la carga en GC y el algoritmo funcionará más rápido;
Basándome en lo que he dicho, aquí hay algunos consejos sobre el uso de estructuras:
- Al elegir colecciones, debe evitar las grandes matrices que almacenan grandes estructuras. Esto incluye estructuras de datos basadas en matrices. Esto puede conducir a una transición al montón de objetos grandes y su fragmentación. Es un error pensar que si nuestra estructura tiene 4 campos del tipo byte, tomará 4 bytes. Debemos entender que en los sistemas de 32 bits cada campo de estructura está alineado en límites de 4 bytes (cada campo de dirección debe dividirse exactamente por 4) y en sistemas de 64 bits, en límites de 8 bytes. El tamaño de una matriz debe depender del tamaño de una estructura y una plataforma que ejecute un programa. En nuestro ejemplo con 4 bytes - 85K / (de 4 a 8 bytes por campo * el número de campos = 4) menos el tamaño de un encabezado de matriz equivale a aproximadamente 2 600 elementos por matriz dependiendo de la plataforma (esto debe redondearse hacia abajo ) Eso no es mucho. Puede haber parecido que podríamos alcanzar fácilmente una constante mágica de 20,000 elementos
- A veces, utiliza una estructura de gran tamaño como fuente de datos y la coloca como un campo en una clase, mientras se replica una copia para producir miles de instancias. Luego expande cada instancia de una clase para el tamaño de una estructura. Conducirá a la expansión de la generación cero y la transición a la generación uno e incluso dos. Si las instancias de una clase tienen un período de vida corto y cree que el GC las recopilará en la generación cero, durante 1 ms, se sentirá decepcionado. Ya están en la generación uno e incluso dos. Esto hace la diferencia. Si el GC recolecta la generación cero durante 1 ms, las generaciones uno y dos se recolectan muy lentamente, lo que conducirá a una disminución de la eficiencia;
- Por la misma razón, debe evitar pasar grandes estructuras a través de una serie de llamadas a métodos. Si todos los elementos se llaman entre sí, estas llamadas ocuparán más espacio en la pila y StackOverflowException matará su aplicación. La siguiente razón es el rendimiento. Cuantas más copias haya, más lentamente todo funciona.
Es por eso que la elección de un tipo de datos no es un proceso obvio. A menudo, esto puede referirse a una optimización prematura, que no se recomienda. Sin embargo, si sabe que su situación se encuentra dentro de los principios establecidos anteriormente, puede elegir fácilmente un tipo de valor.
Este capítulo fue traducido del ruso conjuntamente por el autor y por traductores profesionales . Puede ayudarnos con la traducción del ruso o el inglés a cualquier otro idioma, principalmente al chino o al alemán.
Además, si quieres agradecernos, la mejor manera de hacerlo es darnos una estrella en Github o bifurcar el repositorio
github / sidristij / dotnetbook .