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 FirstNameLastNamees un error.

Lista de cambios

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.

presentación y adelantos

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)objectObject.Equals(object, object)

Las estructuras anulan esto para tener una «igualdad basada en valores», comparando cada campo de la estructura invocando Equalsrecursivamente. 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 Equalstambié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 withexpresiones. 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 Equalsy (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 Studenta 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 personrealmente contiene un Student. Sin embargo, la nueva persona no sería una copia adecuada si no fuera realmente un Studentobjeto, 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 withsimplemente 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 withsoporte de expresión, la igualdad basada en el valor también debe ser «virtual», en el sentido de que Studentes necesario comparar todos los Studentcampos, 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 Equalsmé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í? person1podría pensar que sí, ya que person2tiene todo Personlo correcto, ¡pero person2serí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 usings 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 Mainhoy solo puede tener un método.

Si desea devolver un código de estado, puede hacerlo. Si quieres awaitcosas puedes hacer eso. Y si desea acceder a los argumentos de la línea de comandos, argsestá 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 DeliveryTruckparte 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 andornot, 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 andpara combinar dos patrones relacionales y formar un patrón que representa un intervalo.

Un uso común del notpatrón lo aplicará al nullpatró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 notserá 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 nullexpresiones 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 ???: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

Shares