Анонимные методы против выражения lambda

Может ли кто-нибудь дать краткий различие между анонимным методом и лямбда-выражениями?

Использование анонимного метода:

private void DoSomeWork()
{
    if (textBox1.InvokeRequired)
    {
        //textBox1.Invoke((Action)(() => textBox1.Text = "test"));
        textBox1.Invoke((Action)delegate { textBox1.Text = "test"; });
    }
}

Это только нормальное выражение лямбда, которое передается строго типизированному делегату, или его больше скрывают.

Мне хорошо известно, что строго типизированный делегат вроде

UpdateTextDelegate mydelegate = new UpdateTextDelegate(MethodName)

достаточно в качестве параметра типа System.Delegate, но идея анонимного метода для меня довольно полезна.

Ответ 1

Что такое анонимный метод? Это действительно анонимно? У него есть имя? Все хорошие вопросы, поэтому давайте начнем с них и будем продвигаться к лямбда-выражениям по мере продвижения.

Когда вы это сделаете:

public void TestSomething()
{
    Test(delegate { Debug.WriteLine("Test"); });
}

Что на самом деле происходит?

Сначала компилятор решает взять "тело" метода, который заключается в следующем:

Debug.WriteLine("Test");

и выделите это в метод.

Два вопроса, которые должен ответить компилятору:

  • Где я должен поместить метод?
  • Как должна выглядеть подпись метода?

На второй вопрос легко ответить. Часть delegate { отвечает на это. Метод не принимает никаких параметров (ничего между delegate и {), и поскольку мы не заботимся о его имени (отсюда и "анонимная" часть), мы можем объявить метод как таковой:

public void SomeOddMethod()
{
    Debug.WriteLine("Test");
}

Но зачем это все это делало?

Посмотрим, что такое делегат, например Action.

Делегат, если мы на мгновение игнорируем тот факт, что делегаты в .NET фактически связаны списком нескольких одиночных "делегатов", ссылкой (указателем) на две вещи:

  • Объектный экземпляр
  • Метод экземпляра объекта

Итак, с этими знаниями, первая часть кода действительно может быть переписана следующим образом:

public void TestSomething()
{
    Test(new Action(this.SomeOddMethod));
}

private void SomeOddMethod()
{
    Debug.WriteLine("Test");
}

Теперь проблема заключается в том, что компилятор не знает, что именно делает Test с предоставленным им делегатом, и поскольку половина делегата является ссылкой на экземпляр, по которому этот метод назовем this в приведенном выше примере, мы не знаем, сколько данных будет указано.

Например, рассмотрите, был ли приведенный выше код частью действительно огромного объекта, но объект, который временно живет только. Также подумайте, что Test сохранит этот делегат где-нибудь, где он будет жить в течение длительного времени. Это "долгое время" также привязалось бы к жизни этого огромного объекта, сохраняя также ссылку на это в течение долгого времени, вероятно, не очень хорошо.

Таким образом, компилятор делает больше, чем просто создает метод, он также создает класс для его хранения. Это отвечает на первый вопрос, куда я должен его поместить?

Таким образом, приведенный выше код можно переписать следующим образом:

public void TestSomething()
{
    var temp = new SomeClass;
    Test(new Action(temp.SomeOddMethod));
}

private class SomeClass
{
    private void SomeOddMethod()
    {
        Debug.WriteLine("Test");
    }
}

То есть, для этого примера, что такое анонимный метод.

Вещи становятся немного более волосатыми, если вы начинаете использовать локальные переменные, рассмотрите этот пример:

public void Test()
{
    int x = 10;
    Test(delegate { Debug.WriteLine("x=" + x); });
}

Это то, что происходит под капотом или, по крайней мере, что-то очень близко к нему:

public void TestSomething()
{
    var temp = new SomeClass;
    temp.x = 10;
    Test(new Action(temp.SomeOddMethod));
}

private class SomeClass
{
    public int x;

    private void SomeOddMethod()
    {
        Debug.WriteLine("x=" + x);
    }
}

Компилятор создает класс, поднимает все переменные, которые требуется методу в этом классе, и переписывает весь доступ к локальным переменным для доступа к полям анонимного типа.

Имя класса и метод немного нечетны, спросите LINQPad, что это будет:

void Main()
{
    int x = 10;
    Test(delegate { Debug.WriteLine("x=" + x); });
}

public void Test(Action action)
{
    action();
}

Если я попрошу LINQPad вывести IL (промежуточный язык) этой программы, я получаю следующее:

// var temp = new UserQuery+<>c__DisplayClass1();
IL_0000:  newobj      UserQuery+<>c__DisplayClass1..ctor
IL_0005:  stloc.0     // CS$<>8__locals2
IL_0006:  ldloc.0     // CS$<>8__locals2

// temp.x = 10;
IL_0007:  ldc.i4.s    0A 
IL_0009:  stfld       UserQuery+<>c__DisplayClass1.x

// var action = new Action(temp.<Main>b__0);
IL_000E:  ldarg.0     
IL_000F:  ldloc.0     // CS$<>8__locals2
IL_0010:  ldftn       UserQuery+<>c__DisplayClass1.<Main>b__0
IL_0016:  newobj      System.Action..ctor

// Test(action);
IL_001B:  call        UserQuery.Test

Test:
IL_0000:  ldarg.1     
IL_0001:  callvirt    System.Action.Invoke
IL_0006:  ret         

<>c__DisplayClass1.<Main>b__0:
IL_0000:  ldstr       "x="
IL_0005:  ldarg.0     
IL_0006:  ldfld       UserQuery+<>c__DisplayClass1.x
IL_000B:  box         System.Int32
IL_0010:  call        System.String.Concat
IL_0015:  call        System.Diagnostics.Debug.WriteLine
IL_001A:  ret         

<>c__DisplayClass1..ctor:
IL_0000:  ldarg.0     
IL_0001:  call        System.Object..ctor
IL_0006:  ret         

Здесь вы можете увидеть, что имя класса UserQuery+<>c__DisplayClass1, а имя метода - <Main>b__0. Я редактировал код С#, создавший этот код, LINQPad не производит ничего, кроме IL в приведенном выше примере.

Знаки меньшего и большего, чем есть, должны гарантировать, что вы не можете случайно создать тип и/или метод, который соответствует тому, что был создан для вас компилятором.

Итак, в основном, что такое анонимный метод.

Итак, что это?

Test(() => Debug.WriteLine("Test"));

Ну, в этом случае это то же самое, это ярлык для создания анонимного метода.

Вы можете написать это двумя способами:

() => { ... code here ... }
() => ... single expression here ...

В своем первом виде вы можете написать весь код, который вы будете делать в обычном теле метода. Во второй форме вы можете написать одно выражение или оператор.

Однако в этом случае компилятор будет рассматривать это:

() => ...

так же, как это:

delegate { ... }

Они все еще анонимные методы, просто, что синтаксис () => - это ярлык для доступа к нему.

Итак, если это ярлык для доступа к нему, почему у нас это есть?

Ну, это облегчает жизнь, с которой она была добавлена, это LINQ.

Рассмотрим этот оператор LINQ:

var customers = from customer in db.Customers
                where customer.Name == "ACME"
                select customer.Address;

Этот код переписывается следующим образом:

var customers =
    db.Customers
      .Where(customer => customer.Name == "ACME")
      .Select(customer => customer.Address");

Если вы должны использовать синтаксис delegate { ... }, вам придется переписать выражения с помощью return ... и т.д., и они будут выглядеть более напуганными. Таким образом, синтаксис лямбда был добавлен, чтобы облегчить нам жизнь программистам при написании кода, как указано выше.

Итак, что такое выражения?

До сих пор я не показывал, как Test был определен, но определим Test для приведенного выше кода:

public void Test(Action action)

Этого должно хватить. В нем говорится, что "мне нужен делегат, он имеет тип Action (без параметров, не возвращающих никаких значений)".

Однако Microsoft также добавила другой способ определения этого метода:

public void Test(Expression<Func<....>> expr)

Обратите внимание, что я опустил часть, часть ...., вернусь к этому 1.

Этот код в сочетании с этим вызовом:

Test(() => x + 10);

фактически не будет передаваться делегату, ни что-нибудь, что можно вызвать (сразу). Вместо этого компилятор переписывает этот код на нечто похожее (но совсем не похожее) на код ниже:

var operand1 = new VariableReferenceOperand("x");
var operand2 = new ConstantOperand(10);
var expression = new AdditionOperator(operand1, operand2);
Test(expression);

В основном, компилятор будет создавать объект Expression<Func<...>>, содержащий ссылки на переменные, литералы, используемые операторы и т.д. и передать это дерево объектов методу.

Почему?

Ну, рассмотрим часть db.Customers.Where(...) выше.

Было бы неплохо, если бы вместо того, чтобы загружать всех клиентов (и все их данные) из базы данных клиенту, пробирая их все, выясняя, какой клиент имеет правильное имя и т.д., код действительно спросит базе данных, чтобы сразу найти этого единственного, правильного клиента?

Это цель выражения. Entity Framework, Linq2SQL или любой другой такой поддерживающий LINQ уровень базы данных будет принимать это выражение, анализировать его, выделять отдельно и записывать правильно отформатированный SQL, который будет выполняться в базе данных.

Этого никогда не получилось, если бы мы все еще предоставляли делегатам методы, содержащие IL. Это может быть сделано только из-за нескольких вещей:

  • Синтаксис, разрешенный в выражении лямбда, подходящем для Expression<Func<...>>, ограничен (нет инструкций и т.д.).
  • Синтаксис лямбда без фигурных скобок, который сообщает компилятору, что это более простая форма кода

Итак, суммируем:

  • Анонимные методы на самом деле не все, что анонимно, они заканчиваются как именованный тип с помощью именованного метода, только вам не нужно называть эти вещи сами.
  • Это много магии компилятора под капотом, который перемещает вещи вокруг, так что вам не нужно
  • Выражения и делегаты - это два способа взглянуть на некоторые из тех же вещей.
  • Выражения предназначены для фреймворков, которые хотят знать, что делает код и каким образом, чтобы они могли использовать эти знания для оптимизации процесса (например, написания инструкции SQL).
  • Делегаты предназначены для фреймворков, которые обеспокоены возможностью вызова метода

Сноска:

  • Часть .... для такого простого выражения предназначена для типа возвращаемого значения, которое вы получаете из выражения. () => ... simple expression ... допускает только выражения, то есть что-то, возвращающее значение, и не может быть несколькими операторами. Таким образом, допустимый тип выражения таков: Expression<Func<int>>, в основном, выражение является функцией (методом), возвращающей целочисленное значение.

    Обратите внимание, что выражение, возвращающее значение, является пределом для параметров или типов Expression<...>, но не делегатов. Это полностью легальный код, если тип параметра Test равен Action:

    Test(() => Debug.WriteLine("Test"));
    

    Очевидно, что Debug.WriteLine("Test") ничего не возвращает, но это законно. Если метод Test требовал выражения, но это не так, поскольку выражение должно возвращать значение.

Ответ 2

Есть одна тонкая разница, о которой вы должны знать. Рассмотрим следующие запросы (используя пресловутую NorthWind).

Customers.Where(delegate(Customers c) { return c.City == "London";});
Customers.Where(c => c.City == "London");

Первый использует анонимный делегат, а второй использует лямбда-выражение. Если вы оцениваете результаты обоих, вы увидите то же самое. Однако, глядя на сгенерированный SQL, мы увидим совсем другую историю. Первый генерирует

SELECT [t0].[CustomerID], [t0].[CompanyName], [t0].[ContactName], [t0].[ContactTitle], [t0].[Address], [t0].[City], [t0].[Region], [t0].[PostalCode], [t0].[Country], [t0].[Phone], [t0].[Fax]
FROM [Customers] AS [t0]

В то время как второе генерирует

SELECT [t0].[CustomerID], [t0].[CompanyName], [t0].[ContactName], [t0].[ContactTitle], [t0].[Address], [t0].[City], [t0].[Region], [t0].[PostalCode], [t0].[Country], [t0].[Phone], [t0].[Fax]
FROM [Customers] AS [t0]
WHERE [t0].[City] = @p0

Обратите внимание, что в первом случае предложение where не передается в базу данных. Почему это? Компилятор может определить, что выражение лямбда - это простое однострочное выражение, которое можно сохранить как дерево выражений, тогда как анонимный делегат не является лямбда-выражением и, следовательно, не может быть заменен как Expression<Func<T>>. В результате в первом случае лучшим совпадением для метода расширения Where является тот, который расширяет IEnumerable, а не версию IQueryable, для которой требуется Expression<Func<T, bool>>.

На данный момент для анонимного делегата мало пользы. Это более подробный и менее гибкий. В общем, я бы рекомендовал всегда использовать синтаксис лямбда над анонимным синтаксисом делегата, а также подобрать аналитическую и синтаксическую терминологию.

Ответ 3

Если быть точным, то, что вы называете "анонимным делегатом", на самом деле является анонимным методом.

Ну, как лямбды, так и анонимные методы - это просто синтаксический сахар. Компилятор будет генерировать для вас хотя бы "нормальный" метод, хотя иногда (в случае замыкания) он генерирует вложенный класс с не более длинным анонимным методом в нем.