Есть ли причина предпочитать лямбда-синтаксис, даже если есть только один параметр?

14
List.ForEach(Console.WriteLine);

List.ForEach(s => Console.WriteLine(s));

Для меня разница чисто косметическая, но есть ли какие-то тонкие причины, по которым одно может быть предпочтительнее другого?

Benjol
источник
По моему опыту, когда вторая версия казалась предпочтительной, обычно это происходило из-за плохого наименования рассматриваемого метода.
Роман Рейнер

Ответы:

23

Глядя на скомпилированный код через ILSpy, на самом деле есть разница в двух ссылках. Для упрощенной программы, подобной этой:

namespace ScratchLambda
{
    using System;
    using System.Collections.Generic;
    using System.Linq;
    using System.Text;

    internal class Program
    {
        private static void Main(string[] args)
        {
            var list = Enumerable.Range(1, 10).ToList();
            ExplicitLambda(list);
            ImplicitLambda(list);
        }

        private static void ImplicitLambda(List<int> list)
        {
            list.ForEach(Console.WriteLine);
        }

        private static void ExplicitLambda(List<int> list)
        {
            list.ForEach(s => Console.WriteLine(s));
        }
    }
}

ILSpy декомпилирует это как:

using System;
using System.Collections.Generic;
using System.Linq;
namespace ScratchLambda
{
    internal class Program
    {
        private static void Main(string[] args)
        {
            List<int> list = Enumerable.Range(1, 10).ToList<int>();
            Program.ExplicitLambda(list);
            Program.ImplicitLambda(list);
        }
        private static void ImplicitLambda(List<int> list)
        {
            list.ForEach(new Action<int>(Console.WriteLine));
        }
        private static void ExplicitLambda(List<int> list)
        {
            list.ForEach(delegate(int s)
            {
                Console.WriteLine(s);
            }
            );
        }
    }
}

Если вы посмотрите на стек вызовов IL для обоих, реализация Explicit имеет намного больше вызовов (и создает сгенерированный метод):

.method private hidebysig static 
    void ExplicitLambda (
        class [mscorlib]System.Collections.Generic.List`1<int32> list
    ) cil managed 
{
    // Method begins at RVA 0x2093
    // Code size 36 (0x24)
    .maxstack 8

    IL_0000: ldarg.0
    IL_0001: ldsfld class [mscorlib]System.Action`1<int32> ScratchLambda.Program::'CS$<>9__CachedAnonymousMethodDelegate1'
    IL_0006: brtrue.s IL_0019

    IL_0008: ldnull
    IL_0009: ldftn void ScratchLambda.Program::'<ExplicitLambda>b__0'(int32)
    IL_000f: newobj instance void class [mscorlib]System.Action`1<int32>::.ctor(object, native int)
    IL_0014: stsfld class [mscorlib]System.Action`1<int32> ScratchLambda.Program::'CS$<>9__CachedAnonymousMethodDelegate1'

    IL_0019: ldsfld class [mscorlib]System.Action`1<int32> ScratchLambda.Program::'CS$<>9__CachedAnonymousMethodDelegate1'
    IL_001e: callvirt instance void class [mscorlib]System.Collections.Generic.List`1<int32>::ForEach(class [mscorlib]System.Action`1<!0>)
    IL_0023: ret
} // end of method Program::ExplicitLambda


.method private hidebysig static 
    void '<ExplicitLambda>b__0' (
        int32 s
    ) cil managed 
{
    .custom instance void [mscorlib]System.Runtime.CompilerServices.CompilerGeneratedAttribute::.ctor() = (
        01 00 00 00
    )
    // Method begins at RVA 0x208b
    // Code size 7 (0x7)
    .maxstack 8

    IL_0000: ldarg.0
    IL_0001: call void [mscorlib]System.Console::WriteLine(int32)
    IL_0006: ret
} // end of method Program::'<ExplicitLambda>b__0'

в то время как реализация Implicit более краткая:

.method private hidebysig static 
    void ImplicitLambda (
        class [mscorlib]System.Collections.Generic.List`1<int32> list
    ) cil managed 
{
    // Method begins at RVA 0x2077
    // Code size 19 (0x13)
    .maxstack 8

    IL_0000: ldarg.0
    IL_0001: ldnull
    IL_0002: ldftn void [mscorlib]System.Console::WriteLine(int32)
    IL_0008: newobj instance void class [mscorlib]System.Action`1<int32>::.ctor(object, native int)
    IL_000d: callvirt instance void class [mscorlib]System.Collections.Generic.List`1<int32>::ForEach(class [mscorlib]System.Action`1<!0>)
    IL_0012: ret
} // end of method Program::ImplicitLambda
Agent_9191
источник
Обратите внимание, что это сборка выпуска кода из быстрой программы, поэтому может быть место для дальнейшей оптимизации. Но это вывод по умолчанию из Visual Studio.
Agent_9191
2
+1 Это потому, что лямбда-синтаксис фактически оборачивает необработанный вызов метода в анонимную функцию <i> без причины </ i>. Это совершенно бессмысленно, поэтому вы должны использовать необработанную группу методов в качестве параметра Func <>, когда он доступен.
Эд Джеймс
Ух ты, ты получишь зеленую галочку за исследования!
Бенджол
2

Я бы предпочел лямбда-синтаксис в целом . Когда вы видите это, тогда он говорит вам, что это за тип. Когда вы видите Console.WriteLine, вы должны спросить IDE, какой это тип. Конечно, в этом тривиальном примере это очевидно, но в общем случае это может быть не так уж и много.

DeadMG
источник
Я бы предпочел синтаксис labmda для соответствия со случаями, когда это требуется.
bunglestink
4
Я не специалист по C #, но на языках, которые я использовал с лямбдами (JavaScript, Scheme и Haskell), люди могли бы дать вам противоположный совет. Я думаю, это просто показывает, насколько хороший стиль зависит от языка.
Тихон Джелвис
каким образом он говорит вам тип? конечно, вы можете явно указывать тип параметра лямбда-выражения, но это далеко не принято делать в этой ситуации
jk.
1

с двумя примерами, которые вы дали, они отличаются тем, когда вы говорите

List.ForEach(Console.WriteLine) 

вы на самом деле говорите циклу ForEach использовать метод WriteLine

List.ForEach(s => Console.WriteLine(s));

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

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

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

ТАМ
источник
1

Есть очень веская причина отдать предпочтение первой строчке.

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

public class A {
    public int Data;
    public void WriteData() {
        Console.WriteLine(this.Data);
    }
}

var a1 = new A() {Data=4};
Action action = a1.WriteData;
a1 = null;

Мы не можем позвонить, a1.WriteData();потому что a1это ноль. Однако мы можем actionбез проблем вызвать делегат, и он напечатает 4, потому что actionсодержит ссылку на экземпляр, с которым должен быть вызван метод.

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

public class Container {
    private List<int> data = new List<int>() {1,2,3,4,5};
    public void PrintItems() {
        //There is an implicit reference to an instance of Container here
        data.ForEach(s => Console.WriteLine(s));
    }
}

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

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

public class Container {
    private List<int> data = new List<int>() {1,2,3,4,5};
    public void PrintItems() {
        //Since Console.WriteLine is a static method, there is no implicit reference
        data.ForEach(Console.WriteLine);
    }
}
Зев Шпиц
источник