Que sé yo... Un lenguaje cuantas más cosas trae, a mi entender más complijidad contrae y más dificil es aclarar ciertos puntos...
Y para colmo se pueden hacer más cosas de diferentes formas, haciendo que se pierda la claridad del mejor camino para resolver las cosas. Puff ... Y sin duda C#, java, scala, kotlin, etc... son muy completos y por ende muy complejos de entender cuando usar que.
Luego de esta introducción/opinión, vamos al tema del post. Cuando utilizar estructuras, registro o clases en C#?
¿Puede el tipo de datos ser un tipo de valor? entonces es un estructura. ¿No? ¿Su tipo describe un estado similar a un valor, preferiblemente inmutable? entonces registro. Y sino clase pero podemos usar registro para:
- DTO si el flujo unidireccional.
- Immutable request
- SearchParameters
Veamos esto con más detalle. Una estructura, una clase y un registro son tipos de datos de usuario.
Las estructuras son tipos de valor. Las clases son tipos de referencia. Los registros son por defecto tipos de referencia inmutables.
Cuando necesita algún tipo de jerarquía para describir sus tipos de datos como herencia o una estructura que apunta a otra estructura o básicamente cosas que apuntan a otras cosas, necesita un tipo de referencia.
Los registros resuelven el problema cuando desea que su tipo esté orientado a valores de forma predeterminada. Los registros son tipos de referencia pero con la semántica orientada al valor.
En conclusión,
Estructura si:
- Representa lógicamente un solo valor, similar a los tipos primitivos (int, double, etc.).
- Tiene un tamaño de instancia inferior a 16 bytes.
- Es inmutable
- No tendrá que ser referenciado con frecuencia.
¿No? Debe ser algún tipo de referencia (clase o registro).
¿El tipo de datos encapsula algún tipo de valor complejo? ¿El valor es inmutable? ¿Lo usa en flujo unidireccional (una vía)? Vamos con registro.
¿No? Vamos con clase.
Por cierto: no te olvides que habrá registros anónimos en C# 10.0, como para ponerle más complejidad.
Otra cosa, una instancia de registro puede ser mutable si la convierte en mutable.
class Program
{
static void Main()
{
var test = new Foo("a");
Console.WriteLine(test.MutableProperty);
test.MutableProperty = 15;
Console.WriteLine(test.MutableProperty);
//test.Bar = "new string"; // will not compile
}
}
public record Foo(string Bar)
{
public double MutableProperty { get; set; } = 10.0;
}
Una asignación de un registro es una copia superficial del registro. Una copia con expresión de un registro no es ni superficial ni profunda. La copia se crea mediante un método de clonación especial emitido por el compilador de C#. Los miembros de tipo de valor se copian y encuadran. Los miembros de tipo de referencia apuntan a la misma referencia. Puede hacer una copia profunda de un registro si y solo si el registro solo tiene propiedades de tipo de valor. Cualquier propiedad de miembro de tipo de referencia de un registro se copia como una copia superficial.
Veamos este ejemplo (usando la función de nivel superior en C# 9.0):
using System.Collections.Generic;
using static System.Console;
var foo = new SomeRecord(new List<string>());
var fooAsShallowCopy = foo;
var fooAsWithCopy = foo with { }; // A syntactic sugar for new SomeRecord(foo.List);
var fooWithDifferentList = foo with { List = new List<string>() { "a", "b" } };
var differentFooWithSameList = new SomeRecord(foo.List); // This is the same like foo with { };
foo.List.Add("a");
WriteLine($"Count in foo: {foo.List.Count}"); // 1
WriteLine($"Count in fooAsShallowCopy: {fooAsShallowCopy.List.Count}"); // 1
WriteLine($"Count in fooWithDifferentList: {fooWithDifferentList.List.Count}"); // 2
WriteLine($"Count in differentFooWithSameList: {differentFooWithSameList.List.Count}"); // 1
WriteLine($"Count in fooAsWithCopy: {fooAsWithCopy.List.Count}"); // 1
WriteLine("");
WriteLine($"Equals (foo & fooAsShallowCopy): {Equals(foo, fooAsShallowCopy)}"); // True. The lists inside are the same.
WriteLine($"Equals (foo & fooWithDifferentList): {Equals(foo, fooWithDifferentList)}"); // False. The lists are different
WriteLine($"Equals (foo & differentFooWithSameList): {Equals(foo, differentFooWithSameList)}"); // True. The list are the same.
WriteLine($"Equals (foo & fooAsWithCopy): {Equals(foo, fooAsWithCopy)}"); // True. The list are the same, see below.
WriteLine($"ReferenceEquals (foo.List & fooAsShallowCopy.List): {ReferenceEquals(foo.List, fooAsShallowCopy.List)}"); // True. The records property points to the same reference.
WriteLine($"ReferenceEquals (foo.List & fooWithDifferentList.List): {ReferenceEquals(foo.List, fooWithDifferentList.List)}"); // False. The list are different instances.
WriteLine($"ReferenceEquals (foo.List & differentFooWithSameList.List): {ReferenceEquals(foo.List, differentFooWithSameList.List)}"); // True. The records property points to the same reference.
WriteLine($"ReferenceEquals (foo.List & fooAsWithCopy.List): {ReferenceEquals(foo.List, fooAsWithCopy.List)}"); // True. The records property points to the same reference.
WriteLine("");
WriteLine($"ReferenceEquals (foo & fooAsShallowCopy): {ReferenceEquals(foo, fooAsShallowCopy)}"); // True. !!! fooAsCopy is pure shallow copy of foo. !!!
WriteLine($"ReferenceEquals (foo & fooWithDifferentList): {ReferenceEquals(foo, fooWithDifferentList)}"); // False. These records are two different reference variables.
WriteLine($"ReferenceEquals (foo & differentFooWithSameList): {ReferenceEquals(foo, differentFooWithSameList)}"); // False. These records are two different reference variables and reference type property hold by these records does not matter in ReferenceEqual.
WriteLine($"ReferenceEquals (foo & fooAsWithCopy): {ReferenceEquals(foo, fooAsWithCopy)}"); // False. The same story as differentFooWithSameList.
WriteLine("");
var bar = new RecordOnlyWithValueNonMutableProperty(0);
var barAsShallowCopy = bar;
var differentBarDifferentProperty = bar with { NonMutableProperty = 1 };
var barAsWithCopy = bar with { };
WriteLine($"Equals (bar & barAsShallowCopy): {Equals(bar, barAsShallowCopy)}"); // True.
WriteLine($"Equals (bar & differentBarDifferentProperty): {Equals(bar, differentBarDifferentProperty)}"); // False. Remember, the value equality is used.
WriteLine($"Equals (bar & barAsWithCopy): {Equals(bar, barAsWithCopy)}"); // True. Remember, the value equality is used.
WriteLine($"ReferenceEquals (bar & barAsShallowCopy): {ReferenceEquals(bar, barAsShallowCopy)}"); // True. The shallow copy.
WriteLine($"ReferenceEquals (bar & differentBarDifferentProperty): {ReferenceEquals(bar, differentBarDifferentProperty)}"); // False. Operator with creates a new reference variable.
WriteLine($"ReferenceEquals (bar & barAsWithCopy): {ReferenceEquals(bar, barAsWithCopy)}"); // False. Operator with creates a new reference variable.
WriteLine("");
var fooBar = new RecordOnlyWithValueMutableProperty();
var fooBarAsShallowCopy = fooBar; // A shallow copy, the reference to bar is assigned to barAsCopy
var fooBarAsWithCopy = fooBar with { }; // A deep copy by coincidence because fooBar has only one value property which is copied into barAsDeepCopy.
WriteLine($"Equals (fooBar & fooBarAsShallowCopy): {Equals(fooBar, fooBarAsShallowCopy)}"); // True.
WriteLine($"Equals (fooBar & fooBarAsWithCopy): {Equals(fooBar, fooBarAsWithCopy)}"); // True. Remember, the value equality is used.
WriteLine($"ReferenceEquals (fooBar & fooBarAsShallowCopy): {ReferenceEquals(fooBar, fooBarAsShallowCopy)}"); // True. The shallow copy.
WriteLine($"ReferenceEquals (fooBar & fooBarAsWithCopy): {ReferenceEquals(fooBar, fooBarAsWithCopy)}"); // False. Operator with creates a new reference variable.
WriteLine("");
fooBar.MutableProperty = 2;
fooBarAsShallowCopy.MutableProperty = 3;
fooBarAsWithCopy.MutableProperty = 3;
WriteLine($"fooBar.MutableProperty = {fooBar.MutableProperty} | fooBarAsShallowCopy.MutableProperty = {fooBarAsShallowCopy.MutableProperty} | fooBarAsWithCopy.MutableProperty = {fooBarAsWithCopy.MutableProperty}"); // fooBar.MutableProperty = 3 | fooBarAsShallowCopy.MutableProperty = 3 | fooBarAsWithCopy.MutableProperty = 3
WriteLine($"Equals (fooBar & fooBarAsShallowCopy): {Equals(fooBar, fooBarAsShallowCopy)}"); // True.
WriteLine($"Equals (fooBar & fooBarAsWithCopy): {Equals(fooBar, fooBarAsWithCopy)}"); // True. Remember, the value equality is used. 3 != 4
WriteLine($"ReferenceEquals (fooBar & fooBarAsShallowCopy): {ReferenceEquals(fooBar, fooBarAsShallowCopy)}"); // True. The shallow copy.
WriteLine($"ReferenceEquals (fooBar & fooBarAsWithCopy): {ReferenceEquals(fooBar, fooBarAsWithCopy)}"); // False. Operator with creates a new reference variable.
WriteLine("");
fooBarAsWithCopy.MutableProperty = 4;
WriteLine($"fooBar.MutableProperty = {fooBar.MutableProperty} | fooBarAsShallowCopy.MutableProperty = {fooBarAsShallowCopy.MutableProperty} | fooBarAsWithCopy.MutableProperty = {fooBarAsWithCopy.MutableProperty}"); // fooBar.MutableProperty = 3 | fooBarAsShallowCopy.MutableProperty = 3 | fooBarAsWithCopy.MutableProperty = 4
WriteLine($"Equals (fooBar & fooBarAsWithCopy): {Equals(fooBar, fooBarAsWithCopy)}"); // False. Remember, the value equality is used. 3 != 4
WriteLine("");
var venom = new MixedRecord(new List<string>(), 0); // Reference/Value property, mutable non-mutable.
var eddieBrock = venom;
var carnage = venom with { };
venom.List.Add("I'm a predator.");
carnage.List.Add("All I ever wanted in this world is a carnage.");
WriteLine($"Count in venom: {venom.List.Count}"); // 2
WriteLine($"Count in eddieBrock: {eddieBrock.List.Count}"); // 2
WriteLine($"Count in carnage: {carnage.List.Count}"); // 2
WriteLine($"Equals (venom & eddieBrock): {Equals(venom, eddieBrock)}"); // True.
WriteLine($"Equals (venom & carnage): {Equals(venom, carnage)}"); // True. Value properties has the same values, the List property points to the same reference.
WriteLine($"ReferenceEquals (venom & eddieBrock): {ReferenceEquals(venom, eddieBrock)}"); // True. The shallow copy.
WriteLine($"ReferenceEquals (venom & carnage): {ReferenceEquals(venom, carnage)}"); // False. Operator with creates a new reference variable.
WriteLine("");
eddieBrock.MutableList = new List<string>();
eddieBrock.MutableProperty = 3;
WriteLine($"Equals (venom & eddieBrock): {Equals(venom, eddieBrock)}"); // True. Reference or value type does not matter. Still a shallow copy of venom, still true.
WriteLine($"Equals (venom & carnage): {Equals(venom, carnage)}"); // False. the venom.List property does not points to the same reference like in carnage.List anymore.
WriteLine($"ReferenceEquals (venom & eddieBrock): {ReferenceEquals(venom, eddieBrock)}"); // True. The shallow copy.
WriteLine($"ReferenceEquals (venom & carnage): {ReferenceEquals(venom, carnage)}"); // False. Operator with creates a new reference variable.
WriteLine($"ReferenceEquals (venom.List & carnage.List): {ReferenceEquals(venom.List, carnage.List)}"); // True. Non mutable reference type.
WriteLine($"ReferenceEquals (venom.MutableList & carnage.MutableList): {ReferenceEquals(venom.MutableList, carnage.MutableList)}"); // False. This is why Equals(venom, carnage) returns false.
WriteLine("");
public record SomeRecord(List<string> List);
public record RecordOnlyWithValueNonMutableProperty(int NonMutableProperty);
public record RecordOnlyWithValueMutableProperty
{
public int MutableProperty { get; set; } = 1; // this property gets boxed
}
public record MixedRecord(List<string> List, int NonMutableProperty)
{
public List<string> MutableList { get; set; } = new();
public int MutableProperty { get; set; } = 1; // this property gets boxed
}
La penalización de rendimiento es obvia aquí. A mayor cantidad de datos para copiar en una instancia de registro que tenga, mayor penalización de rendimiento obtendrá. En general, debe crear clases pequeñas y livianas y esta regla también se aplica a los registros.
Si su aplicación usa una base de datos o un sistema de archivos, no me preocuparía mucho por esta penalización. Las operaciones de la base de datos/sistema de archivos son generalmente más lentas.