C# Lambdas and “this” variable scope

2019-04-19 18:38发布

I am wondering whether I can use the this keyword inside a C# lambda, although actually I know that I can but I want to make sure that this isn't a bad thing or will produce subtle issues later on.

Having read the rules on variable scope for lambdas, I can see that:

A variable that is captured will not be garbage-collected until the delegate that references it goes out of scope.

So this leads me to assume that an object instance (this) will also be captured. To test this I wrote this contrived example which is what I want to approximately aim for in my real code - written in LINQPad, hence why I have the Dump() method calls:

void Main()
{
    Repository repo = new Repository();
    Person person = repo.GetPerson(1);

    person.ID.Dump("Person ID - Value Assigned");
    person.Name.Dump("Person Name - Lazily Created");
}

class Person
{
    public Person(Lazy<string> name)
    {
        this.name = name;
    }

    public int ID { get; set; }

    private Lazy<string> name;
    public string Name
    {
        get { return name.Value; }
    }
}

class Repository
{
    public Person GetPerson(int id)
    {
        // Setup person to lazily load a name value
        Person person = new Person(
            new Lazy<string>(
                () => this.GetName()    // <--- This I'm not sure on...
            )
        );
        person.ID = id;
        return person;
    }

    public string GetName()
    {
        return "John Smith";
    }
}

This runs and gives me the correct output so accessing this from within a lambda clearly works. What I am wanting to check though is:

  • Does this follow the same variable scope rules as local variables, meaning that the this reference is kept in memory until the lambda is not used anymore? It would appear so from my little experiment but if anyone can give further details I'd be interested.
  • Is this advisable? I do not want to get into the situation later where this pattern could cause problems.

3条回答
Melony?
2楼-- · 2019-04-19 19:16

There is nothing wrong with using this in a lambda, but as you mention, if you do use this (or if you use it implicitly, by calling any nonstatic member function or using a nonstatic member variable) then the garbage collector will keep the object that this refers to alive at least as long as the delegate is alive. Since you pass a lambda to Lazy, this implies that the Repository will be alive at least as long as the Lazy object is alive (even if you never call Lazy.Value).

To demystify it a bit, it helps to look in a disassembler. Consider this code:

class Foo {
    static Action fLambda, gLambda;

    int x;
    void f() {
        int y = 0;
        fLambda = () => ++y;
    }
    void g() {
        int y = 0;
        gLambda = () => y += x;
    }
}

The standard compiler changes this to the following (try to ignore the <> extra angle brackets). As you can see, lambdas that use variables from inside the function body are transformed into classes:

internal class Foo
{
    private static Action fLambda;
    private static Action gLambda;
    private int x;

    private void f()
    {
        Foo.<>c__DisplayClass1 <>c__DisplayClass = new Foo.<>c__DisplayClass1();
        <>c__DisplayClass.y = 0;
        Foo.fLambda = new Action(<>c__DisplayClass.<f>b__0);
    }
    private void g()
    {
        Foo.<>c__DisplayClass4 <>c__DisplayClass = new Foo.<>c__DisplayClass4();
        <>c__DisplayClass.<>4__this = this;
        <>c__DisplayClass.y = 0;
        Foo.gLambda = new Action(<>c__DisplayClass.<g>b__3);
    }

    [CompilerGenerated]
    private sealed class <>c__DisplayClass1
    {
        public int y;
        public void <f>b__0()
        {
            this.y++;
        }
    }
    [CompilerGenerated]
    private sealed class <>c__DisplayClass4
    {
        public int y;
        public Foo <>4__this;
        public void <g>b__3()
        {
            this.y += this.<>4__this.x;
        }
    }

}

If you use this, whether implicitly or explicitly, it becomes a member variable in the compiler-generated class. So the class for f(), DisplayClass1, does not contain a reference to Foo, but the class for g(), DisplayClass2, does.

The compiler handles lambdas in a simpler manner if they don't reference any local variables. So consider some slightly different code:

public class Foo {
    static Action pLambda, qLambda;

    int x;
    void p() {
        int y = 0;
        pLambda = () => Console.WriteLine("Simple lambda!");
    }
    void q() {
        int y = 0;
        qLambda = () => Console.WriteLine(x);
    }
}

This time the lambdas don't reference any local variables, so the compiler translates your lambda functions into ordinary functions. The lambda in p() does not use this so it becomes a static function (called <p>b__0); the lambda in q() does use this (implicitly) so it becomes a non-static function (called <q>b__2):

public class Foo {
    private static Action pLambda, qLambda;

    private int x;
    private void p()
    {
        Foo.pLambda = new Action(Foo.<p>b__0);
    }
    private void q()
    {
        Foo.qLambda = new Action(this.<q>b__2);
    }
    [CompilerGenerated] private static void <p>b__0()
    {
        Console.WriteLine("Simple lambda!");
    }
    [CompilerGenerated] private void <q>b__2()
    {
        Console.WriteLine(this.x);
    }
    // (I don't know why this is here)
    [CompilerGenerated] private static Action CS$<>9__CachedAnonymousMethodDelegate1;
}

Note: I viewed the compiler output using ILSpy with the option "decompile anonymous methods/lambdas" turned off.

查看更多
Viruses.
3楼-- · 2019-04-19 19:18

While it is correct to use this in a lambda like that, you just need to be aware that your Repository object will not be garbage collectable until your Person object is garbage collectable.

You might want to have a field to cache the result from your lambda, and once it is Lazy filled, release the lambda since you do not need it anymore.

Something like:

private Lazy<string> nameProxy; 
private string name;
public string Name 
{ 
  get 
  {
    if(name==null)
    {
      name = nameProxy.Value;
      nameProxy = null;
    }
    return name;
  } 
} 
查看更多
霸刀☆藐视天下
4楼-- · 2019-04-19 19:20

It is absolutelly fine to use this in lambdas, but there is some stuff you should keep in mind:

  • this will be kept in memory until the lambda is not used anymore
  • if you do not pass lambda "with this" outside your class, then you will not face problems
  • if you do pass lambda "with this" outside your class, then you should remember, that your class will not be collected by the GC until there are references to the lambda left.

And related to your use-case, you should keep in mind that the Repository instance will never be collected by the GC until people it created are in use.

查看更多
登录 后发表回答