(Actualizado a 10 de Agosto de 2024)
Por ejemplo novedades en tuplas, interfaces, structs…
Tuplas
Se pueden crear tuplas directamente, una tupla almacena una colección de valores, no confundir con el tipo Tuple (que es una clase y por lo tanto tipo referencia) este tipo realmente es ValueTuple y es una estructura (paso por valor).
Se pueden establecer nombres a los elementos o declararlos de forma anónima.
string a = "A";
string b = "B";
var tuple = (a, b); // Tupla si nombre
System.Console.WriteLine(tuple.a); // Se usa el nombre de la variable
System.Console.WriteLine(tuple.b);
/* El resultado es:
B
*/
var tupleNamed = (key: a, value: b); // Tupla con nombre
System.Console.WriteLine(tupleNamed.key);
System.Console.WriteLine(tupleNamed.value);
/* El resultado es
A
B
*/
Las tuplas tienen características especiales, por ejemplo es posible compararlas, la comparación es por estructura y valor, es decir mismos elementos y mismo valor.
Los elementos pueden estar en otra ubicación si tienen nombre asignado y se pueden relacionar sino será una comparación posicional.
var tuple1 = (key: "1", value: "1.1");
var tuple2 = (key: "1", value: "1.1");
Console.WriteLine(tuple1 == tuple2 ? "Equals" : "Not Equals");
/* El resultado es
Equals
*/
Structs
Un struct puede ser de tipo lectura, en ese caso no se puede modificar ningún valor del mismo y por lo tanto debería ser inicializado por completo dentro del propio struct, por ejemplo en el constructor.
public readonly struct Person
{
public string Name { get; }
public string Surname { get; }
public Person(string name, string surName)
{
this.Name = name;
this.Surname = surName;
}
}
// Uso
DataStructures.Person structure = new DataStructures.Person("John", "Doe");
Console.WriteLine(structure.Name);
Console.WriteLine(structure.Surname);
/* El resultado es
John
Doe
*/
También es posible marcar más elementos como readonly esto obliga a que si un método es marcado como readonly no se pueden asignar valores dentro del mismo.
public struct Animal
{
public string Genre { get; set; }
public string Specie { get; set; }
public Animal(string genre, string specie)
{
this.Genre = genre;
this.Specie = specie;
}
public readonly string FullString()
{
// Specie = "EE"; Esta línea produciría un error.
return $"{Genre} -> {Specie}";
}
}
También hay que tener en cuenta que usar elementos no marcados como readonly dentro de métodos marcados como readonly generan una copia del mismo lo cual puede producir una penalización en el rendimiento.
[C#10] Se pueden crear structs sin tener que crear constructores con todos los parámetros. Esta inicialización se puede combinar, inicializando algunos valores en el constructor y otros directamente en las propiedades. Se pueden incluso crear arrays que se inicializarán a los valores por defecto de la estructura.
public readonly struct PersonStruct
{
public string Name { get; }
public string Surname { get; } = "Goodbye";
public PersonStruct()
{
Name = "Hello";
}
public PersonStruct(string name, string surName)
{
this.Name = name;
this.Surname = surName;
}
}
var ps = new PersonStruct[2];
Console.WriteLine(string.Join(", ", ps)); // Shows (), ()
Otra forma de inicializar es no crear ningún constructor, declarar la variable e inicializar los valores antes de usarla.
public struct Address
{
public string Street;
public int Number;
public double Distance;
public override string ToString() => $"{Street} ({Number}) in {Distance}";
}
Address address;
address.Street = "Street";
address.Number = 66;
address.Distance = 100.9;
Console.WriteLine(address.ToString()); // Shows Street (66) in 100,9
También se puede usar la instrucción with para crear una copia como en record. [C# 10] el operador de la izquierda puede ser un tipo anónimo.
Interfaces
Es posible crear interfaces con implementaciones por defecto, por ejemplo en caso de querer ampliar una interfaz existente se puede añadir un nuevo metódo con una implementación por defecto por lo que las clases que implementen esta interfaz no se verán afectadas.
Por ejemplo, teniendo una interfaz como:
public interface IActions
{
public double Walk();
public double Run();
}
De hecho ahora es posible tener cualquier modificador de acceso así como variables y funciones estáticas, por ejemplo para parametrizar el uso del código cliente cuando use la interfaz o incluso que los métodos puedan ser sobreescritas marcándolos como protegidos como se puede ver en la función DefaultFlight(…)
public interface IActions
{
private static int targetDistance = 0;
public static void SetTravel(int travel)
{
if (travel > 10)
targetDistance = 20;
else
targetDistance = 30;
}
public double Walk();
public double Run();
/// <summary>
/// Implementación por defecto para clases antiguas.
/// </summary>
public double Flight(int miles) => DefaultFlight(this);
protected static double DefaultFlight(IActions actions)
{
return 10 * targetDistance;
}
}
En este caso cremos una clase que implemente la primera versión de la interfaz pero no la segunda:
public class Lion : IActions
{
public double Run()
{
return 2;
}
public double Walk()
{
return 1;
}
}
En este caso para poder acceder a este código por defecto tendríamos que usar directamente la referencia a la interfaz que es la que tiene el código:
IActions ilion = lion as IActions;
Console.WriteLine(ilion.Flight(0));
Miembros virtuales en interfaces (C# 11)
Ahora se pueden definir operadores sobrecargados u otros miembros estáticos, y estas interfaces a su vez se pueden usar como restricciones para crear tipos genéricos que usan operadores o métodos estáticos. Por ejemplo podríamos hacer esto:
public interface IGetNext<T> where T : IGetNext<T>
{
static abstract T operator ++(T other);
}
Y podríamos usarlo en un struct así:
public struct RepeatSequence : IGetNext<RepeatSequence>
{
private const char Ch = 'A';
public string Text = new string(Ch, 1);
public RepeatSequence() {}
public static RepeatSequence operator ++(RepeatSequence other)
=> other with { Text = other.Text + Ch };
public override string ToString() => Text;
}
Esto se puede usar para crear algoritmo matemáticos genéricos, aquí un ejemplo completo:
public record Translation<T>(T XOffset, T YOffset) : IAdditiveIdentity<Translation<T>, Translation<T>>
where T : IAdditionOperators<T, T, T>, IAdditiveIdentity<T, T>
{
public static Translation<T> AdditiveIdentity =>
new Translation<T>(XOffset: T.AdditiveIdentity, YOffset: T.AdditiveIdentity);
}
public record Point<T>(T X, T Y) : IAdditionOperators<Point<T>, Translation<T>, Point<T>>
where T : IAdditionOperators<T, T, T>, IAdditiveIdentity<T, T>
{
public static Point<T> operator +(Point<T> left, Translation<T> right) =>
left with { X = left.X + right.XOffset, Y = left.Y + right.YOffset };
}
En este ejemplo se puede ver como se usan las interfaces IAdditionOperators<,> que devuelve el elemento identidad y IAdditiveIdentity<,> que contiene los operadores a sobrecargar. Estos cambios han traído de la mano:
- Crear el operador >>> que permite que permite evitar el casteo de numeros con signo y sin signo.
- Operador shift relajado, ahora no es necesario que en el operador shift el operando sea int.
- Operadores checked y unchecked, ahora se pueden utilizar con operadores definidos por el usuario.
Registros (C#9)
Con C#9 tenemos un nuevo tipo de estructura, los registros, estos intentan reforzar la idea de tipos inmutables pero siendo tipos por referencia, en concreto los registros son tipos por referencia inmutables (aunque se pueden hacer mutables) pero teniendo en cuenta que al intentar comparar dos registros con los mismos valores indicará que son iguales.
En el siguiente ejemplo teniendo dos estructuras con los mismos valores, el resultado será «Equals» incluso si se crea un objecto por copia.
PersonStruct personStruct1 = new PersonStruct("John", "Doe");
PersonStruct personStruct2 = new PersonStruct("John", "Doe");
if (personStruct1.Equals(personStruct2)) // GetHashCode()->True
Console.WriteLine("Equals");
else
Console.WriteLine("Not equals");
PersonRecord personRecordCopy = new PersonRecord(personRecord1);
if (personRecordCopy == personRecord1)
// By copy
Console.WriteLine("Equals");
else
Console.WriteLine("Not equals");
Aunque es necesario inicializar todas las propiedades sin embargo no será posible modificar propiedades, por ejemplo esto dará error.
personStruct1.Name = "Other name"; // CS0200
De hecho los registros aceptan herencia (y sealed), aunque un hijo heredado tenga los mismo valores será diferente.
Incluso se puede crear un registro sin declarar explícitamente las propiedades (registros posicionales), por ejemplo:
public record Animal(string colour);
Esto crea un registro que contiene una propiedad llamada colour y que se inicializa creando un objeto que tendrá un constructor con un parámetro.
También se puede heredar de este registro aunque para eso habrá que sobreescribir algunos métodos y propiedades para indicar si ese registro es igual o no a otros tipos registros cuando se hagan comparaciones.
Aquí se puede destacar la propiedad EqualityContract, esta propiedad devuelve el tipo de la clase hija de tal forma que si el tipo de la hija es el mismo que el del padre y las propiedades son iguales los dos objetos se considerarán iguales.
public record Base(string Foo);
public record Child(string Foo, string Bar) : Base(Foo)
{
protected override Type EqualityContract => typeof(Base);
}
var b = new Base("Foo");
var c = new Child("Foo", "Bar");
Console.WriteLine(b == c); // True
Los registros soportan características adicionales como uso de Deconstruct() y expresiones with, como detalle con un with se puede crear un objeto exactamente igual. [C# 10] el operador de la izquierda puede ser un tipo anónimo. A la hora de usar with se puede personalizar la copia para que sea por valor en lugar de referencia, en ese caso solo hay que pasar al constructor un parámetro del mismo tipo que el registro y asignar los valores.
PersonRecord clone = person with { }; // Creates a copy
dynamic personRecordCopy = person with { };
Los registros incluyen varios métodos sintetizados.
- Métodos para comparaciones basados en valor
- Equals, ==, != y el nuevo EqualityContract
- Sobreescritura de GetHashCode()
- Constructor por copia y Clone()
- El constructor por copia ya comentado.
- Clone() realmente no tiene este nombre, es generado internamente pero tampoco se puede generar un método Clone() con ese nombre.
- PrintMembers() y ToString()
- PrintMembers() saca un listado de todas las propiedades del registro
- ToString() similar a PrintMembers() con un poco de información adicional
[C#10] Los registros pueden ser por tipo referencia (record class) o por valor (record struct) así mismo también se puede indicar que son de solo lectura (readonly record). Aquí que tener en cuenta que por referencia o valor no tienen el mismo comportamiento.
// We define a readonly record struct with 3 properties in a single line
public readonly record struct School(string address, int number, double distance);
[C#10] También se puede hacer sealed sobre el método ToString() en clases derivadas para asegurar que todas las clases usarán la misma implementación.
public record class TeacherRecord : PersonRecord
{
public sealed override string ToString() => Subject;
}
[C#12] Ahora se puede usar ref readonly principalmente para referencias de solo lectura que se crearon antes de que existiese in
