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
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级别aFoo*
/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();
}
}