C#Lambdas和“this”变量范围

时间:2012-06-19 14:59:16

标签: c# lambda scope this

我想知道我是否可以在C#lambda中使用this关键字,虽然实际上我知道我可以但我想确保这不是一件坏事或者稍后会产生微妙的问题。

阅读了variable scope for lambdas的规则后,我可以看到:

  

捕获的变量在收到之前不会被垃圾收集   引用它的委托超出了范围。

因此,这使我假设还将捕获对象实例(this)。为了测试这个,我写了这个人为的例子,这是我想要在我的真实代码中实现的目标 - 用LINQPad编写,因此为什么我有Dump()方法调用:

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

这会运行并给我正确的输出,因此从lambda中访问this显然有效。我想要检查的是:

  • 这是否遵循与局部变量相同的变量范围规则,这意味着this引用保留在内存中,直到不再使用lambda为止?从我的小实验中可以看出来,但如果有人能提供进一步的细节,我会感兴趣。
  • 这是可取的吗?我不想在此模式可能导致问题的情况下陷入困境。

3 个答案:

答案 0 :(得分:14)

在lambda中使用this没有任何问题,但正如您所提到的,如果您使用this(或者如果您隐式使用它,则通过调用任何非静态成员函数或使用非静态函数成员变量)然后垃圾收集器将保持this引用的对象至少只要委托是活着的。由于你将lambda传递给Lazy,这意味着Repository至少只要Lazy对象存活就会存活(即使你从不调用Lazy.Value

为了揭开它的神秘面纱,有助于查看反汇编程序。请考虑以下代码:

class Foo {
    static Action fLambda, gLambda;

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

标准编译器将此更改为以下内容(尝试忽略<>个额外的尖括号)。如您所见,使用函数体内部变量的lambda将转换为类:

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

}

如果使用this,无论是隐式还是显式,它都会成为编译器生成的类中的成员变量。因此,f()DisplayClass1的类不包含对Foo的引用,但g()的类DisplayClass2的引用。

如果lambda不引用任何局部变量,编译器会以更简单的方式处理它。因此,请考虑一些略有不同的代码:

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

这次lambda不引用任何局部变量,因此编译器将lambda函数转换为普通函数。 p()中的lambda不使用this,因此它变为静态函数(称为<p>b__0); q()中的lambda确实使用this(隐式),因此它变为非静态函数(称为<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;
}

注意:我使用ILSpy查看了编译器输出,并选择“反编译匿名方法/ lambdas”关闭

答案 1 :(得分:1)

虽然在这样的lambda中使用this是正确的,但您只需要知道在Repository对象是垃圾收集之前,您的Person对象不会是垃圾收集的。

你可能希望有一个字段来缓存lambda的结果,一旦它被Lazy填充,就释放lambda,因为你不再需要了它。

类似的东西:

private Lazy<string> nameProxy; 
private string name;
public string Name 
{ 
  get 
  {
    if(name==null)
    {
      name = nameProxy.Value;
      nameProxy = null;
    }
    return name;
  } 
} 

答案 2 :(得分:0)

在lambda中使用this绝对没问题,但是你应该记住一些东西:

  • this将保留在内存中,直到不再使用lambda
  • 如果你没有在课外传递lambda“this”,那么你就不会遇到问题
  • 如果你在课堂外传递lambda“with this”,那么你应该记住,在{lambmda'被引用之前,GC不会收集你的类。

与您的用例相关,您应该记住Repository实例永远不会被GC收集,直到它创建的人员正在使用中。