Expresiones lambda y funciones anónimas
Use una expresión lambda para crear una función anónima. Use el operador de declaración lambda =>
para separar la lista de parámetros de la lamba de su cuerpo. Una expresión lambda puede tener cualquiera de las dos formas siguientes:
Una lambda de expresión que tiene una expresión como cuerpo:
(input-parameters) => expression
Una lambda de instrucción que tiene un bloque de instrucciones como cuerpo:
(input-parameters) => { <sequence-of-statements> }
Para crear una expresión lambda, especifique los parámetros de entrada (si existen) a la izquierda del operador lambda y una expresión o bloque de instrucciones en el otro lado.
Toda expresión lambda se puede convertir en un tipo delegado. Los tipos de sus parámetros y el valor devuelto definen el tipo de delegado al que se puede convertir una expresión lambda. Si una expresión lambda no devuelve un valor, se puede convertir en uno de los tipos delegados Action
; de lo contrario, se puede convertir en uno de los tipos delegados Func
. Por ejemplo, una expresión lambda que tiene dos parámetros y no devuelve ningún valor corresponde a un delegado Action<T1,T2>. Una expresión lambda que tiene un parámetro y devuelve un valor se puede convertir en un delegado Func<T,TResult>. En el ejemplo siguiente, la expresión lambda x => x * x
, que especifica un parámetro denominado x
y devuelve el valor de x
al cuadrado, se asigna a una variable de un tipo delegado:
Func<int, int> square = x => x * x;
Console.WriteLine(square(5));
// Output:
// 25
Las expresiones lambda también se pueden convertir en los tipos de árbol de expresión, como se muestra en los ejemplos siguientes:
System.Linq.Expressions.Expression<Func<int, int>> e = x => x * x;
Console.WriteLine(e);
// Output:
// x => (x * x)
Las expresiones lambda se usan en cualquier código que requiera instancias de tipos delegados o árboles de expresión. Un ejemplo es el argumento del método Task.Run(Action) para pasar el código que se debe ejecutar en segundo plano. También puede usar expresiones lambda al escribir LINQ en C#, como se muestra en el ejemplo siguiente:
int[] numbers = { 2, 3, 4, 5 };
var squaredNumbers = numbers.Select(x => x * x);
Console.WriteLine(string.Join(" ", squaredNumbers));
// Output:
// 4 9 16 25
Cuando se usa la sintaxis de método para llamar al método Enumerable.Select en la clase System.Linq.Enumerable, por ejemplo en LINQ to Objects y en LINQ to XML, el parámetro es un tipo delegado System.Func<T,TResult>. Cuando se llama al método Queryable.Select en la clase System.Linq.Queryable, por ejemplo en LINQ to SQL, el tipo de parámetro es un tipo de árbol de expresión Expression<Func<TSource,TResult>>
. En ambos casos, se puede usar la misma expresión lambda para especificar el valor del parámetro. Esto hace que las dos llamadas Select
tengan un aspecto similar aunque, de hecho, el tipo de objetos creados a partir las lambdas es distinto.
Lambdas de expresión
Una expresión lambda con una expresión en el lado derecho del operador =>
se denomina lambda de expresión. Una expresión lambda devuelve el resultado de evaluar la condición y tiene la siguiente forma:
(input-parameters) => expression
El cuerpo de una expresión lambda puede constar de una llamada al método. Pero si crea árboles de expresión que se evalúan fuera del contexto de Common Language Runtime (CLR) de .NET, como en SQL Server, no debe usar llamadas de métodos en expresiones lambda. Los métodos no tienen ningún significado fuera del contexto de Common Language Runtime (CLR) de .NET.
Lambdas de instrucción
Una lambda de instrucción es similar a un lambda de expresión, salvo que las instrucciones se encierran entre llaves:
(input-parameters) => { <sequence-of-statements> }
El cuerpo de una lambda de instrucción puede estar compuesto de cualquier número de instrucciones; sin embargo, en la práctica, generalmente no hay más de dos o tres.
Action<string> greet = name =>
{
string greeting = $"Hello {name}!";
Console.WriteLine(greeting);
};
greet("World");
// Output:
// Hello World!
No se pueden usar expresiones lambdas para crear árboles de expresión.
Parámetros de entrada de una expresión lambda
Los parámetros de entrada de una expresión lambda se encierran entre paréntesis. Para especificar cero parámetros de entrada, utilice paréntesis vacíos:
Action line = () => Console.WriteLine();
Si una expresión lambda solo tiene un parámetro de entrada, los paréntesis son opcionales:
Func<double, double> cube = x => x * x * x;
Dos o más parámetros de entrada se separan mediante comas:
Func<int, int, bool> testForEquality = (x, y) => x == y;
A veces, el compilador no puede deducir los tipos de parámetros de entrada. Puede especificar los tipos de manera explícita, tal como se muestra en el ejemplo siguiente:
Func<int, string, bool> isTooLong = (int x, string s) => s.Length > x;
Los tipos de parámetro de entrada deben ser todos explícitos o todos implícitos; de lo contrario, se produce un error del compilador CS0748.
Puede usar descarta para especificar dos o más parámetros de entrada de una expresión lambda que no se usan en la expresión:
Func<int, int, int> constant = (_, _) => 42;
Los parámetros de descarte lambda pueden ser útiles cuando se usa una expresión lambda para proporcionar un controlador de eventos.
Nota:
Por compatibilidad con versiones anteriores, si solo un parámetro de entrada se denomina _
, dentro de una expresión lambda, _
se trata como el nombre de ese parámetro.
A partir de C# 12, puede proporcionar valores predeterminados para los parámetros en expresiones lambda. La sintaxis y las restricciones en los valores de parámetro predeterminados son las mismas que para los métodos y las funciones locales. En el ejemplo siguiente se declara una expresión lambda con un parámetro predeterminado y, a continuación, la llama una vez mediante el valor predeterminado y otra vez con dos parámetros explícitos:
var IncrementBy = (int source, int increment = 1) => source + increment;
Console.WriteLine(IncrementBy(5)); // 6
Console.WriteLine(IncrementBy(5, 2)); // 7
También puede declarar expresiones lambda con matrices params
como colecciones o parámetros:
var sum = (params IEnumerable<int> values) =>
{
int sum = 0;
foreach (var value in values)
sum += value;
return sum;
};
var empty = sum();
Console.WriteLine(empty); // 0
var sequence = new[] { 1, 2, 3, 4, 5 };
var total = sum(sequence);
Console.WriteLine(total); // 15
Como parte de estas actualizaciones, cuando un grupo de métodos que tiene un parámetro predeterminado se asigna a una expresión lambda, esa expresión lambda también tiene el mismo parámetro predeterminado. Un grupo de métodos con un parámetro de colección params
también se puede asignar a una expresión lambda.
Las expresiones lambda con parámetros predeterminados o colecciones params
como parámetros no tienen tipos naturales que correspondan a tipos Func<>
o Action<>
. Sin embargo, puede definir tipos delegados que incluyan valores de parámetro predeterminados:
delegate int IncrementByDelegate(int source, int increment = 1);
delegate int SumDelegate(params int[] values);
delegate int SumCollectionDelegate(params IEnumerable<int> values);
O bien, puede usar variables con tipo implícito con declaraciones var
para definir el tipo delegado. El compilador sintetiza el tipo de delegado correcto.
Para obtener más información sobre los parámetros predeterminados en expresiones lambda, consulte la especificación de características para los parámetros predeterminados en expresiones lambda.
Lambdas asincrónicas
Puede crear fácilmente expresiones e instrucciones lambda que incorporen el procesamiento asincrónico mediante las palabras clave async y await . Por ejemplo, en el siguiente ejemplo de formularios Windows Forms se incluye un controlador de eventos que llama y espera un método asincrónico, ExampleMethodAsync
.
public partial class Form1 : Form
{
public Form1()
{
InitializeComponent();
button1.Click += button1_Click;
}
private async void button1_Click(object sender, EventArgs e)
{
await ExampleMethodAsync();
textBox1.Text += "\r\nControl returned to Click event handler.\n";
}
private async Task ExampleMethodAsync()
{
// The following line simulates a task-returning asynchronous process.
await Task.Delay(1000);
}
}
Puede agregar el mismo controlador de eventos utilizando una lambda asincrónica. Para agregar este controlador, agregue un modificador async
antes de la lista de parámetros lambda, como se muestra en el ejemplo siguiente:
public partial class Form1 : Form
{
public Form1()
{
InitializeComponent();
button1.Click += async (sender, e) =>
{
await ExampleMethodAsync();
textBox1.Text += "\r\nControl returned to Click event handler.\n";
};
}
private async Task ExampleMethodAsync()
{
// The following line simulates a task-returning asynchronous process.
await Task.Delay(1000);
}
}
Para obtener más información sobre cómo crear y usar métodos asincrónicos, vea Programación asincrónica con async y await.
Expresiones lambda y tuplas
El lenguaje C# proporciona compatibilidad integrada para las tuplas. Puede proporcionar una tupla como argumento a una expresión lambda, mientras que la expresión lambda también puede devolver una tupla. En algunos casos, el compilador de C# usa la inferencia de tipos para determinar los tipos de componentes de la tupla.
Para definir una tupla, incluya entre paréntesis una lista delimitada por comas de los componentes. En el ejemplo siguiente se usa la tupla con tres componentes para pasar una secuencia de números a una expresión lambda, que duplica cada valor y devuelve una tupla con tres componentes que contiene el resultado de las multiplicaciones.
Func<(int, int, int), (int, int, int)> doubleThem = ns => (2 * ns.Item1, 2 * ns.Item2, 2 * ns.Item3);
var numbers = (2, 3, 4);
var doubledNumbers = doubleThem(numbers);
Console.WriteLine($"The set {numbers} doubled: {doubledNumbers}");
// Output:
// The set (2, 3, 4) doubled: (4, 6, 8)
Normalmente, los campos de una tupla se denominan Item1
, Item2
, etc. aunque puede definir una tupla con componentes con nombre, como en el ejemplo siguiente.
Func<(int n1, int n2, int n3), (int, int, int)> doubleThem = ns => (2 * ns.n1, 2 * ns.n2, 2 * ns.n3);
var numbers = (2, 3, 4);
var doubledNumbers = doubleThem(numbers);
Console.WriteLine($"The set {numbers} doubled: {doubledNumbers}");
Para más información sobre las tuplas de C#, consulte el artículo sobre los tipos de tuplas.
Lambdas con los operadores de consulta estándar
LINQ to Objects, entre otras implementaciones, tiene un parámetro de entrada cuyo tipo es uno de la familia Func<TResult> de delegados genéricos. Estos delegados usan parámetros de tipo para definir el número y el tipo de los parámetros de entrada, así como el tipo de valor devuelto del delegado. Los delegadosFunc
son útiles para encapsular expresiones definidas por el usuario que se aplican a cada elemento en un conjunto de datos de origen. Por ejemplo, considere el tipo delegado Func<T,TResult>:
public delegate TResult Func<in T, out TResult>(T arg)
Se pueden crear instancias del delegado como una instancia Func<int, bool>
, donde int
es un parámetro de entrada y bool
es el valor devuelto. El valor devuelto siempre se especifica en el último parámetro de tipo. Por ejemplo, Func<int, string, bool>
define un delegado con dos parámetros de entrada, int
y string
, y un tipo de valor devuelto de bool
. El delegado Func
siguiente, cuando se invoca, devuelve un valor booleano que indica si el parámetro de entrada es igual a cinco:
Func<int, bool> equalsFive = x => x == 5;
bool result = equalsFive(4);
Console.WriteLine(result); // False
También puede proporcionar una expresión lambda cuando el tipo de argumento es Expression<TDelegate>, (por ejemplo, en los operadores de consulta estándar que se definen en el tipo Queryable). Al especificar un argumento Expression<TDelegate>, la lambda se compila en un árbol de expresión.
En el ejemplo siguiente se usa el operador de consulta estándar Count:
int[] numbers = { 5, 4, 1, 3, 9, 8, 6, 7, 2, 0 };
int oddNumbers = numbers.Count(n => n % 2 == 1);
Console.WriteLine($"There are {oddNumbers} odd numbers in {string.Join(" ", numbers)}");
El compilador puede deducir el tipo del parámetro de entrada o también se puede especificar explícitamente. Esta expresión lambda particular cuenta aquellos enteros (n
) que divididos por dos dan como resto 1.
El siguiente ejemplo genera una secuencia que contiene todos los elementos de la matriz numbers
que preceden al 9, ya que ese es el primer número de la secuencia que no cumple la condición:
int[] numbers = { 5, 4, 1, 3, 9, 8, 6, 7, 2, 0 };
var firstNumbersLessThanSix = numbers.TakeWhile(n => n < 6);
Console.WriteLine(string.Join(" ", firstNumbersLessThanSix));
// Output:
// 5 4 1 3
En el siguiente ejemplo se especifican varios parámetros de entrada encerrándolos entre paréntesis. El método devuelve todos los elementos de la matriz numbers
hasta que encuentra un número cuyo valor es menor que la posición ordinal en la matriz:
int[] numbers = { 5, 4, 1, 3, 9, 8, 6, 7, 2, 0 };
var firstSmallNumbers = numbers.TakeWhile((n, index) => n >= index);
Console.WriteLine(string.Join(" ", firstSmallNumbers));
// Output:
// 5 4
No se usan expresiones lambda directamente en las expresiones de consulta, pero se pueden usar en las llamadas de método dentro de expresiones de consulta, como se muestra en el ejemplo siguiente:
var numberSets = new List<int[]>
{
new[] { 1, 2, 3, 4, 5 },
new[] { 0, 0, 0 },
new[] { 9, 8 },
new[] { 1, 0, 1, 0, 1, 0, 1, 0 }
};
var setsWithManyPositives =
from numberSet in numberSets
where numberSet.Count(n => n > 0) > 3
select numberSet;
foreach (var numberSet in setsWithManyPositives)
{
Console.WriteLine(string.Join(" ", numberSet));
}
// Output:
// 1 2 3 4 5
// 1 0 1 0 1 0 1 0
Inferencia de tipos en expresiones lambda
Al escribir lambdas, no tiene por qué especificar un tipo para los parámetros de entrada, ya que el compilador puede deducir el tipo según el cuerpo de lambda, los tipos de parámetros y otros factores, tal como se describe en la especificación del lenguaje C#. Para la mayoría de los operadores de consulta estándar, la primera entrada es el tipo de los elementos en la secuencia de origen. Si está realizando una consulta sobre IEnumerable<Customer>
, se deducirá que la variable de entrada será un objeto Customer
, lo cual significa que se podrá tener acceso a sus métodos y propiedades:
customers.Where(c => c.City == "London");
Las reglas generales para la inferencia de tipos de las lambdas son las siguientes:
- La lambda debe contener el mismo número de parámetros que el tipo delegado.
- Cada parámetro de entrada de la lambda debe poder convertirse implícitamente a su parámetro de delegado correspondiente.
- El valor devuelto de la lambda (si existe) debe poder convertirse implícitamente al tipo de valor devuelto del delegado.
Tipo natural de una expresión lambda
Una expresión lambda en sí misma no tiene un tipo porque el sistema de tipos común no tiene ningún concepto intrínseco de "expresión lambda". Sin embargo, a veces es conveniente hablar informalmente del "tipo" de una expresión lambda. Este "tipo" informal hace referencia al tipo del delegado o el tipo de Expression en el que se convierte la expresión lambda.
A partir de C# 10, una expresión lambda puede tener un tipo natural. En lugar de forzarle a declarar un tipo de delegado, como Func<...>
o Action<...>
para una expresión lambda, el compilador puede deducir el tipo de delegado de la expresión lambda. Por ejemplo, consideremos la siguiente declaración:
var parse = (string s) => int.Parse(s);
El compilador puede deducir que parse
sea un elemento Func<string, int>
. El compilador elige un delegado Func
o Action
disponible, si existe uno adecuado. De lo contrario, sintetiza un tipo de delegado. Por ejemplo, el tipo de delegado se sintetiza si la expresión lambda tiene parámetros ref
. Cuando una expresión lambda tiene un tipo natural, se puede asignar a un tipo menos explícito, como System.Object o System.Delegate:
object parse = (string s) => int.Parse(s); // Func<string, int>
Delegate parse = (string s) => int.Parse(s); // Func<string, int>
Los grupos de métodos (es decir, los nombres de método sin listas de parámetros) con exactamente una sobrecarga tienen un tipo natural:
var read = Console.Read; // Just one overload; Func<int> inferred
var write = Console.Write; // ERROR: Multiple overloads, can't choose
Si asigna una expresión lambda a System.Linq.Expressions.LambdaExpression o System.Linq.Expressions.Expression, y esta tiene un tipo de delegado natural, la expresión tiene un tipo natural de System.Linq.Expressions.Expression<TDelegate>, con el tipo de delegado natural utilizado como argumento para el parámetro de tipo:
LambdaExpression parseExpr = (string s) => int.Parse(s); // Expression<Func<string, int>>
Expression parseExpr = (string s) => int.Parse(s); // Expression<Func<string, int>>
No todas las expresiones lambda tienen un tipo natural. Considere la declaración siguiente:
var parse = s => int.Parse(s); // ERROR: Not enough type info in the lambda
El compilador no puede inferir un tipo de parámetro para s
. Cuando el compilador no puede inferir un tipo natural, debe declarar el tipo siguiente:
Func<string, int> parse = s => int.Parse(s);
Tipo de valor devuelto explícito
Normalmente, el tipo de valor devuelto de una expresión lambda es obvio e inferido. Para algunas expresiones eso no funciona:
var choose = (bool b) => b ? 1 : "two"; // ERROR: Can't infer return type
A partir de C# 10, puede especificar el tipo de valor devuelto de una expresión lambda antes de los parámetros de entrada. Al especificar un tipo de valor devuelto explícito, debe encuadrar entre paréntesis los parámetros de entrada:
var choose = object (bool b) => b ? 1 : "two"; // Func<bool, object>
Atributos
A partir de C# 10, puede agregar atributos a una expresión lambda y sus parámetros. En el ejemplo siguiente se muestra cómo agregar atributos a una expresión lambda:
Func<string?, int?> parse = [ProvidesNullCheck] (s) => (s is not null) ? int.Parse(s) : null;
También puede agregar atributos a los parámetros de entrada o al valor devuelto, como se muestra en el ejemplo siguiente:
var concat = ([DisallowNull] string a, [DisallowNull] string b) => a + b;
var inc = [return: NotNullIfNotNull(nameof(s))] (int? s) => s.HasValue ? s++ : null;
Como se muestra en los ejemplos anteriores, debe encuadrar entre paréntesis los parámetros de entrada al agregar atributos a una expresión lambda o a sus parámetros.
Importante
Las expresiones lambda se invocan mediante el tipo de delegado subyacente. Esto es diferente de los métodos y las funciones locales. El método Invoke
del delegado no comprueba los atributos de la expresión lambda. Los atributos no tienen ningún efecto cuando se invoca la expresión lambda. Los atributos de las expresiones lambda son útiles para el análisis de código y se pueden descubrir mediante la reflexión. Una consecuencia de esta decisión es que System.Diagnostics.ConditionalAttribute no se puede aplicar a una expresión lambda.
Captura de variables externas y el ámbito de las variables en las expresiones lambda
Las operaciones lambda pueden hacer referencia a variables externas. Estas variables externas son las variables que están en el ámbito del método que define la expresión lambda o en el ámbito del tipo que contiene la expresión lambda. Las variables que se capturan de esta manera se almacenan para su uso en la expresión lambda, cuando de otro modo quedarían fuera de ámbito y serían eliminadas por la recolección de elementos no utilizados. Para poder utilizar una variable externa en una expresión lambda, debe estar previamente asignada. En el ejemplo siguiente se muestran estas reglas:
public static class VariableScopeWithLambdas
{
public class VariableCaptureGame
{
internal Action<int>? updateCapturedLocalVariable;
internal Func<int, bool>? isEqualToCapturedLocalVariable;
public void Run(int input)
{
int j = 0;
updateCapturedLocalVariable = x =>
{
j = x;
bool result = j > input;
Console.WriteLine($"{j} is greater than {input}: {result}");
};
isEqualToCapturedLocalVariable = x => x == j;
Console.WriteLine($"Local variable before lambda invocation: {j}");
updateCapturedLocalVariable(10);
Console.WriteLine($"Local variable after lambda invocation: {j}");
}
}
public static void Main()
{
var game = new VariableCaptureGame();
int gameInput = 5;
game.Run(gameInput);
int jTry = 10;
bool result = game.isEqualToCapturedLocalVariable!(jTry);
Console.WriteLine($"Captured local variable is equal to {jTry}: {result}");
int anotherJ = 3;
game.updateCapturedLocalVariable!(anotherJ);
bool equalToAnother = game.isEqualToCapturedLocalVariable(anotherJ);
Console.WriteLine($"Another lambda observes a new value of captured variable: {equalToAnother}");
}
// Output:
// Local variable before lambda invocation: 0
// 10 is greater than 5: True
// Local variable after lambda invocation: 10
// Captured local variable is equal to 10: True
// 3 is greater than 5: False
// Another lambda observes a new value of captured variable: True
}
Las reglas siguientes se aplican al ámbito de las variables en las expresiones lambda:
- Una variable capturada no se recolectará como elemento no utilizado hasta que el delegado que hace referencia a ella sea apto para la recolección de elementos no utilizados.
- Las variables introducidas en una expresión lambda no son visibles en el método envolvente.
- Una expresión lambda no puede capturar directamente un parámetro in, ref ni out desde el método envolvente.
- Una instrucción return en una expresión lambda no hace que el método envolvente devuelva un valor.
- Una expresión lambda no puede contener una instrucción goto, break ni continue si el destino de esa instrucción de salto está fuera del bloque de la misma. También es un error utilizar una instrucción de salto fuera del bloque de la expresión lambda si el destino está dentro del bloque.
Puede aplicar el modificador static
a una expresión lambda para evitar la captura involuntaria de variables locales o estado de instancia mediante la expresión lambda:
Func<double, double> square = static x => x * x;
Una expresión lambda estática no puede capturar variables locales o el estado de la instancia desde ámbitos de inclusión, pero puede hacer referencia a miembros estáticos y definiciones de constantes.
Especificación del lenguaje C#
Para obtener más información, vea la sección Expresiones de función anónima de la Especificación del lenguaje C#.
Para obtener más información sobre estas características, consulte las siguientes notas de propuesta de características: