C#闭包变量范围

C#闭包变量范围,c#,.net,C#,.net,关于变量作用域如何应用于闭包的(另一个?)问题。下面是一个简单的例子: public class Foo { public string name; public Foo(string name) { this.name = name; } } public class Program { static Action getAction(Foo obj) { return () => Console.Wri

关于变量作用域如何应用于闭包的(另一个?)问题。下面是一个简单的例子:

public class Foo
{
    public string name;
    public Foo(string name)
    {
        this.name = name;
    }
}


public class Program
{
    static Action getAction(Foo obj)
    {
        return () => Console.WriteLine(obj.name); 
    }

    static void Main(string[] args)
    {
        Foo obj1 = new Foo("x1");    
        Action a = getAction(obj1);  
        obj1 = new Foo("x2");        
        a();                         
    }
}
这将打印
x1
。可以解释为:

getAction
返回一个匿名函数,该函数包含一个包含变量
obj
的闭包
obj
obj1
具有相同的参考值,但其与
obj1
的关系到此结束,因为闭包仅包含
obj
。换句话说,
obj1
之后取的任何值都不会影响闭包。因此,无论何时/无论如何,
a
被调用(例如,
a
被传递到某个其他函数),它总是打印
x1

现在我的问题是:

  • 上述解释正确吗
  • 我心里没有具体的场景,但是如果我们想让程序打印
    x2
    (例如关闭以封闭外部范围),该怎么办?这是可以做到的(或者尝试一下也没有意义)

  • 简短回答:解释是正确的,如果要将值从
    x1
    更改为
    x2
    ,则必须更改传递给操作的特定对象

    obj1.name='x2'

    当对象作为参数传递给函数时,它会将引用(指针)复制到obj

    此时,您有一个对象和两个引用

    • Foo obj1
      Main
      中的变量,并且
    • Foo obj
      getAction
    每当您选择将另一个对象(或null)设置为
    obj1
    时,它不会影响
    getAction
    中的第二个引用,让我们考虑一下:

    static Action getAction(Foo obj)
    {
        return () => Console.WriteLine(obj.name); 
    }
    
    闭合在参数
    obj
    上方;此
    obj
    是一个按值传递的引用,因此如果调用方这样做:

    x = someA();
    var action = getAction(x);
    x = someB(); // not seen by action
    
    然后闭包仍然在原始值之上,因为引用(而不是对象)在传递给
    getAction
    时被复制

    请注意,如果调用者更改原始对象上的值,则该方法将看到:

    x = someA();
    var action = getAction(x);
    x.name = "something else"; // seen by action
    
    getAction
    方法中,它基本上是:

    var tmp = new SomeCompilerGeneratedType();
    tmp.obj = obj;
    return new Action(tmp.SomeCompilerGeneratedMethod);
    
    与:


    以下是为Main生成的IL:

    IL_0000:  ldstr       "x1"
    IL_0005:  newobj      UserQuery+Foo..ctor
    IL_000A:  stloc.0     // obj1
    IL_000B:  ldloc.0     // obj1
    IL_000C:  call        UserQuery.getAction
    IL_0011:  stloc.1     // a
    IL_0012:  ldstr       "x2"
    IL_0017:  newobj      UserQuery+Foo..ctor
    IL_001C:  stloc.0     // obj1
    IL_001D:  ldloc.1     // a
    IL_001E:  callvirt    System.Action.Invoke
    IL_0023:  ret      
    
    从中我推断,当您调用
    getAction()
    时,它会为
    obj1
    创建带有值的方法,当您创建新实例并调用委托时,由于闭包,它在编译器创建的方法中有以前的值,因此它会打印x1

    当您调用
    getAction(obj1)
    时,
    Foo obj
    现在指的是new Foo(“X1”)`,然后您在做
    obj1=new Foo(“x2”)
    ,现在
    obj1
    是指
    new Foo(“x2”)
    Foo obj
    仍然指的是
    new Foo(“X1”)


    您可以将代码重写为

    public class Program
    {
        static void Main(string[] args)
        {
            Foo obj1 = new Foo("x1");
            // rewrite of
            // Action a = GetAction( obj1 );
            Foo obj = obj1;
            Action a = () => Console.WriteLine( obj.name );  
    
            obj1 = new Foo("x2");        
            a();                         
        }
    }
    

    这是内部发生的事情。您将引用分配给
    obj
    ,并构建一个引用
    obj

    的操作。您的解释是正确的,基本上是对中C语言规范中所写内容进行重新表述的一种方式(为了完整性,在此报告,重点是我的):

    未使用ref或out修饰符声明的参数是值 参数

    值参数在调用 函数成员(方法、实例构造函数、访问器或 运算符)(第7.4节),参数所属,且为 使用调用中给定的参数值初始化。A 返回函数成员后,value参数不再存在

    为了确定赋值检查,需要一个值参数 被认为是最初分配的


    您的解释似乎很明显,因为您有两个不同的变量,它们最初引用相同的对象,但随后您将不同的对象分配给其中一个,这对另一个没有影响。我猜,将您的
    obj
    参数声明为
    ref
    可能会解决您的第二个问题,但我不是100%确定。@jmchilinney不允许在闭包中捕获
    ref
    参数。@PetSerAl,我想这可能是为了明确避免这里描述的情况,或者至少避免可能出现的情况。@jmchilinney IIRC,在IL级别a
    Foo*
    /
    ref Foo
    字段(引用到引用,而不是指针)可以很好地理解,但这不是一个可以用C#表示的概念(因此我甚至不得不近似语法来讨论它!),因此,闭包与手工编码具有相同的特征限制可能是合理的;你有一些很好的答案,所以我不再补充了。我会注意到,你可能有点误用了“范围”这个词。命名实体的范围定义为程序文本的区域,在该区域中,该实体可以不加限制地按名称使用。例如,方法
    Main
    可能不引用
    obj
    ,因为
    obj
    的范围不包括方法
    Main
    。但是
    Main
    可以使用
    Foo
    ,因为公共类的作用域确实包括同一命名空间中另一个类中
    Main
    的主体。我猜您的意思是
    tmp.SomeCompilerGeneratedMethod
    。不是
    obj.SomeCompilerGeneratedMethod
    谢谢。。这证实了我对捕获如何在这里工作的理解。我的第二个问题可能是最重要的-如何“扩展”关闭范围,例如,如果希望在关闭范围中包含
    obj1
    ,如何实现?如果我们可以将引用变量作为
    ref
    传递,我假设可以做到这一点,但这似乎是不允许的。@ubi您需要传递一个对属性为的对象的引用
    obj1 = new Foo("x1")  // obj1 is referencing to ----> Foo("x1") memory location
    getAction(obj1)  // --> getAction(Foo obj) obj is referencing to Foo("x1")
    obj1 = new Foo("x2") //  obj1 is now referencing to----> Foo("x2") memory location
    // but in getAction(Foo obj) obj is still referencing to Foo("x1")
    
    public class Program
    {
        static void Main(string[] args)
        {
            Foo obj1 = new Foo("x1");
            // rewrite of
            // Action a = GetAction( obj1 );
            Foo obj = obj1;
            Action a = () => Console.WriteLine( obj.name );  
    
            obj1 = new Foo("x2");        
            a();                         
        }
    }