¿Hay alguna razón para preferir la sintaxis lambda incluso si solo hay un parámetro?

14
List.ForEach(Console.WriteLine);

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

Para mí, la diferencia es puramente cosmética, pero ¿hay alguna razón sutil por la cual uno podría preferirse sobre el otro?

Benjol
fuente
En mi experiencia, cuando la segunda versión parecía preferible, generalmente se debía a una mala denominación del método en cuestión.
Roman Reiner el

Respuestas:

23

Mirando el código compilado a través de ILSpy, en realidad hay una diferencia en las dos referencias. Para un programa simplista como este:

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 lo descompila como:

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 observa la pila de llamadas de IL para ambos, la implementación explícita tiene muchas más llamadas (y crea un método generado):

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

mientras que la implementación implícita es más concisa:

.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
fuente
Tenga en cuenta que esta es la versión de lanzamiento del código de un programa de scratch rápido, por lo que puede haber espacio para una mayor optimización. Pero esta es la salida predeterminada de Visual Studio.
Agent_9191
2
+1 Esto se debe a que la sintaxis lambda en realidad está envolviendo la llamada al método sin procesar en una función anónima <i> sin motivo </i>. Esto es completamente inútil, por lo tanto, debe usar el grupo de métodos sin procesar como parámetro Func <> cuando esté disponible.
Ed James
¡Guau, obtienes la marca verde, para la investigación!
Benjol
2

Prefiero la sintaxis lambda en general . Cuando veas eso, te dirá cuál es el tipo. Cuando vea Console.WriteLine, tendría que preguntarle al IDE de qué tipo es. Por supuesto, en este ejemplo trivial, es obvio, pero en el caso general, podría no ser tanto.

DeadMG
fuente
Prefiero la sintaxis labmda para mantener la coherencia con los casos en los que se requiere.
bunglestink
44
No estoy en una persona de C #, pero en los idiomas que he usado con lambdas (JavaScript, Scheme y Haskell) la gente probablemente te daría el consejo opuesto. Creo que eso muestra cuán buen estilo depende del idioma.
Tikhon Jelvis
de qué manera te dice el tipo? ciertamente puede ser explícito sobre el tipo de parámetro lambdas, pero está lejos de ser común hacerlo, y no se hace en esta situación
jk.
1

con los dos ejemplos que diste, difieren en eso cuando dices

List.ForEach(Console.WriteLine) 

en realidad le estás diciendo al ForEach Loop que use el método WriteLine

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

en realidad está definiendo un método que llamará el foreach y luego le está diciendo qué manejar allí.

por lo tanto, para líneas simples, si su método al que va a llamar lleva la misma firma que el método que ya se llamó, preferiría no definir la lambda, creo que es un poco más legible.

Los métodos con lambdas incompatibles son definitivamente una buena opción, suponiendo que no sean demasiado complicados.

tam
fuente
1

Hay una razón muy fuerte para preferir la primera línea.

Cada delegado tiene una Targetpropiedad, que permite a los delegados referirse a métodos de instancia, incluso después de que la instancia se haya salido del alcance.

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;

No podemos llamar a1.WriteData();porque a1es nulo. Sin embargo, podemos invocar al actiondelegado sin problemas, y se imprimirá 4, porque actioncontiene una referencia a la instancia con la que se debe llamar al método.

Cuando los métodos anónimos se pasan como delegados en un contexto de instancia, el delegado aún tendrá una referencia a la clase que lo contiene, aunque no sea obvio:

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

En este caso específico, es razonable suponer que .ForEachno está almacenando el delegado internamente, lo que significaría que la instancia deContainer y todos sus datos aún se conservan. Pero no hay garantía de eso; el método que recibe al delegado puede retener al delegado y la instancia indefinidamente.

Los métodos estáticos, por otro lado, no tienen una instancia de referencia. Lo siguiente no tendrá una referencia implícita a la instancia 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
fuente