Y a-t-il une raison de préférer la syntaxe lambda même s'il n'y a qu'un seul paramètre?

14
List.ForEach(Console.WriteLine);

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

Pour moi, la différence est purement cosmétique, mais y a-t-il des raisons subtiles pour lesquelles l'une pourrait être préférée à l'autre?

Benjol
la source
D'après mon expérience, chaque fois que la deuxième version semblait préférable, c'était généralement en raison d'une mauvaise dénomination de la méthode en question.
Roman Reiner

Réponses:

23

En regardant le code compilé via ILSpy, il y a en fait une différence entre les deux références. Pour un programme simpliste comme celui-ci:

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 le décompile comme:

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

Si vous regardez la pile d'appels IL pour les deux, l'implémentation explicite a beaucoup plus d'appels (et crée une méthode générée):

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

tandis que l'implémentation implicite est plus concise:

.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
la source
Notez qu'il s'agit de la version finale du code d'un programme de scratch rapide, donc il peut y avoir de la place pour une optimisation supplémentaire. Mais c'est la sortie par défaut de Visual Studio.
Agent_9191
2
+1 C'est parce que la syntaxe lambda enveloppe en fait l'appel de méthode brute dans une fonction anonyme <i> sans raison </i>. Ceci est complètement inutile, par conséquent, vous devez utiliser le groupe de méthodes brutes comme paramètre Func <> lorsqu'il est disponible.
Ed James
Wow, vous obtenez la coche verte, pour la recherche!
Benjol
2

Je préfère la syntaxe lambda en général . Lorsque vous voyez cela, il vous indique quel est le type. Lorsque vous voyez Console.WriteLine, vous devez demander à l'IDE de quel type il s'agit. Bien sûr, dans cet exemple trivial, c'est évident, mais dans le cas général, ce n'est peut-être pas tant.

DeadMG
la source
Je préférerais la syntaxe labmda pour la cohérence avec les cas où elle est requise.
bunglestink
4
Je ne suis pas en C #, mais dans les langages que j'ai utilisés avec les lambdas (JavaScript, Scheme et Haskell), les gens vous donneraient probablement le contraire. Je pense que cela montre à quel point le bon style dépend de la langue.
Tikhon Jelvis
de quelle manière vous indique-t-il le type? vous pouvez certainement être explicite sur le type d'un paramètre lambdas mais son loin d'être courant pour le faire, et ne se fait pas dans cette situation
jk.
1

avec les deux exemples que vous avez donnés, ils diffèrent en ce que lorsque vous dites

List.ForEach(Console.WriteLine) 

vous dites en fait à la boucle ForEach d'utiliser la méthode WriteLine

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

définit en fait une méthode que foreach appellera, puis vous lui direz quoi y gérer.

donc pour un simple liners si votre méthode que vous allez appeler porte la même signature que la méthode qui est déjà appelée, je préférerais ne pas définir le lambda, je pense que c'est un peu plus lisible.

pour les méthodes avec des lambdas incompatibles sont certainement une bonne façon de procéder, en supposant qu'elles ne sont pas trop compliquées.

tam
la source
1

Il y a une raison très forte de préférer la première ligne.

Chaque délégué a une Targetpropriété, qui permet aux délégués de se référer aux méthodes d'instance, même après que l'instance est hors de portée.

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;

Nous ne pouvons pas appeler a1.WriteData();car a1est nul. Cependant, nous pouvons appeler le actiondélégué sans problème, et il s'imprimera 4, car actioncontient une référence à l'instance avec laquelle la méthode doit être appelée.

Lorsque des méthodes anonymes sont passées en tant que délégué dans un contexte d'instance, le délégué contiendra toujours une référence à la classe contenante, même si ce n'est pas évident:

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

Dans ce cas spécifique, il est raisonnable de supposer que .ForEachne stocke pas le délégué en interne, ce qui signifierait que l'instance deContainer et toutes ses données sont toujours conservées. Mais il n'y a aucune garantie de cela; la méthode de réception du délégué peut s'accrocher indéfiniment au délégué et à l'instance.

Les méthodes statiques, en revanche, n'ont aucune instance à référencer. Les éléments suivants n'auront pas de référence implicite à l'instance de 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
la source