C# es un lenguaje en constante evolución. Empiezan a escucharse voces que demandan un periodo de estabilidad ante tanto cambio, pero está claro que la última palabra la tiene Microsoft.
Con la versión 11 de .NET a punto de caramelo, podemos relacionar los últimos cambios más reseñables a C#. Los mejores cambios son los que no se ven: los ingenieros de Microsoft están optimizando agresivamente el código generado por el compilador, de manera que sea tan rápido como sea posible.
Para la mayor parte del código aquí, necesitamos instalar la .NET 11.0-rc1, configurar el proyecto para usarla, y activar preview. Un ejemplo de proyecto aparece a continuación.
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net11.0</TargetFramework>
<RootNamespace>Matrix</RootNamespace>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<Preview>enable</Preview>
</PropertyGroup>
</Project>
Vale, pero... ¿cuál es la versión de C# que se corresponde con estas versiones de .NET? Es una buena pregunta, porque no hay una respuesta sencilla. Es imposible, de hecho, obtenerla, mediante la herramienta dotnet.
| .NET | .NET 9 | .NET 10 | .NET 11 |
|---|---|---|---|
| C# | C# 13 | C# 14 | C# 15 |
Así que, como decía Jacobo, vamos por partes.
Novedades en C
C# 13: índices negativos
Ahora es posible acceder a elementos desde un índice negativo. Este índice negativo se cuenta desde el último elemento, de manera que -1 se corresponde con el último, -2 el antepenúltimo, etc. Solo es necesario tener en cuenta que, en lugar de utilizar, por ejemplo, -1, la sintaxis exige utilizar ^1.
var buffer = new int[10];
buffer[^1] = 11;
buffer[^2] = 12;
buffer[^3] = 13;
buffer[^4] = 14;
buffer[^5] = 15;
buffer[^6] = 16;
buffer[^7] = 17;
buffer[^8] = 18;
buffer[^9] = 19;
buffer[^10] = 20;
foreach (var x in buffer) {
Console.WriteLine(x);
}
La salida es la que esperamos:
20
19
18
17
16
15
14
13
12
11
C# 14: La palabra clave field
Ya escribí sobre fielden C# recientemente. De la misma forma en la que disponemos de value, para representar el valor asignado en la propiedad set, tenemos ahora field para representar al atributo sintético.
public class Punto {
public int X { get; set {
if ( value < 0 ) {
throw new ArgumentException( "value < 0" );
}
?X? = value;
}}
// ...
}
En el código de arriba, comprobamos si el valor que va a ser asignado en el set (llamado value), de la propiedad X es o no negativo. En caso de ser negativo, se lanza una excepción. El código es erróneo porque se nombra ?x?para representar el campo a asignar. En este punto, tendríamos que crear un atributo real a la clase.
public class Punto {
public int X { get; set {
if ( value < 0 ) {
throw new ArgumentException( "value < 0" );
}
this.x = value;
}}
// ...
private int x;
}
¡Pero ahora ya no es necesario! Volviendo al código de más arriba, podemos escribir lo siguiente.
public class Punto {
public int X { get; set {
if ( value < 0 ) {
throw new ArgumentException( "value < 0" );
}
field = value;
}}
// ...
}
C# 14: Asignación condicional
Hasta ahora, era posible utilizar el operador ? como parte de una expresión. En caso de que la referencia a la izquierda de ? sea null, entonces se devuelve directamente null. En otro caso, se devuelve el resultado de la expresión como tal.
Por ejemplo, person?.Name devuelve el nombre del objeto de la clase Person referenciada por person. Si en cambio, person es null, entonces se devuelve null.
Pero es que ahora, además, es posible utilizarla para asignar (o no), un valor, si la referencia a la izquierda de ? no es null.
var people = new List<Person>();
// ...
var person = people.FirstOrDefault( p => p.age < 18 );
Console.WriteLine( person?.Name ); // Already supported
person?.Name = "The One"; // New!
C# 15: Jerarquías cerradas
Es posible crear una jerarquía que se sabe que, en el momento de la compilación, va a tener un número determinado de clases derivadas.
public closed class Person {
public required string Name { get; init; }
}
public class Employee: Person {
public required double Salary { get; init; }
}
public class Retired: Person {
public required double Pension { get; init; }
}
En la clase Person añadimos el modificador closed para indicar que esta clase solo va a tener las clases derivadas que indiquemos; ninguna más.
Esto permite hacer un tipo de switch que se base en los tipos derivados de los objetos Person.
public closed class Person {
public required string Name { get; init; }
public static string IncomeLabel(Person p) => p switch
{
Employee => "Salary",
Retired => "Pension"
}
}
La ventaja es que en este switch el compilador puede comprobar que hemos tratado todos los posibles casos como clases derivadas tiene Persona.
A mi se me ocurre que es una versión super-vitaminada de enumerados, pero claro, con un cierto coste, pues involucra la creación de objetos.
C# 15: Uniones
Ya escribí sobre cómo C# soporta uniones en C#, aunque no nos quede demasiado claro para qué exactamente.
C# 15: break y continue etiquetados
Esta característica está soportada en Java desde el primer día, pero por algún motivo no ha conseguido llegar hasta C# hasta ahora.
La idea es la posibilidad de romper un bucle (o saltar una vuelta de bucle), desde un bucle anidado en el primero.
var m = new Matrix<int>( 10, 10 );
var rnd = new Random();
// Fill
for (int i = 0; i < m.Rows; ++i) {
for(int j = 0; j < m.Cols; ++j) {
m.Set( i, j, rnd.Next( 100 ) );
}
}
Find: for (int i = 0; i < m.Rows; ++i) {
for(int j = 0; j < m.Cols; ++j) {
if ( m.Get( i, j ) == 50 ) {
Console.WriteLine( $"Encontrado en ({i}, {j})" );
break Find;
}
}
}
public class Matrix<T> {
public Matrix(int rows, int cols)
{
this.Rows = rows;
this.Cols = cols;
this.m = new T[cols, rows];
}
public int Rows { get; set; }
public int Cols { get; set; }
public T[,] Data => (T[,]) this.m.Clone();
public T Get(int r, int c) => this.m[r, c];
public void Set(int r, int c, T x) => this.m[r, c] = x;
private T[,] m;
}
En el código anterior vemos una clase Matrix, que primero se rellena mediante un doble bucle con valores aleatorios, y después se recorre, de nuevo con un doble-bucle, para buscar el valor 50. En caso de encontrarse, no es suficiente con break, puesto que solo romperíamos el bucle anidado. Pero con la versión de break etiquetada, podemos indicar cuál de los dos bucles queremos romper.
¿Y tú, cómo llevas tanto cambio y tanto avance en C#?
Top comments (0)