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 ser de 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 parámetros de entrada (si existen) en el lado izquierdo del operador lambda y una expresión o un bloque de instrucciones en el otro lado.
Cualquier expresión lambda se puede convertir en un tipo de 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 de delegado de Action
; De lo contrario, se puede convertir en uno de los tipos de delegado de Func
. Por ejemplo, una expresión lambda que tiene dos parámetros y no devuelve ningún valor se puede convertir en un delegado de Action<T1,T2>. Una expresión lambda que tiene un parámetro y devuelve un valor se puede convertir en un delegado de 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
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 expresiones de , como se muestra en el ejemplo siguiente:
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 basada en métodos para llamar al método Enumerable.Select en la clase System.Linq.Enumerable, por ejemplo, en LINQ to Objects y LINQ to XML, el parámetro es un tipo de 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, puede usar la misma expresión lambda para especificar el valor del parámetro. Esto hace que las dos llamadas Select
parezcan similares, aunque de hecho el tipo de objetos creados a partir de las expresiones lambda es diferente.
Lambdas de expresión
Una expresión lambda con una expresión en el lado derecho del operador =>
se denomina expresión lambda . Una expresión lambda devuelve el resultado de la expresión y toma la siguiente forma básica:
(input-parameters) => expression
El cuerpo de una expresión lambda puede constar de una llamada de 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
Incluya los parámetros de entrada de una expresión lambda entre paréntesis. Especifique cero parámetros de entrada con 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 están separados por 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 explícitamente como se muestra en el ejemplo siguiente:
Func<int, string, bool> isTooLong = (int x, string s) => s.Length > x;
Los tipos de parámetros de entrada deben ser todos explícitos o todos implícitos; De lo contrario, se produce un error del compilador de 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
Para la compatibilidad con versiones anteriores, si solo se denomina un único parámetro de entrada _
, en 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 de 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, se llama una vez mediante el valor predeterminado y una 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 params
matrices o colecciones como 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 de params
como parámetros no tienen tipos naturales que se correspondan con 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 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, el siguiente ejemplo de Windows Forms contiene un controlador de eventos que llama a 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 mediante 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 una tupla.
Para definir una tupla, incluya una lista delimitada por comas de sus componentes entre paréntesis. 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 contienen 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. Sin embargo, puede definir una tupla con componentes con nombre, como hace 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 parámetros de entrada y el tipo de valor devuelto del delegado. Los delegados Func
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 de delegado Func<T,TResult>:
public delegate TResult Func<in T, out TResult>(T arg)
El delegado puede ser instanciado como una instancia de 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 siguiente Func
delegado, 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 un Expression<TDelegate>, por ejemplo, en los operadores de consulta estándar definidos en el tipo Queryable. Al especificar un argumento Expression<TDelegate>, la expresión lambda se compila en un árbol de expresiones.
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 puede especificarlo explícitamente. Esta expresión lambda determinada cuenta los enteros (n
) que, cuando se dividen entre dos, tienen un resto de 1.
En el ejemplo siguiente se genera una secuencia que contiene todos los elementos de la matriz numbers
que preceden al 9, ya que 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 ejemplo siguiente se especifican varios parámetros de entrada al incluirlos 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 su 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 expresiones de consulta, pero puede usarlas en 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, a menudo no es necesario especificar un tipo para los parámetros de entrada porque el compilador puede deducir el tipo en función del cuerpo lambda, los tipos de parámetro 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 de la secuencia de origen. Si está consultando un IEnumerable<Customer>
, se deduce que la variable de entrada es un objeto Customer
, lo que significa que tiene acceso a sus métodos y propiedades:
customers.Where(c => c.City == "London");
Las reglas generales para la inferencia de tipos para lambdas son las siguientes:
- La expresión lambda debe contener el mismo número de parámetros que el tipo de delegado.
- Cada parámetro de entrada de la expresión lambda debe convertirse implícitamente en su parámetro 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. Ese "tipo" informal hace referencia al tipo delegado o al tipo Expression al que se convierte la expresión lambda.
Una expresión lambda puede tener un tipo natural . En lugar de forzar 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, considere la siguiente declaración:
var parse = (string s) => int.Parse(s);
El compilador puede deducir parse
ser un Func<string, int>
. El compilador elige un delegado de 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 la expresión lambda tiene un tipo delegado natural, la expresión tiene un tipo natural de System.Linq.Expressions.Expression<TDelegate>, con el tipo de delegado natural usado 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. Tenga en cuenta la siguiente declaración:
var parse = s => int.Parse(s); // ERROR: Not enough type info in the lambda
El compilador no puede deducir un tipo de parámetro para s
. Cuando el compilador no puede deducir un tipo natural, debe declarar el tipo:
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 que no funcionan:
var choose = (bool b) => b ? 1 : "two"; // ERROR: Can't infer return type
Puede especificar el tipo de valor devuelto de una expresión lambda antes de los parámetros de entrada. Al especificar un tipo de retorno explícito, debe colocar entre paréntesis los parámetros de entrada.
var choose = object (bool b) => b ? 1 : "two"; // Func<bool, object>
Atributos
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 a través del 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 en 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 detectar a través de la reflexión. Una consecuencia de esta decisión es que el System.Diagnostics.ConditionalAttribute no se puede aplicar a una expresión lambda.
Captura de variables externas y ámbito de variable en 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, incluso si las variables de otro modo saldrían del ámbito y serían recolectadas por el recolector de basura. Una variable externa debe asignarse definitivamente antes de que se pueda consumir en una expresión lambda. 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 variable en 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 dentro de 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, breako continue si el destino de esa instrucción de salto está fuera del bloque de la expresión lambda. 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 el 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: