Search code examples
c#lambdaanonymous-methods

How and where are anonymous methods without parameters used?


When comparing anonymous methods to lambda expressions, I've seen explanations that anonymous methods provide flexibility.

Flexibility here means that you can omit parameters from anonymous methods.

// inflexible anonymous method

Action<int> action = delegate(int number) 
{ 
    Console.WriteLine($"Anonymous method: {number}"); 
}
action(1234);

// Output
// Anonymous method: 1234
// flexible anonymous method

Action<int> action = delegate 
{ 
    Console.WriteLine("Anonymous method"); 
}
action(1234);

// Output
// Anonymous method
Action<int> action = (number) => 
{ 
    Console.WriteLine($"Lambda expression: {number}"); 
};
action(1234);

// Output
// Lambda expression: 1234

However, I have two questions.

First, how does an anonymous method with an omitted parameter use the passed value? (1234 here)

Second, where do you use the flexibility that anonymous methods provide?


Solution

  • I'll only answer the first question. The second question is highly opinion based, depends on context and at best I can offer incomplete / insufficient examples. Refer to the examples offered in Action Delegate. One use case is Composability

    How does an anonymous method with an omitted parameter use the passed value?

    It doesn't. The generated IL (intermediate language) doesn't account in any way for the provided argument. Which seems logical because if it does, it needs to have knowledge about all the callers of that method.

    IL_0000:  ldsfld      UserQuery.CS$<>9__CachedAnonymousMethodDelegate1
    IL_0005:  brtrue.s    IL_0018
    IL_0007:  ldnull      
    IL_0008:  ldftn       UserQuery.<Main>b__0
    IL_000E:  newobj      System.Action<System.Int32>..ctor
    IL_0013:  stsfld      UserQuery.CS$<>9__CachedAnonymousMethodDelegate1
    IL_0018:  ldsfld      UserQuery.CS$<>9__CachedAnonymousMethodDelegate1
    IL_001D:  stloc.0     // action
    IL_001E:  ldloc.0     // action
    IL_001F:  ldc.i4      D2 04 00 00 
    IL_0024:  callvirt    System.Action<System.Int32>.Invoke
    IL_0029:  ret         
    
    <Main>b__0:
    IL_0000:  ldstr       "Anonymous method"
    IL_0005:  call        System.Console.WriteLine
    IL_000A:  ret         
    

    On IL_001F the value 1234 is pushed on the stack. In <Main>b__0 there is no "pop" from the stack. So the value is lost.

    If you would have a method that takes an int, like so void test(int arg) the IL would look like:

    test:
    IL_0000:  ldstr       "Test method"
    IL_0005:  ldarg.1     
    IL_0006:  box         System.Int32
    IL_000B:  call        System.Console.WriteLine
    IL_0010:  ret 
    

    Here it does "pop" the argument of the stack in IL_005.

    It is worth remembering that a delegate is a generated type that derives from System.Delegate and there is an implementation detail you see on line IL_0024.

    The common language runtime provides an Invoke method for each delegate type, with the same signature as the delegate. You do not have to call this method explicitly from C#, Visual Basic, or Visual C++, because the compilers call it automatically.

    Do note that your first and last example return the same IL for the .Net 6 compiler, which you can inspect here