Czy istnieje powód, aby preferować składnię lambda, nawet jeśli jest tylko jeden parametr?

14
List.ForEach(Console.WriteLine);

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

Dla mnie różnica jest czysto kosmetyczna, ale czy istnieją jakieś subtelne powody, dla których jedno może być preferowane nad drugim?

Benjol
źródło
Z mojego doświadczenia wynika, że ​​zawsze, gdy druga wersja wydawała się lepsza, zwykle było to spowodowane złym nazwaniem omawianej metody.
Roman Reiner

Odpowiedzi:

23

Patrząc na skompilowany kod za pomocą ILSpy, w rzeczywistości istnieją dwie różnice w dwóch odnośnikach. W przypadku takiego uproszczonego programu:

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 dekompiluje to jako:

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

Jeśli spojrzysz na stos wywołań IL dla obu, implementacja Jawna zawiera o wiele więcej wywołań (i tworzy wygenerowaną metodę):

.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'

podczas gdy implementacja niejawna jest bardziej zwięzła:

.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
źródło
Należy pamiętać, że jest to wersja kompilacji kodu z programu szybkiego scratchowania, więc może być miejsce na dalszą optymalizację. Ale to jest domyślne wyjście z Visual Studio.
Agent_9191,
2
+1 To dlatego, że składnia lambda faktycznie otacza surowe wywołanie metody w anonimowej funkcji <i> bez powodu </i>. Jest to całkowicie bezcelowe, dlatego powinieneś użyć surowej grupy metod jako parametru Func <>, gdy jest on dostępny.
Ed James
Wow, dostałeś zielonego kleszcza, do badań!
Benjol,
2

Wolałbym ogólnie składnię lambda . Kiedy to zobaczysz, powie ci, jaki jest typ. Kiedy zobaczysz Console.WriteLine, będziesz musiał zapytać IDE, jaki to typ. Oczywiście w tym trywialnym przykładzie jest to oczywiste, ale w ogólnym przypadku może nie być tak wiele.

DeadMG
źródło
Wolę składnię labmda dla spójności z przypadkami, w których jest to wymagane.
bunglestink
4
Nie jestem osobą w języku C #, ale w językach, których używałem z lambdas (JavaScript, Scheme i Haskell), ludzie prawdopodobnie dawaliby ci przeciwne rady. Myślę, że to pokazuje, jak dobry styl zależy od języka.
Tikhon Jelvis,
w jaki sposób mówi ci typ? z pewnością możesz wyraźnie powiedzieć o typie parametru lambdas, ale nie jest to często spotykane i nie robi się tego w tej sytuacji
jk.
1

w dwóch podanych przez ciebie przykładach różnią się tym, co mówisz

List.ForEach(Console.WriteLine) 

tak naprawdę mówisz ForEach Loop, aby używał metody WriteLine

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

faktycznie definiuje metodę, którą wywoła foreach, a następnie mówisz jej, co z tym zrobić.

więc dla prostych linijek, jeśli twoja metoda, którą zamierzasz wywołać, nosi ten sam podpis, co metoda, która została już wywołana Wolałbym nie definiować lambda, myślę, że jest to trochę bardziej czytelne.

ponieważ metody z niekompatybilnymi lambdami są zdecydowanie dobrą drogą, zakładając, że nie są zbyt skomplikowane.

tam
źródło
1

Jest bardzo silny powód, aby preferować pierwszą linię.

Każdy delegat ma Targetwłaściwość, która pozwala mu odwoływać się do metod instancji, nawet po tym, jak instancja wyszła poza zakres.

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;

Nie możemy zadzwonić, a1.WriteData();ponieważ a1ma wartość zero. Możemy jednak actionbez problemu wywołać delegata, który wydrukuje 4, ponieważ actionzawiera odwołanie do instancji, z którą należy wywołać metodę.

Gdy anonimowe metody są przekazywane jako delegat w kontekście instancji, delegat nadal będzie zawierał odwołanie do zawierającej klasy, nawet jeśli nie jest to oczywiste:

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

W tym konkretnym przypadku uzasadnione jest założenie, że .ForEachnie przechowuje się delegata wewnętrznie, co oznaczałoby, że instancja Containeri wszystkie jej dane są nadal przechowywane. Ale nie ma na to gwarancji; metoda odbierająca delegata może zatrzymać delegata i instancję na czas nieokreślony.

Z drugiej strony metody statyczne nie mają instancji, do których można się odwoływać. Poniższe informacje nie będą miały domyślnego odniesienia do wystąpienia 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);
    }
}
Zev Spitz
źródło