C# Lambda表达式和“this”变量作用域

14
我想知道是否可以在C# lambda内部使用this关键字,虽然我确实知道可以,但我想确保这不是一个坏事或将来会产生微妙的问题。
阅读变量范围lambda规则后,我可以看到:

捕获的变量在引用它的委托退出作用域之前不会被垃圾回收。

所以我认为对象实例(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个回答

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包含对Foo的引用。
如果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查看编译器输出,关闭了“反编译匿名方法/lambda”的选项。


优秀的回答。解释得很清楚。看到反汇编代码真的很酷,可以真正了解发生了什么。 - Eric Andres
我同意,看到你的代码实际上被转换成什么非常有帮助。在 Reflector 中查看过这样的代码,但以前并不真正理解它是如何工作的,所以感谢您的解释。 - Peter Monks
我已经尝试了2个小时在ILSpy中搜索“反编译匿名方法/lambda”的等效方法。我认为这部分需要更好地突出显示。谢谢。 - Sameer
1
非常棒的答案。我一直在尝试使用ILSpy解码委托闭包,但从未看到显示类。您建议的“关闭反编译匿名方法/lambda”对我很有帮助,并且可以看到编译器如何处理变量作用域。非常感谢@Qwertie的帮助。谢谢。 - Raja Moparthi

1

虽然在lambda表达式中使用this是正确的,但您需要知道,只有在Person对象被垃圾回收之后,Repository对象才能被垃圾回收。

您可能希望有一个字段来缓存lambda表达式的结果,并且一旦它被惰性填充,就释放lambda表达式,因为您不再需要它。

类似于:

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

一个有趣的想法。如果我真的想避免在lambda中使用this,我可以想到其他适用于我的实际情况的方法,但我也可能考虑这种方法,谢谢。 - Peter Monks

0

在lambda表达式中使用this是完全可以的,但是有一些事情需要注意:

  • this会一直保存在内存中,直到lambda不再使用
  • 如果您没有将带有this的lambda传递到类外部,则不会遇到问题
  • 如果您将带有this的lambda传递到类外部,则应记住,只要还有对lambda的引用,您的类就不会被GC收集。

与您的用例相关的是,您应该记住,Repository实例在创建它的人仍在使用时永远不会被GC收集。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接