(Actualizado a 10 de Agosto de 2024)
Aquí incluyo expresiones que son relativamente nuevas o no muy conocidas, no pretende ser un listado formal ni tampoco preciso en el término expresiones.
Propiedades
Se pueden usar expresiones para asociar código a varios elementos incluyendo propiedades (auto properties), en este ejemplo se asocia código a la propiedad Name usando una variable de clase (o variable miembro), este código devolverá el valor de la variable o lo establecerá con el valor proporcionado. También se puede usar para funciones y otros elementos de código.
Otro asunto diferente son los inicializadores de propiedad donde la propiedad simplemente coge un valor inicial por defecto como se ve en la propiedad Surname.
public class Sample
{
private string name = "First";
public string Name
{
get => name;
set => name = value;
}
public string Surname { get; set; } = "Surname";
}
Modificador required
Este es un nuevo modificador que obliga a los consumidores de una clase a utilizar las propiedades marcadas como required esto evita tener que crear constructores personalizados para cada combinación y también evita tener que crear un excesivo código en las clases heredadas llamando a los constructores.
public class Person
{
// The default constructor requires that FirstName and LastName be set at construction time
public required string FirstName { get; init; }
public string MiddleName { get; init; } = "";
public required string LastName { get; init; }
}
Condición ternaria por referencia
Es conocida la expresión: a = exp ? b : c; donde se asigna a la variable a el resultado de b o c dependiendo de la expresión exp, si es cierta se asignará b y si es falsa se asignará c. Por ejemplo:
var a = 12;
var b = 3;
var c = 6;
int res = (a > 10) ? b : c;
Tendría este equivalente usando la sentencia if:
int a = 12;
int res = 0;
if(a > 10)
{
res = b;
}
else
{
res = c;
}
También es posible hacerlo usando referencias en lugar de paso por valor, por ejemplo:
var a = 12;
var b = 3;
var c = 6;
ref int res = ref (a > 10) ? ref b : ref c;
res = -1; // Ahora c vale -1
En este caso se devuelve c, pero no el valor de c, es decir, se devuelve la referencia por lo tanto la última línea de código está modificando c no la variable res.
Null coalescing
Más conocida es esta expresión:
object b = a ?? new string("Null!");
En este caso si la variable a es null la variable b cogerá el valor de la expresión, en este caso new string(«Null!») esto es equivalente a:
if (a == null)
{
b = new string("Null!");
}
También muy útil para verificar parámetros nulos en métodos y lanzar excepciones:
object a = null;
object b = a ?? throw new ArgumentNullException("Object null.");
También es posible asignar directamente el valor, por ejemplo esta expresion:
if (variable is null)
{
variable = expression;
}
Es equivalente a:
variable ??= expression;
Parámetro out
Muchas veces sobre todo para hacer casting entre tipos se usa la variable out lo que obliga a declarla previamente, ya no es necesario hacer:
int i = -1;
if (int.TryParse(numericValue, out i))
return i;
return -1;
Se puede hacer lo siguiente:
if (int.TryParse(numericValue, out int i))
return i;
return -1;
Interpolación
Es conocida la posibilidad de usar de usar el operador $ para sustituir variables en cadenas de caracteres al estilo String.Format, por ejemplo:
var value = 100.435678M;
WriteLine($"You called with => {value:N3}"); // Saca 100.436
WriteLine($"\n\n{"*",-3} Hola {"*",3}"); // Imprime '* Hola *'
A partir de la versión 8, este operador se puede combinar con $ en cualquier orden, por lo que $@ y @$ son intercambiables.
[C# 10] Es posible también crear interpoladores personalizados, los valores de la interpolación llaman a String.Format() ahora este comportamiento se puede personalizar.
[C#10] También se puede hacer con constantes, solo con cadenas que también sean constantes y no con números.
[C#11] Ahora se pueden agregar líneas en la interpolación, por ejemplo:
string message = $"The usage policy for {safetyScore} is {
safetyScore switch
{
> 90 => "Unlimited usage",
> 80 => "General usage, with daily safety check",
> 70 => "Issues must be addressed within 1 week",
> 50 => "Issues must be addressed within 1 day",
_ => "Issues must be addressed before continued use",
}
}";
[C# 11] Incluir literales en cadenas, como:
int X = 2;
int Y = 3;
var pointMessage = $"""The point "{X}, {Y}" is {Math.Sqrt(X * X + Y * Y):F3} from the origin""";
Console.WriteLine(pointMessage);
Raw string literals
Hay más control sobre los literales de cadena, se crea una nueva forma usando «»» (triple comilla) que colocará el texto exactamente como se indica, la triple comilla tiene que ir sola al principio y al final, el resto del texto puede ir indentado, las comillas finales marcan la primera columna, no puede haber texto colocado antes.
string longMessage = """
This is a long message.
It has several lines.
Some are indented
more than others.
Some should start at the first column.
Some have "quoted text" in them.
""";
Indicar utf8
Ahora se puede indicar que una cadena tiene que ser utf8:
ReadOnlySpan<byte> AuthWithTrailingSpace = new byte[] { 0x41, 0x55, 0x54, 0x48, 0x20 };
ReadOnlySpan<byte> AuthStringLiteral = "AUTH "u8; AuthStringLiteral = "AUTH "u8;
Hay que usar ReadOnlySpan<T>
Null propagation
Muchas veces se escribe código para comprobar si hay valores nulos, por ejemplo:
object d = null;
var a = new { A = new { B = new { C = d } } };
if(a != null)
if(a.A != null)
if(a.A.B != null)
if(a.A.B.C != null)
Console.WriteLine($"{nameof(a.A.B.C)} = {C.ToString()}");
Esto se puede simplicar usando el operador ? el código anterior quedaría así (se incluye el opeador nameof() como ejemplo de uso, este operador coge el nombre de la variable para evitar tener que escribirlo). El código equivalente sería:
var a = new { A = new { B = new { C = d } } };
Console.WriteLine($"{nameof(C)} = {a?.A?.B?.C?.ToString()}");
Discard
A veces se crean variables temporales que no se van a usar o se pasan parámetros que no se necesitan en el contexto de una llamada, para este tipo de escenarios existe la opción de descartar esos valores y no usarlos.
bool b = int.TryParse(integer, out int _);
if (b)
{
Console.WriteLine("Can be casted.");
}
else
{
Console.WriteLine("It can not be casted.");
}
También en la devolución de una función (lo que incluye también por ejemplo expresiones lambdas) pero teniendo en cuenta que no puede haber más de un discard en el mismo ámbito (sería como crear dos variables con el mismo nombre).
public double ReturnDiscard(double value)
{
var _ = Math.Sqrt(value++);
return Math.Ceiling(_);
}
Conversion de valores
Se puede comparar el tipo y castear un valor dentro de una estructura if directamente, se usará el primero casting que funcione.
public string ConvertUsingIf(object element)
{
if (element is string s)
return s.ToUpper();// Casting included in the same line
else if (element is int i)
return Math.Abs(i).ToString();
else if (element is double d)
return Math.Truncate(d).ToString();
else
return string.Empty;
}
Console.WriteLine(structureControl.ConvertUsingIf("Element"));
Console.WriteLine(structureControl.ConvertUsingIf(5));
Console.WriteLine(structureControl.ConvertUsingIf(4.98));
/* El resultado es:
ELEMENT
5
4
*/
Con la sentencia switch se puede conseguir el mismo resultado
public string ConvertToStringUsingSwitch(object element)
{
switch (element)
{
case string s:
return s.ToUpper();
case int i:
return Math.Abs(i).ToString();
case double d:
return Math.Truncate(d).ToString();
default:
return string.Empty;
}
}
Console.WriteLine(structureControl.ConvertToStringUsingSwitch("Element"));
Console.WriteLine(structureControl.ConvertToStringUsingSwitch(5));
Console.WriteLine(structureControl.ConvertToStringUsingSwitch(4.98));
/* El resultado es:
ELEMENT
5
4
*/
De hecho con switch incluso podemos añadir algunas condiciones dentro de la conversion.
public string ConvertToStringUsingSwitchWhen(object element)
{
switch (element)
{
case string s when s.Length > 0:
return s.ToUpper();
case int i when i > 5: // Entero y mayor de 5
return Math.Abs(i).ToString();
case int i: // Cualquier entero
return Math.Sqrt(i).ToString();
case double d:
return Math.Truncate(d).ToString();
case var o when (o?.ToString().Length ?? 0) == 0:// Usando 'var'
return "Empty";
case null: // Comprobando contra 'null'
throw new ArgumentNullException(paramName: nameof(element), message: "Element must not be null");
default:
return string.Empty;
}
}
Patrones recursivos
Se añade más potencia a los patrones, ahora los patrones de expresiones son recursivos por lo que se pueden aplicar expresiones de patrones a expresiones de patrones.
Para los siguientes ejemplos se usará esta clase:
public class Building : IDisposable
{
public int Age { get; set; }
public string State { get; set; }
public void Deconstruct(out int age, out string state)
{
age = Age;
state = State.ToLower();
}
public void Dispose()
{
Age = 0;
}
}
Expresiones Switch
Ahora hay una forma más compacta de expresar un switch.
public Color ExpressionPatterns() =>
Console.BackgroundColor switch
{
ConsoleColor.Red => Color.FromArgb(255, 0, 0),
ConsoleColor.Green => Color.FromArgb(0, 255, 0),
ConsoleColor.Blue => Color.FromArgb(0, 0, 255),
_ => throw new ArgumentException("Invalid value")
};
[C# 10] Es posible ahora hacer referencia a propiedades anidadas.
public string ExtendedPropertyPattern(Building building)
=> building switch
{
{ Name.FirstName: "John" } => "ok",
{ Name.FirstName: "Doe" } => "ko",
_ => "Unknown"
};
Patrones de propiedad
Es posible usar switch a través de las propiedades de un objeto, por ejemplo con una clase Person que tenga una propiedad Age. Esto devolvería un resultado en función del valor de la propiedad Age.
public int PropertyExpression(Building p) =>
p switch
{
{ Age: 10 } => p.Age * 1,
{ Age: 20 } => p.Age * 2,
_ => 30,
};
Patrones posicionales
Se puede combinar la funcionalidad de deconstrucción con patrones posicionales dentro de sentencias switch, es decir, se crea el método Deconstruct y se usa dentro de una sentencia switch parecido al manejo de tuplas:
public string PositionalPattern(Building building)
=> building switch
{
(0, "") => "No born",
(15, "A") => "Young female",
(25, "B") => "Too much older man",
(_, _) => "No human",
_ => "Unknown"
};
Patrones de Tupla
Es posible usar tuplas dentro de switchs:
public static string SwitchTuple(string first, string second)
=> (first, second) switch
{
("one", "two") => "Wins second",
("two", "one") => "Wins first",
(_, _) => "tie"
};
Patrones avanzados (C# 9)
Poco a poco se ha ido mejorando el uso de patrones y ahora se permite simplificaciones adicionales, por ejemplo:
public bool PatternMatchFunction(string a) =>
a[0] is (>= 'a' and <= 'z') or (>= 'A' and <= 'Z' and not default(char));
El ejemplo anterior usamos paréntesis para remarcar la precedencia y además se permite también comprobación contra null (x is not null).
Patrones en listas
Ahora se puede hacer encajar un array o lista contra una secuencia de patrones.
int[] numbers = { 1, 2, 3 };
Console.WriteLine(numbers is [1, 2, 3]); // True
Console.WriteLine(numbers is [1, 2, 4]); // False
Console.WriteLine(numbers is [0 or 1, <= 2, >= 3]); // True
O también se puede hacer
List<int> numbers = new() { 1, 2, 3 };
if (numbers is [var first, _, _])
{
Console.WriteLine($"The first element of a three-item list is {first}.");
}
// Output:
// The first element of a three-item list is 1.
Excepciones
Es posible establecer condiciones dentro de las claúsulas catch para poder filtrar por condiciones concretas:
public void RaiseException(string code)
{
try
{
throw new ArgumentException(code);
}
catch (ArgumentException ae) when (ae.Message.Contains("100"))
{// Mismo tipo (ArgumentException) y valor ("100")
System.Console.WriteLine($"Error => {ae.Message}");
}
catch (ArgumentException ae) when (ae.Message.Contains("200"))
{
System.Console.WriteLine($"Error => {ae.Message}");
}
catch (ArgumentException ae)
{// Cualquier otro ArgumentException no incluido antes
System.Console.WriteLine($"Error => {ae.Message}");
}
}
Using
Ahora la sentencia using no está limitada por las llaves, durará lo mismo que el ámbito en el que esté contenida la variable creada por using.
El siguiente código devolverá un 0 en su propiedad Age debido al método Dispose de la clase Building.
public Building ShortUsing()
{
using Building b = new Building()
{
Age = 20,
State = "WA"
};
b.Age = 40;
return b;
}
También se pueden crear using globales, no es más que una forma de declarar un using en un fichero que se aplicará de forma global en todo el proyecto. Una forma útil es crear un fichero solo para los using.
global using System;
[C# 12] Ahora se puede usar using para cualquier tipo no solo tipos con nombre, por ejemplo tuplas, arrays, punteros… por ejemplo:
// The alias is to `List<...>` which is itself not a nullable
// reference type itself, even though it contains one as a type argument.
using Y = System.Collections.Generic.List<string?>;
using Z = int?;
Namespace [C#10]
Ahora es posible eliminar las llaves asociadas a los namespace, así se gana un poco de tabulación y espacio hacia la izquierda. Tip: Solo con poner ; al final del namespace VS limpiará los corchetes.
namespace UpToDate.Demos;
internal class DemoExpression
{...}
Secuencias asincrónicas
Es posible usar await para un bucle que consume una función asincrónica, por ejemplo teniendo este método.
public static async IAsyncEnumerable<string> GenerateWord(string a)
{
for (int i = 65; i < 97; i++)
{
await Task.Delay(100);
a += (char)i;
yield return a;
}
}
Se podría consumir directamente de esta forma:
await foreach (var word in Expressions.GenerateWord("z"))
{
Console.WriteLine(word);
}
Índices y rangos
Un índice define un elemento dentro de una secuencia, por ejemplo ^0 se refiere al primer elemento, mientras que un rango define una serie de elementos dentro de la secuencia, por ejemplo 0..8 indica los elementos del 0 al 7 ya que el primer elemento siempre se incluye y el último se omite.
var numbers = new string[]
{
"One",
"Two",
"Three",
"Four",
"Five"
};
Esto funciona con los siguientes elementos
- string
- Matrices
- Span<T>
- ReadOnlySpan<T>
var last = numbers[^1]; // The last one ("Five)
var rangeFromBegin = numbers[0..3]; // The first three ("One", "Two", "Three")
var rangeFromEnd = numbers[^2..^1]; // The penultimate element ("Four")
var all = numbers[..]; // All ("One","Two","Three","Four","Five")
var fromBegin = numbers[..2]; // First two ("One","Two")
var middle = numbers[3..]; // From third till end ("Four", "Five")
Range range = 1..4;
var range = numbers[range]; // From second till third ("Two", "Three", "Four")
Index index = ^2;
var index = numbers[index]; // The penultimate element ("Four")
Expresiones en colecciones
Una forma sencilla de agrupar colecciones, sirve para tipos array, Span<> y ReadOnlySpan<> y tipos que se pueden inicializar como colecciones (por ejemplo List<T>), en este ejemplo se concatenan 3 arrays.
int[] row0 = [1, 2, 3];
int[] row1 = [4, 5, 6];
int[] row2 = [7, 8, 9];
int[] single = [.. row0, .. row1, .. row2];
foreach (var element in single)
{
Console.Write($"{element}, ");
}
Init
Una forma de establecer valores a las propiedades durante la creación del objeto, es útil para inicializar en lugar de usar un constructor o establecer valores en propiedades desde clases derivadas.
Por ejemplo el siguiente código permite inicializar un objeto a través de sus propiedades y después cambiar algunas de ellas menos la marcada como init que dará un error.
public class Building
{
...
public string Name { get; init; } = "None";
...
}
Building b = new () { Age = 5, Name = "Chrysler", State = "USA" };
b.Age = 40;
b.Name = "11";
// CS8852
Scoped
Este es un modificador para tipos por valor (en concreto ref struct) que asegura que el código no extenderá el tiempo de vida de la variable. Solo se puede aplicar a variables locales y parámetros.
Span<char> values = stackalloc char[3] { 'T', 'o', 'm' };
new Test().TestMethod(values);
ref struct Test
{
public void TestMethod(scoped ReadOnlySpan<char> characters)
{
// The body of the method must only use characters in the local scope, and cannot assign it directly to any field or classes unles they themselves would be scoped.
}
}
Constructores (C# 9)
Se pueden realizar algunas simplificaciones en los constructores si el tipo está bien establecido, por ejemplo está función:
public Building IsNull(Building b)
{
if(b is null)
return new();
else
return b;
}
Se podría llamar usando:
if (expressions.IsNull(new()) is null)
Console.WriteLine("Is null");
else
Console.WriteLine("Is not null");
En todos estos casos donde se usa new() no hace falta indicar el tipo porque está bien definido ya sea a través de los parámetros, el valor de retorno o la propia construcción del objeto.
Constructores primarios
Los constructores primarios permiten declaraciones más compactas eliminando propiedades y añadiéndolas directamente en el constructor, ahora se admiten para estructuras, registros y clases. Por ejemplo lo que antes era:
public readonly struct Distance
{
public readonly double Magnitude { get; }
public readonly double Direction { get; }
public Distance(double dx, double dy)
{
Magnitude = Math.Sqrt(dx * dx + dy * dy);
Direction = Math.Atan2(dy, dx);
}
}
Ahora se puede hacer asi:
public readonly struct Distance(double dx, double dy)
{
public readonly double Magnitude { get; } = Math.Sqrt(dx * dx + dy * dy);
public readonly double Direction { get; } = Math.Atan2(dy, dx);
}
Y podemos asimismo heredar estos constructores o crear varios constructores primarios.
public class EmpireState(decimal height, decimal weight) : Building("WA")
{
public EmpireState(decimal height, decimal weight, string address) : this(height, weight) { }
}
Lambda
Se incorpora el concepto de tipo natural de tal forma que ya no hace falta indicar expresamente los tipos de parámetros o devolución, por ejemplo:
var parse = (string s) => int.Parse(s); // Equivalent to Func<string, int>
Aunque en algunos casos si se quiere indicar se puede indicar el parámetro de devolución:
var choose = object (bool b) => b ? 1 : "two"; // Func<bool, object>
Ahora también se pueden añadir atributos a la expresión lambda, a parámetros de entrada y al valor de retorno, por ejemplo:
var lambdaAttribute = [Custom("lambda attribute")][return: Custom("return attribute")]int ([Custom("parameter attribute")]string s) => int.Parse(s);
[C# 12] Ahora también se admite parámetros por defecto, por ejemplo:
var IncrementBy = (int source, int increment = 1) => source + increment;
Atributos Genéricos (C# 11)
Ahora se pueden crear atributos genéricos, por ejemplo:
public class GenericCustomAttribute<T> : Attribute { }
Que luego podemos usar como atributo a un método por ejemplo:
[GenericCustom<string>()]
public string ShowCustomAttribute()
{
return "Custom operation string";
}
A la hora de usar al el atributo tiene que estar construido, (no puede volver a ser genérico) y hay algunas restricciones como que no se puede usar dynamic, string?, (int x, int y)… pero se pueden usar los sustitutos naturales (object, string, ValueTuple<int, int>).
Atributo Experimental
Se puede marcar código como experimental para que saque una advertencia, realmente el compilador mostrará un error que se puede evitar usando la directiva #pragma
[Experimental("DiagID", UrlFormat = "https://example.org/{0}")]
public class GenericCustomAttribute<T> : Attribute { }
#pragma warning disable DiagID
[GenericCustom<string>()]
public string ShowCustomAttribute()
{
return "Custom operation string";
}
