Partilhar via


Expressões do Lambda e funções anônimas

Você usa uma expressão lambda para criar uma função anônima. Use o operador => de declaração lambda para separar a lista de parâmetros do lambda de seu corpo. Uma expressão lambda pode ser de qualquer uma das duas formas a seguir:

Para criar uma expressão lambda, especifique parâmetros de entrada (se houver) no lado esquerdo do operador lambda e uma expressão ou um bloco de instrução no outro lado.

Qualquer expressão lambda pode ser convertida em um tipo de delegado . Os tipos de seus parâmetros e valor de retorno definem o tipo de delegado para o qual uma expressão lambda pode ser convertida. Se uma expressão lambda não retornar um valor, ela poderá ser convertida em um dos tipos de Action delegado, caso contrário, poderá ser convertida em um dos tipos de Func delegado. Por exemplo, uma expressão lambda que tem dois parâmetros e não retorna nenhum valor pode ser convertida em um Action<T1,T2> delegado. Uma expressão lambda que tem um parâmetro e retorna um valor pode ser convertida em um Func<T,TResult> delegado. No exemplo a seguir, a expressão x => x * xlambda , que especifica um parâmetro chamado x e retorna o valor de x quadrado, é atribuída a uma variável de um tipo delegado:

Func<int, int> square = x => x * x;
Console.WriteLine(square(5));
// Output:
// 25

As lambdas de expressão também podem ser convertidas para os tipos de árvore de expressão, como mostra o exemplo a seguir:

System.Linq.Expressions.Expression<Func<int, int>> e = x => x * x;
Console.WriteLine(e);
// Output:
// x => (x * x)

Você usa expressões lambda em qualquer código que exija instâncias de tipos delegados ou árvores de expressão. Um exemplo é o argumento para o Task.Run(Action) método para passar o código que deve ser executado em segundo plano. Você também pode usar expressões lambda ao escrever LINQ em C#, como mostra o exemplo a seguir:

int[] numbers = { 2, 3, 4, 5 };
var squaredNumbers = numbers.Select(x => x * x);
Console.WriteLine(string.Join(" ", squaredNumbers));
// Output:
// 4 9 16 25

Quando você usa sintaxe baseada em método para chamar o Enumerable.Select método na System.Linq.Enumerable classe, por exemplo, em LINQ to Objects e LINQ to XML, o parâmetro é um tipo System.Func<T,TResult>delegado. Quando você chama o Queryable.SelectSystem.Linq.Queryable método na classe, por exemplo, em LINQ to SQL, o tipo de parâmetro é um tipo Expression<Func<TSource,TResult>>de árvore de expressão. Em ambos os casos, você pode usar a mesma expressão lambda para especificar o valor do parâmetro. Isso faz com que as duas Select chamadas pareçam semelhantes, embora na verdade o tipo de objetos criados a partir das lambdas seja diferente.

Expressão lambdas

Uma expressão lambda com uma expressão no lado direito do => operador é chamada de expressão lambda. Uma expressão lambda retorna o resultado da expressão e assume a seguinte forma básica:

(input-parameters) => expression

O corpo de uma expressão lambda pode consistir em uma chamada de método. No entanto, se você estiver criando árvores de expressão que são avaliadas fora do contexto do .NET Common Language Runtime (CLR), como no SQL Server, você não deve usar chamadas de método em expressões lambda. Os métodos não têm significado fora do contexto do .NET Common Language Runtime (CLR).

Declaração lambdas

Uma instrução lambda se assemelha a uma expressão lambda, exceto que suas instruções estão entre chaves:

(input-parameters) => { <sequence-of-statements> }

O corpo de uma declaração lambda pode consistir em qualquer número de declarações; no entanto, na prática, normalmente não há mais do que dois ou três.

Action<string> greet = name =>
{
    string greeting = $"Hello {name}!";
    Console.WriteLine(greeting);
};
greet("World");
// Output:
// Hello World!

Não é possível usar lambdas de instrução para criar árvores de expressão.

Parâmetros de entrada de uma expressão lambda

Você coloca parâmetros de entrada de uma expressão lambda entre parênteses. Especifique parâmetros de entrada zero com parênteses vazios:

Action line = () => Console.WriteLine();

Se uma expressão lambda tiver apenas um parâmetro de entrada, os parênteses serão opcionais:

Func<double, double> cube = x => x * x * x;

Dois ou mais parâmetros de entrada são separados por vírgulas:

Func<int, int, bool> testForEquality = (x, y) => x == y;

Às vezes, o compilador não pode inferir os tipos de parâmetros de entrada. Você pode especificar os tipos explicitamente como mostrado no exemplo a seguir:

Func<int, string, bool> isTooLong = (int x, string s) => s.Length > x;

Os tipos de parâmetros de entrada devem ser todos explícitos ou implícitos; caso contrário, ocorrerá um erro de compilador CS0748 .

Você pode usar descartar para especificar dois ou mais parâmetros de entrada de uma expressão lambda que não são usados na expressão:

Func<int, int, int> constant = (_, _) => 42;

Os parâmetros de descarte do Lambda podem ser úteis quando você usa uma expressão lambda para fornecer um manipulador de eventos.

Nota

Para compatibilidade com versões anteriores, se apenas um único parâmetro de entrada for nomeado _, então, dentro de uma expressão lambda, _ será tratado como o nome desse parâmetro.

A partir do C# 12, você pode fornecer valores padrão para parâmetros em expressões lambda. A sintaxe e as restrições nos valores de parâmetros padrão são as mesmas que para métodos e funções locais. O exemplo a seguir declara uma expressão lambda com um parâmetro padrão e, em seguida, chama-a uma vez usando o padrão e uma vez com dois parâmetros explícitos:

var IncrementBy = (int source, int increment = 1) => source + increment;

Console.WriteLine(IncrementBy(5)); // 6
Console.WriteLine(IncrementBy(5, 2)); // 7

Você também pode declarar expressões lambda com params matrizes ou coleções 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 dessas atualizações, quando um grupo de métodos que tem um parâmetro padrão é atribuído a uma expressão lambda, essa expressão lambda também tem o mesmo parâmetro padrão. Um grupo de métodos com um params parâmetro collection também pode ser atribuído a uma expressão lambda.

Expressões lambda com parâmetros padrão ou params coleções como parâmetros não têm tipos naturais que correspondam a Func<> ou Action<> tipos. No entanto, você pode definir tipos delegados que incluam valores de parâmetro padrão:

delegate int IncrementByDelegate(int source, int increment = 1);
delegate int SumDelegate(params int[] values);
delegate int SumCollectionDelegate(params IEnumerable<int> values);

Ou, você pode usar variáveis digitadas implicitamente com var declarações para definir o tipo de delegado. O compilador sintetiza o tipo de delegado correto.

Para obter mais informações sobre parâmetros padrão em expressões lambda, consulte a especificação de recurso para parâmetros padrão em expressões lambda.

Lambdas assíncronas

Você pode criar facilmente expressões lambda e instruções que incorporam processamento assíncrono usando as palavras-chave async e await . Por exemplo, o exemplo de Windows Forms a seguir contém um manipulador de eventos que chama e aguarda um método assíncrono, 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);
    }
}

Você pode adicionar o mesmo manipulador de eventos usando um lambda assíncrono. Para adicionar esse manipulador, adicione um async modificador antes da lista de parâmetros lambda, como mostra o exemplo a seguir:

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 obter mais informações sobre como criar e usar métodos assíncronos, consulte Programação assíncrona com async e await.

Expressões lambda e tuplas

A linguagem C# fornece suporte interno para tuplas. Você pode fornecer uma tupla como argumento para uma expressão lambda, e sua expressão lambda também pode retornar uma tupla. Em alguns casos, o compilador C# usa inferência de tipo para determinar os tipos de componentes de tupla.

Você define uma tupla colocando entre parênteses uma lista delimitada por vírgulas de seus componentes. O exemplo a seguir usa tupla com três componentes para passar uma sequência de números para uma expressão lambda, que dobra cada valor e retorna uma tupla com três componentes que contém o resultado das multiplicações.

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, os campos de uma tupla são nomeados Item1, Item2, e assim por diante. No entanto, você pode definir uma tupla com componentes nomeados, como faz o exemplo a seguir.

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 obter mais informações sobre tuplas C#, consulte Tipos de tupla.

Lambdas com os operadores de consulta padrão

LINQ to Objects, entre outras implementações, tem um parâmetro de entrada cujo tipo é um da Func<TResult> família de delegados genéricos. Esses delegados usam parâmetros de tipo para definir o número e o tipo de parâmetros de entrada e o tipo de retorno do delegado. Func Os delegados são úteis para encapsular expressões definidas pelo usuário que são aplicadas a cada elemento em um conjunto de dados de origem. Por exemplo, considere o Func<T,TResult> tipo de delegado:

public delegate TResult Func<in T, out TResult>(T arg)

O delegado pode ser instanciado como uma Func<int, bool> instância onde int é um parâmetro de entrada e bool é o valor de retorno. O valor de retorno é sempre especificado no último parâmetro type. Por exemplo, Func<int, string, bool> define um delegado com dois parâmetros int de entrada e string, e um tipo de retorno de bool. O seguinte Func delegado, quando invocado, retorna o valor booleano que indica se o parâmetro de entrada é igual a cinco:

Func<int, bool> equalsFive = x => x == 5;
bool result = equalsFive(4);
Console.WriteLine(result);   // False

Você também pode fornecer uma expressão lambda quando o tipo de argumento é um Expression<TDelegate>, por exemplo, nos operadores de consulta padrão definidos no Queryable tipo. Quando você especifica um Expression<TDelegate> argumento, o lambda é compilado em uma árvore de expressão.

O exemplo a seguir usa o Count operador de consulta padrão:

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)}");

O compilador pode inferir o tipo do parâmetro de entrada, ou você também pode especificá-lo explicitamente. Esta expressão lambda em particular conta os inteiros (n) que, quando divididos por dois, têm um restante de 1.

O exemplo a seguir produz uma sequência que contém todos os elementos na numbers matriz que precedem o 9, porque esse é o primeiro número na sequência que não atende à condição:

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

O exemplo a seguir especifica vários parâmetros de entrada colocando-os entre parênteses. O método retorna todos os elementos na numbers matriz até encontrar um número cujo valor é menor do que sua posição ordinal na 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

Você não usa expressões lambda diretamente em expressões de consulta, mas pode usá-las em chamadas de método dentro de expressões de consulta, como mostra o exemplo a seguir:

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

Inferência de tipo em expressões lambda

Ao escrever lambdas, muitas vezes você não precisa especificar um tipo para os parâmetros de entrada porque o compilador pode inferir o tipo com base no corpo lambda, nos tipos de parâmetros e em outros fatores, conforme descrito na especificação da linguagem C#. Para a maioria dos operadores de consulta padrão, a primeira entrada é o tipo dos elementos na sequência de origem. Se você estiver consultando um IEnumerable<Customer>, então a variável de entrada é inferida como um Customer objeto, o que significa que você tem acesso aos seus métodos e propriedades:

customers.Where(c => c.City == "London");

As regras gerais para inferência de tipo para lambdas são as seguintes:

  • O lambda deve conter o mesmo número de parâmetros que o tipo delegado.
  • Cada parâmetro de entrada no lambda deve ser implicitamente conversível em seu parâmetro delegado correspondente.
  • O valor de retorno do lambda (se houver) deve ser implicitamente conversível para o tipo de retorno do delegado.

Tipo natural de uma expressão lambda

Uma expressão lambda em si não tem um tipo porque o sistema de tipos comum não tem um conceito intrínseco de "expressão lambda". No entanto, às vezes é conveniente falar informalmente do "tipo" de uma expressão lambda. Esse "tipo" informal refere-se ao tipo Expression ou tipo delegado para o qual a expressão lambda é convertida.

A partir do C# 10, uma expressão lambda pode ter um tipo natural. Em vez de forçá-lo a declarar um tipo de delegado, como Func<...> ou Action<...> para uma expressão lambda, o compilador pode inferir o tipo de delegado da expressão lambda. Por exemplo, considere a seguinte declaração:

var parse = (string s) => int.Parse(s);

O compilador pode inferir parse ser um Func<string, int>arquivo . O compilador escolhe um disponível Func ou Action delegado, se existir um adequado. Caso contrário, ele sintetiza um tipo de delegado. Por exemplo, o tipo de delegado é sintetizado se a expressão lambda tiver ref parâmetros. Quando uma expressão lambda tem um tipo natural, ela pode ser atribuída a um tipo menos explícito, como System.Object ou System.Delegate:

object parse = (string s) => int.Parse(s);   // Func<string, int>
Delegate parse = (string s) => int.Parse(s); // Func<string, int>

Grupos de métodos (ou seja, nomes de métodos sem listas de parâmetros) com exatamente uma sobrecarga têm um tipo natural:

var read = Console.Read; // Just one overload; Func<int> inferred
var write = Console.Write; // ERROR: Multiple overloads, can't choose

Se você atribuir uma expressão lambda a System.Linq.Expressions.LambdaExpression, ou System.Linq.Expressions.Expression, e o lambda tiver um tipo de delegado natural, a expressão terá um tipo natural de , com o tipo de System.Linq.Expressions.Expression<TDelegate>delegado natural usado como argumento para o parâmetro type:

LambdaExpression parseExpr = (string s) => int.Parse(s); // Expression<Func<string, int>>
Expression parseExpr = (string s) => int.Parse(s);       // Expression<Func<string, int>>

Nem todas as expressões lambda têm um tipo natural. Considere a seguinte declaração:

var parse = s => int.Parse(s); // ERROR: Not enough type info in the lambda

O compilador não pode inferir um tipo de parâmetro para s. Quando o compilador não pode inferir um tipo natural, você deve declarar o tipo:

Func<string, int> parse = s => int.Parse(s);

Tipo de retorno explícito

Normalmente, o tipo de retorno de uma expressão lambda é óbvio e inferido. Para algumas expressões que não funcionam:

var choose = (bool b) => b ? 1 : "two"; // ERROR: Can't infer return type

A partir do C# 10, você pode especificar o tipo de retorno de uma expressão lambda antes dos parâmetros de entrada. Ao especificar um tipo de retorno explícito, você deve colocar entre parênteses os parâmetros de entrada:

var choose = object (bool b) => b ? 1 : "two"; // Func<bool, object>

Atributos

A partir do C# 10, você pode adicionar atributos a uma expressão lambda e seus parâmetros. O exemplo a seguir mostra como adicionar atributos a uma expressão lambda:

Func<string?, int?> parse = [ProvidesNullCheck] (s) => (s is not null) ? int.Parse(s) : null;

Você também pode adicionar atributos aos parâmetros de entrada ou valor de retorno, como mostra o exemplo a seguir:

var concat = ([DisallowNull] string a, [DisallowNull] string b) => a + b;
var inc = [return: NotNullIfNotNull(nameof(s))] (int? s) => s.HasValue ? s++ : null;

Como mostram os exemplos anteriores, você deve colocar entre parênteses os parâmetros de entrada ao adicionar atributos a uma expressão lambda ou seus parâmetros.

Importante

As expressões do Lambda são invocadas por meio do tipo de delegado subjacente. Isso é diferente de métodos e funções locais. O método do Invoke delegado não verifica atributos na expressão lambda. Os atributos não têm qualquer efeito quando a expressão lambda é invocada. Os atributos em expressões lambda são úteis para análise de código e podem ser descobertos por meio de reflexão. Uma consequência desta decisão é que o System.Diagnostics.ConditionalAttribute não pode ser aplicado a uma expressão lambda.

Captura de variáveis externas e escopo de variáveis em expressões lambda

Lambdas podem referir-se a variáveis externas. Essas variáveis externas são as variáveis que estão no escopo no método que define a expressão lambda, ou no escopo no tipo que contém a expressão lambda. As variáveis capturadas dessa maneira são armazenadas para uso na expressão lambda, mesmo que as variáveis saiam do escopo e sejam coletadas como lixo. Uma variável externa deve ser definitivamente atribuída antes de poder ser consumida em uma expressão lambda. O exemplo a seguir demonstra essas regras:

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
}

As regras a seguir se aplicam ao escopo variável em expressões lambda:

  • Uma variável capturada não será coletada até que o delegado que a referenciar se torne elegível para a coleta de lixo.
  • As variáveis introduzidas em uma expressão lambda não são visíveis no método de delimitação.
  • Uma expressão lambda não pode capturar diretamente um parâmetro in, ref ou out do método de delimitação.
  • Uma instrução return em uma expressão lambda não faz com que o método de inclusão retorne.
  • Uma expressão lambda não pode conter uma instrução goto, break ou continue se o destino dessa instrução jump estiver fora do bloco de expressão lambda. Também é um erro ter uma instrução jump fora do bloco de expressão lambda se o destino estiver dentro do bloco.

Você pode aplicar o static modificador a uma expressão lambda para evitar a captura não intencional de variáveis locais ou estado da instância pelo lambda:

Func<double, double> square = static x => x * x;

Um lambda estático não pode capturar variáveis locais ou o estado da instância a partir de escopos incluídos, mas pode fazer referência a membros estáticos e definições constantes.

Especificação da linguagem C#

Para obter mais informações, consulte a seção Expressões de função anônimas da especificação da linguagem C#.

Para obter mais informações sobre esses recursos, consulte as seguintes notas de proposta de recurso:

Consulte também