-2

I want to store a method call in an action. That method is a static method, but with a string argument that varies. So I want to store "call StaticMethod with argument "myString" in an Action.
But... without capturing this.

I can store an anonymous method () => StaticMethod(myString) in the action , but this captures the action target (this). (Edit: it doesn't. See the accepted answer).

I can store the static method in the action, but then I'm missing the argument.

I could store the static method and the arguments separately, but due to technical reasons, this is cumbersome and ugly.

Do I have any other possibility?

[Edit] In other words, I want to do this:

private void MyMethod(string myString){
    var action = Call StaticMethod with argument myString. 
 }

And somewhere else, where I have access to action, but don't know anything about the string, or that there are arguments:

action.Invoke(); 

action can capture the string, but can't capture this (the call target of MyMethod).

Coder14
  • 1,305
  • 1
  • 9
  • 26
  • Where does `myString` come from? How do you want to invoke the delegate later, do you want it to obtain the value itself when you call it, or do you want to pass it in as a parameter? – Lasse V. Karlsen Jun 22 '20 at 11:51
  • How about `Action method = StaticMethod;` ? Call it with `method("Test");` – Lasse V. Karlsen Jun 22 '20 at 11:53
  • Where I invoke the action, I don't know anything about that string (or that there is a string). So what I want to do is: var myString = "something"; var action = Call StaticMethod with argument myString. (...) action.Invoke(); //No knowledge of the string here – Coder14 Jun 22 '20 at 11:54
  • So you want the lambda to [capture by value](https://stackoverflow.com/q/451779/11683)? Is there a specific reason [why](https://meta.stackexchange.com/q/66377/147640)? What makes `() => StaticMethod(myString)` wrong? – GSerg Jun 22 '20 at 11:56
  • Well, in that case the delegate will not capture `this`, but it will capture a closure constructed for you related to your method, to capture that variable. – Lasse V. Karlsen Jun 22 '20 at 11:56
  • And just to be clear, when you say "I want to do this", you **actually** mean that `myString` will be a local variable, declared just about declaring the action variable? I ask because if `myString` is a field or property of `this` instead, then all my comments change. – Lasse V. Karlsen Jun 22 '20 at 11:58
  • @LasseV.Karlsen, capturing the string is no problem. myString is a local variable. Problem is, I don't know that string (or the number and types of arguments) at the moment I invoke it. See my edit. – Coder14 Jun 22 '20 at 11:58
  • 1
    No, if you do this: `var myString = "Test"; Action action = () => StaticMethod(myString);` it will not capture `this`, and you can call it like `action();` But if `myString` is *not* a local variable or parameter to your method, this changes. So please be exact in what your situation is. Can you show a complete method so that we're sure what you're asking for here? – Lasse V. Karlsen Jun 22 '20 at 11:58
  • Also be aware that if you construct multiple delegates in your method, and one of them requires a closure that also captures `this`, then this closure will be shared for all those delegates. – Lasse V. Karlsen Jun 22 '20 at 12:01
  • It will be a closure object that contains `myString` as well as a generated method for that lambda. Anonymous methods are just lifted out into a generated class, and it's an object of this class that will be the target. – Lasse V. Karlsen Jun 22 '20 at 12:11
  • Ok I understand. Thanks! Can you post this as answer, or should I remove my question? – Coder14 Jun 22 '20 at 12:13
  • 1
    See my addition to my answer. – Lasse V. Karlsen Jun 22 '20 at 12:21

2 Answers2

4

Given the following assumptions:

  • myString is a local variable, or a parameter to the method (Not! a property or field)
  • StaticMethod is really a static method
  • There are no other delegates being constructed in the same method as you want to construct action

then the following:

public void M() {
    string myString = "something";
    Action action = () => StaticMethod(myString);
}

public static void StaticMethod(string value) { }

will be compiled as this:

[CompilerGenerated]
private sealed class <>c__DisplayClass0_0
{
    public string myString;

    internal void <M>b__0()
    {
        StaticMethod(myString);
    }
}

public void M()
{
    <>c__DisplayClass0_0 <>c__DisplayClass0_ = new <>c__DisplayClass0_0();
    <>c__DisplayClass0_.myString = "something";
    Action action = new Action(<>c__DisplayClass0_.<M>b__0);
}

As you can see, your lambda is lifted out to a generated display class, and myString is also lifted out from your method, going from a local variable to a field on that display class. This is the closure, and the object constructed in the M method will be the target of the action, and this closure will not capture this.

You can see this in action here: SharpLab

Note that small changes to your code will invalidate this as the assumptions change. For instance, if you also, in the same method, declare another delegate that also requires a closure and also requires access to this, then the same closure will be shared between the two delegates.

Example:

public void M() {
    string myString = "something";
    Action action = () => StaticMethod(myString);
    
    Action otherAction = () => StaticMethod(Property);
}
    
public string Property => "Value";

Generates this code:

[CompilerGenerated]
private sealed class <>c__DisplayClass0_0
{
    public string myString;

    public C <>4__this;

    internal void <M>b__0()
    {
        StaticMethod(myString);
    }

    internal void <M>b__1()
    {
        StaticMethod(<>4__this.Property);
    }
}

public string Property
{
    get
    {
        return "Value";
    }
}

public void M()
{
    <>c__DisplayClass0_0 <>c__DisplayClass0_ = new <>c__DisplayClass0_0();
    <>c__DisplayClass0_.<>4__this = this;
    <>c__DisplayClass0_.myString = "something";
    Action action = new Action(<>c__DisplayClass0_.<M>b__0);
    Action action2 = new Action(<>c__DisplayClass0_.<M>b__1);
}

As you can see, the same closure is used to support both delegates, and now both delegates has a reference to a closure that captures this.

Lasse V. Karlsen
  • 380,855
  • 102
  • 628
  • 825
1

If you can't define your action at call site at compile time e.:

Action a = () => StaticMethod("myString");

Then you will probably want to use expressions for this.

Example Code:

[Fact]
public void SampleActionWithoutThisScopeTests()
{
    // Using Compile Time Action
    Action compileTimeAction = () => MyStaticMethod("CompileTime myString");

    compileTimeAction.Invoke();
    
    // Using Compiled Expression
    var myString = "Expression myString";
    
    var methodInfo = this.GetType().GetMethod(nameof(MyStaticMethod), BindingFlags.Static | BindingFlags.Public);

    var stringArgumentConstant = Expression.Constant(myString);
    var callExpression = Expression.Call(null, methodInfo, stringArgumentConstant);

    var lambda = Expression.Lambda<Action>(callExpression);

    var action = lambda.Compile();
    
    action.Invoke();
}

public static void MyStaticMethod(string input)
{
    Console.WriteLine(input);
}

EDIT

If you can, and meet the requirements for the compiler-generated closure to not contain "this" via normal lambda syntax (see @Lasse V. Karlsen answer, then definitely use that approach!

Michal Ciechan
  • 13,492
  • 11
  • 76
  • 118