C# 9.0 está tomando forma, y me gustaría compartir nuestra opinión sobre algunas de las principales características que agregaremos a esta próxima versión del lenguaje.
Con cada nueva versión de C# nos esforzamos por lograr una mayor claridad y simplicidad en escenarios de codificación comunes, y C# 9.0 no es la excepción. Un enfoque particular esta vez es apoyar la representación concisa e inmutable de formas de datos.
¡Vamos a sumergirnos!
Propiedades solo de inicio
Los inicializadores de objetos son bastante impresionantes. Le dan al cliente de un tipo un formato muy flexible y legible para crear un objeto, y son especialmente buenos para la creación de objetos anidados donde se crea todo un árbol de objetos de una sola vez. Aquí hay uno simple:
new Person
{
FirstName = "Scott",
LastName = "Hunter"
}
Los inicializadores de objetos también liberan al autor de tipografía de escribir muchas repeticiones de construcción: ¡todo lo que tienen que hacer es escribir algunas propiedades!
public class Person
{
public string FirstName { get; set; }
public string LastName { get; set; }
}
La única gran limitación hoy en día es que las propiedades tienen que ser mutables para que funcionen los inicializadores de objetos: funcionan primero llamando al constructor del objeto (el predeterminado, sin parámetros en este caso) y luego asignándolo a los establecedores de propiedades.
¡Las propiedades solo de inicio arreglan eso! Presentan un init
descriptor de acceso que es una variante del descriptor de set
acceso que solo se puede llamar durante la inicialización del objeto:
public class Person
{
public string FirstName { get; init; }
public string LastName { get; init; }
}
Con esta declaración, el código del cliente anterior sigue siendo legal, pero cualquier asignación posterior a las propiedades FirstName
y LastName
es un error.

Acceso al Init y campos de solo lectura
Debido a que el acceso al init
solo se pueden invocar durante la inicialización, se les permite mutar los campos readonly
de la clase que los encierra, como puede hacerlo en un constructor.
public class Person
{
private readonly string firstName;
private readonly string lastName;
public string FirstName
{
get => firstName;
init => firstName = (value ?? throw new ArgumentNullException(nameof(FirstName)));
}
public string LastName
{
get => lastName;
init => lastName = (value ?? throw new ArgumentNullException(nameof(LastName)));
}
}
Registros
Las propiedades de solo inicio son excelentes si desea hacer que las propiedades individuales sean inmutables. Si desea que todo el objeto sea inmutable y se comporte como un valor, entonces debería considerar declararlo como un registro :
public data class Person
{
public string FirstName { get; init; }
public string LastName { get; init; }
}
La palabra clave data
en la declaración de clase lo marca como un registro. Esto lo imbuye con varios comportamientos adicionales de valor, que profundizaremos en lo siguiente. En términos generales, los registros deben considerarse más como «valores»: ¡datos! – Y menos como objetos. No están destinados a tener un estado encapsulado mutable. En cambio, representa el cambio a lo largo del tiempo creando nuevos registros que representan el nuevo estado. No se definen por su identidad, sino por sus contenidos.

Con expresiones
Cuando se trabaja con datos inmutables, un patrón común es crear nuevos valores a partir de los existentes para representar un nuevo estado. Por ejemplo, si nuestra persona cambiara su apellido, lo representaríamos como un nuevo objeto que es una copia del antiguo, excepto con un apellido diferente. Esta técnica a menudo se conoce como mutación no destructiva . En lugar de representar a la persona a lo largo del tiempo , el registro representa el estado de la persona en un momento dado .
Para ayudar con este estilo de programación, los registros permiten un nuevo tipo de expresión; la expresión with
var otherPerson = person with { LastName = "Hanselman" };
Las expresiones With utilizan la sintaxis del inicializador de objeto para indicar qué es diferente en el nuevo objeto del objeto anterior. Puede especificar múltiples propiedades.
Un registro define implícitamente un «constructor de copia» protected
: un constructor que toma un objeto de registro existente y lo copia campo por campo en el nuevo:
protected Person(Person original) { /* copy all the fields */ } // generated
La expresión with
hace que se llame al constructor de la copia y luego aplica el inicializador de objeto en la parte superior para cambiar las propiedades en consecuencia.
Si no le gusta el comportamiento predeterminado del constructor de copia generado, puede definir el suyo en su lugar, y eso será recogido por la expresión with
.
Igualdad basada en el valor
Todos los objetos heredan un método virtual de la clase. Esto se utiliza como base para el método estático cuando ambos parámetros no son nulos.Equals(object)
object
Object.Equals(object, object)
Las estructuras anulan esto para tener una «igualdad basada en valores», comparando cada campo de la estructura invocando Equals
recursivamente. Los registros hacen lo mismo.
Esto significa que de acuerdo con su «valor», dos objetos de registro pueden ser iguales entre sí sin ser el mismo objeto. Por ejemplo, si modificamos el apellido de la persona modificada nuevamente:
var originalPerson = otherPerson with { LastName = "Hunter" };
Ahora tendríamos ReferenceEquals(person, originalPerson)
= falso (no son el mismo objeto)
pero Equals(person, originalPerson)
= verdadero (tienen el mismo valor).
Si no le gusta el comportamiento predeterminado de comparación campo por campo de la anulación generada Equals
, puede escribir el suyo en su lugar. Solo debe tener cuidado de comprender cómo funciona la igualdad basada en el valor en los registros, especialmente cuando se trata de la herencia, que volveremos a continuación.
Junto con el valor basado Equals
también hay una anulación basada en el valor para acompañarlo.GetHashCode()
Miembros de datos
La mayoría de los registros están destinados a ser inmutables, con propiedades públicas de solo inicio que pueden modificarse de forma no destructiva mediante with
expresiones. Para optimizar ese caso común, los registros cambian los valores predeterminados de lo que significa una simple declaración de miembro del formulario . ¡En lugar de un campo implícitamente privado, como en otras declaraciones de clase y estructura, en los registros esto se considera abreviatura para una propiedad automática pública, solo de inicio! Por lo tanto, la declaración:string FirstName
public data class Person { string FirstName; string LastName; }
Significa exactamente lo mismo que teníamos antes:
public data class Person
{
public string FirstName { get; init; }
public string LastName { get; init; }
}
Creemos que esto genera declaraciones de registro hermosas y claras. Si realmente desea un campo privado, simplemente puede agregar el modificador private
explícitamente:
private string firstName;
Registros posicionales
A veces es útil tener un enfoque más posicional para un registro, donde su contenido se proporciona a través de argumentos de constructor, y se puede extraer con la deconstrucción posicional.
Es perfectamente posible especificar su propio constructor y deconstructor en un registro:
public data class Person
{
string FirstName;
string LastName;
public Person(string firstName, string lastName)
=> (FirstName, LastName) = (firstName, lastName);
public void Deconstruct(out string firstName, out string lastName)
=> (firstName, lastName) = (FirstName, LastName);
}
Pero hay una sintaxis mucho más corta para expresar exactamente lo mismo (carcasa de módulo de nombres de parámetros):
public data class Person(string FirstName, string LastName);
Esto declara las propiedades automáticas de solo init público y el constructor y el deconstructor, para que pueda escribir:
var person = new Person("Scott", "Hunter"); // positional construction
var (f, l) = person; // positional deconstruction
Si no le gusta la propiedad automática generada, puede definir su propia propiedad con el mismo nombre, y el constructor y el deconstructor generados simplemente usarán esa.
Registros y mutación
La semántica basada en el valor de un registro no se adapta bien al estado mutable. Imagina poner un objeto de registro en un diccionario. Encontrarlo nuevamente depende de Equals
y (a veces) de GethashCode
. Pero si el registro cambia su estado, ¡también cambiará a lo que es igual! ¡Es posible que no podamos encontrarlo de nuevo! En una implementación de tabla hash, incluso podría corromper la estructura de datos, ya que la ubicación se basa en el código hash que tiene «al llegar».
Probablemente hay algunos usos avanzados válidos del estado mutable dentro de los registros, especialmente para el almacenamiento en caché. Pero es probable que el trabajo manual que implica anular los comportamientos predeterminados para ignorar dicho estado sea considerable.
Expresiones With y herencia
La igualdad basada en el valor y la mutación no destructiva son notoriamente desafiantes cuando se combinan con la herencia. Agreguemos una clase de registro derivada Student
a nuestro ejemplo en ejecución:
public data class Person { string FirstName; string LastName; }
public data class Student : Person { int ID; }
Y comencemos nuestro ejemplo de expresión with
creando realmente un Student
, pero almacenándolo en una variable Person
:
Person person = new Student { FirstName = "Scott", LastName = "Hunter", ID = GetNewId() };
otherPerson = person with { LastName = "Hanselman" };
En el punto de esa expresión with
en la última línea, el compilador no tiene idea de que person
realmente contiene un Student
. Sin embargo, la nueva persona no sería una copia adecuada si no fuera realmente un Student
objeto, completa con la misma ID
que la primera copiada.
C# hace que esto funcione. Los registros tienen un método virtual oculto que se encarga de «clonar» todo el objeto. Cada tipo de registro derivado anula este método para llamar al constructor de copia de ese tipo, y el constructor de copia de un registro derivado encadena al constructor de copia del registro base. Una expresión with
simplemente llama al método oculto «clonar» y aplica el inicializador de objeto al resultado.
Igualdad y herencia basadas en el valor.
De manera similar al with
soporte de expresión, la igualdad basada en el valor también debe ser «virtual», en el sentido de que Student
es necesario comparar todos los Student
campos, incluso si el tipo estáticamente conocido en el punto de comparación es un tipo base Person
. Eso se logra fácilmente anulando el Equals
método ya virtual .
Sin embargo, hay un desafío adicional con la igualdad: ¿qué sucede si se comparan dos tipos diferentes de Person
? Realmente no podemos dejar que uno de ellos decida qué igualdad aplicar: se supone que la igualdad es simétrica, por lo que el resultado debería ser el mismo independientemente de cuál de los dos objetos sea el primero. En otras palabras, ¡tienen que estar de acuerdo con la igualdad que se aplica!
Un ejemplo para ilustrar el problema:
Person person1 = new Person { FirstName = "Scott", LastName = "Hunter" };
Person person2 = new Student { FirstName = "Scott", LastName = "Hunter", ID = GetNewId() };
¿Son los dos objetos iguales entre sí? person1
podría pensar que sí, ya que person2
tiene todo Person
lo correcto, ¡pero person2
sería diferente! Debemos asegurarnos de que ambos estén de acuerdo en que son objetos diferentes.
Una vez más, C # se encarga de esto automáticamente. La forma en que se hace es que los registros tienen una propiedad virtual protegida llamada EqualityContract
. Cada registro derivado lo anula y, para poder comparar igual, los dos objetos deben tener lo mismo EqualityContract
.
Programas de alto nivel
Escribir un programa simple en C# requiere una cantidad notable de código repetitivo:
using System;
class Program
{
static void Main()
{
Console.WriteLine("Hello World!");
}
}
Esto no solo es abrumador para los principiantes de idiomas, sino que también desordena el código y agrega niveles de sangría.
En C# 9.0, puede elegir escribir su programa principal en el nivel superior:
using System;
Console.WriteLine("Hello World!");
Cualquier declaración está permitida. El programa tiene que ocurrir después del using
s y antes de cualquier tipo o declaración de espacio de nombres en el archivo, y solo puede hacer esto en un archivo, de la misma manera que Main
hoy solo puede tener un método.
Si desea devolver un código de estado, puede hacerlo. Si quieres await
cosas puedes hacer eso. Y si desea acceder a los argumentos de la línea de comandos, args
está disponible como un parámetro «mágico».
Las funciones locales son una forma de declaración y también están permitidas en el programa de nivel superior. Es un error llamarlos desde cualquier lugar fuera de la sección de declaración de nivel superior.
Coincidencia de patrones mejorada
Se han agregado varios tipos nuevos de patrones en C# 9.0. Echemos un vistazo a ellos en el contexto de este fragmento de código del tutorial de coincidencia de patrones :
public static decimal CalculateToll(object vehicle) =>
vehicle switch
{
...
DeliveryTruck t when t.GrossWeightClass > 5000 => 10.00m + 5.00m,
DeliveryTruck t when t.GrossWeightClass < 3000 => 10.00m - 2.00m,
DeliveryTruck _ => 10.00m,
_ => throw new ArgumentException("Not a known vehicle type", nameof(vehicle))
};
Patrones de tipo simple
Actualmente, un patrón de tipo necesita declarar un identificador cuando el tipo coincide, incluso si ese identificador es un descarte _
, como en el caso anterior. Pero ahora puedes escribir el tipo:DeliveryTruck _
DeliveryTruck => 10.00m,
Patrones relacionales
C # 9.0 introduce patrones correspondientes a los operadores relacionales <
, <=
y así sucesivamente. Entonces, ahora puede escribir la DeliveryTruck
parte del patrón anterior como una expresión de interruptor anidada:
DeliveryTruck t when t.GrossWeightClass switch
{
> 5000 => 10.00m + 5.00m,
< 3000 => 10.00m - 2.00m,
_ => 10.00m,
},
Aquí y son patrones relacionales.> 5000
< 3000
Patrones lógicos
Por último se puede combinar con los patrones de operadores lógicos and
, or
y not
, enunciados como las palabras para evitar confusiones con los operadores utilizados en las expresiones. Por ejemplo, los casos del interruptor anidado anterior se podrían poner en orden ascendente como este:
DeliveryTruck t when t.GrossWeightClass switch
{
< 3000 => 10.00m - 2.00m,
>= 3000 and <= 5000 => 10.00m,
> 5000 => 10.00m + 5.00m,
},
El caso medio allí se usa and
para combinar dos patrones relacionales y formar un patrón que representa un intervalo.
Un uso común del not
patrón lo aplicará al null
patrón constante, como en . Por ejemplo, podemos dividir el manejo de casos desconocidos dependiendo de si son nulos:not null
not null => throw new ArgumentException($"Not a known vehicle type: {vehicle}", nameof(vehicle)),
null => throw new ArgumentNullException(nameof(vehicle))
También not
será conveniente en condiciones if que contienen expresiones is donde, en lugar de paréntesis dobles difíciles de manejar:
if (!(e is Customer)) { ... }
Solo puedes decir
if (e is not Customer) { ... }
Mejora de la tipificación de objetivos
«Escritura de destino» es un término que usamos para cuando una expresión obtiene su tipo del contexto de dónde se está utilizando. Por ejemplo, las null
expresiones lambda siempre se escriben en destino.
En C# 9.0, algunas expresiones que no se escribieron previamente en el destino pueden guiarse por su contexto.
Expresiones new
de tipo objetivo
Las expresiones new
en C# siempre han requerido que se especifique un tipo (excepto las expresiones de matriz tipadas implícitamente). Ahora puede omitir el tipo si hay un tipo claro al que se asignan las expresiones.
Point p = new (3, 5);
Objetivo escrito ??
y ?:
A veces, las expresiones condicionales ??
y ?:
no tienen un tipo compartido obvio entre las ramas. Tales casos fallan hoy, pero C# 9.0 los permitirá si hay un tipo de destino al que ambas ramas se convierten:
Person person = student ?? customer; // Shared base type
int? result = b ? 0 : null; // nullable value type
Retornos covariantes
A veces es útil expresar que una anulación de método en una clase derivada tiene un tipo de retorno más específico que la declaración en el tipo base. C# 9.0 permite que:
abstract class Animal
{
public abstract Food GetFood();
...
}
class Tiger : Animal
{
public override Meat GetFood() => ...;
}
Mas informacion en Github
Obtenido del Blog de .Net de Microsoft