我正在考虑不同的闭包实现方式,并且想知道不同风格的优点。似乎有两种选择,即基于执行上下文或值进行封闭。例如,基于上下文的实现如下:
a = 1
def f():
return a
f() # returns 1
a = 2
f() # returns 2
另外,我们可以通过闭包来获取值,代码如下:
a = 1
def f():
return a
f() # returns 1
a = 2
f() # returns 1
是否有语言实现第二个?优缺点是什么?
我正在考虑不同的闭包实现方式,并且想知道不同风格的优点。似乎有两种选择,即基于执行上下文或值进行封闭。例如,基于上下文的实现如下:
a = 1
def f():
return a
f() # returns 1
a = 2
f() # returns 2
另外,我们可以通过闭包来获取值,代码如下:
a = 1
def f():
return a
f() # returns 1
a = 2
f() # returns 1
是否有语言实现第二个?优缺点是什么?
在这种情况下,我认为问题不在于上下文与值的关系,而在于您是将变量作为引用单元还是变量所包含的值进行闭合。
如果您真正意味着上下文,那么您是指动态范围和词法范围。请参阅this维基百科文章,以进行深入比较。
大多数语言实现词法范围(或尝试实现)。一些语言确实实现了动态范围:特别是旧版Lisp,例如emacs的ELisp。大多数具有闭包的语言(例如Scheme、Haskell、ML等)在词法范围内关闭值。动态范围通常被认为是一个坏主意,因为它更难以理解(这是“幽灵般的远距离作用”)。
请注意,即使在词法范围的语言中,如果您将引用单元闭合,则可以获得类似于第一个示例的行为。这就是为什么Scheme和JavaScript闭包的行为像它们一样(因为变量是引用单元)。
各种编程语言都有这两种方式中的一种或两种。
主要区别在于当您对变量进行赋值时会发生什么。因此,正如其他人指出的那样,在变量是不可变的语言中
在按值捕获的语言中,一个问题是如何处理对该变量的赋值。由于它是按值捕获的
正如其他人指出的那样,很多语言没有明确的语法来处理按值捕获和按引用捕获,包括:Python,Ruby,JavaScript,Scheme,Perl,Go,Smalltalk等。 正如其他人指出的那样,ML语言(SML,OCaml)和Haskell可以说是按值捕获,因为它们的变量是不可变的,所以两者之间没有真正的区别,而按值捕获更简单。 正如其他人指出的那样,Java要求捕获的变量必须是final
,基本上是为了按值捕获,否则在相同的作用域中有两个单独的可变副本就会产生混淆;但当它们是final
时,它们无法被修改,因此拥有一个副本和多个副本之间没有区别。
C++11允许您选择按值还是按引用捕获。将要捕获的变量列在方括号中。带有&
的变量按引用捕获;否则,它是按值捕获。仅使用=
表示按值捕获所有未列出的变量;仅使用&
表示按引用捕获所有未列出的变量。在按引用捕获变量时,必须小心不要捕获超出范围的变量。有趣的是(与Java不同),可以通过在匿名函数上使用mutable
修饰符使其成为可变的变量,并按值捕获该变量。
同样地,PHP允许您在声明变量时选择捕获方式。使用&
表示按引用捕获;否则按值捕获。
Apple开发工具中的块(用于C、C++和Objective-C语言;在Mac OS X 10.6+和iOS 4+中可用)也允许您进行选择。当您首次创建一个块时,它可以访问按引用捕获的捕获变量;但是,如果捕获局部变量,这样的块将不被允许离开作用域(例如返回)。必须复制一个块才能使其离开作用域;当复制块时,捕获的变量按值捕获。还可以在声明该变量时使用__block
修饰符,指示要在复制后由块按引用捕获该局部变量。这可能会将其分配到堆上。C++ lambdas 可以通过值显式捕获:
int a = 1;
auto f1 = [a]() -> int { return a; }
f1() == 1;
a = 2;
f1() == 1;
或者通过引用:
a = 1;
auto f2 = [&a]() -> int { return a; }
f2() == 1;
a = 2;
f2() == 2;
你也可以隐式地捕获任何一种方式:
auto f1 = [=]() -> int { return a; }
auto f2 = [&]() -> int { return a; }
优点是您可以控制复制或引用哪些变量以及是否复制或引用。潜在的缺点是您必须注意生命周期问题,因为C++引用是非拥有的:如果a
超出范围,则调用f1
仍然有效,但调用f2
是未定义的。如果自然而然且您不介意开销,您可以始终捕获一个shared_ptr<T>
(具有共享所有权的指针)。
因此,对于不可变值:
按值捕获会强制进行复制。按引用捕获则不会。
按值捕获没有所有权问题。按引用捕获则有。
对于可变值,您当然必须按引用捕获。这里有一个类似于std::partial_sum()
的人为示例:
int sum = 0;
auto f = [&sum](int i) -> int { sum += i; return sum; }
vector<int> input{1, 2, 3, 4, 5};
vector<int> output;
transform(begin(input), end(input), back_inserter(output), f);
sum == 15;
output == vector{1, 3, 6, 10, 15};
int* y = &x;
,然后按值捕获y
。Lambda表达式需要显式地解引用y
,这很丑陋,并且具有相同的生命周期问题:x
仍然可能超出范围。因此,我认为使用引用更有意义。 - Jon Purdy闭包应该像第一种情况一样运作,但有些语言提供了第二种情况。
Smalltalk 按照第一种情况工作。假设一个类定义了方法 m 和 test:
m
| counter c | "temporary vars"
counter = 0.
c = [ counter = counter + 1. counter ].
^ c. "returns the closure"
test
| c | "temporary vars"
c = self m. "obtain a closure that increments a counter"
c value. "return 1"
c value. " returns 2"
m
| counter c | "temporary vars"
counter = 0.
c = [ counter = counter + 1. counter ].
counter = 1.
^ c. "returns the closure"
我猜这会破坏优化,因为counter在闭包创建后被改变了。
至少我是这样理解闭包的。
Felix实际上提供了相当复杂的语义,有时候是违反直觉的。闭包通过指向上下文帧的指针来捕获上下文..在形成闭包的时候。因此,您会期望捕获的变量始终反映出在执行闭包时变量的当前值。
但事实并非如此,因为优化器可能会用其值替换变量,特别是如果“变量”声明为:
val x = 1;
它被视为不可变的值,这样的替换被认为是安全的。即使该值作为参数传递也是如此!例如:
fun f(x:int) () => x;
val y = 1;
val fy = f y; // closure formed
println$ fy();
很可能我们已经将fy定义为if:
val fy = fun () => 1;
已经被编写。在这种情况下,变量可能是相同的:
var z = 1;
val fz = f z;
z = 2;
println$ fz (); // prints 1 .. maybe
通过将x替换为闭包形成时z的值,但也可以通过将x替换为变量名z来打印2。
在Felix中,应用哪种优化是不确定的,这是故意的:它允许编译器自由选择(它认为最好的优化)。
如果您想强制解释,可以使用以下参数参数:
fun f(var x:int) () => x; // 强制急切求值,将参数复制到参数 fun f( x: unit -> int ) => x(); // 强制延迟求值
对于原始问题,您可以通过简单地使用指针来强制进行延迟解释:
var x = 1;
fun f()=> *&x;
强制使用急切解释是没有意义的。如果您想这样做:
var x = 1;
val y = x;
var x = 2;
fun f() => y; // prints 1
我必须说,我对这些语义并不满意,但目前情况就是这样,并且似乎相当合乎逻辑。更令人困扰的是:
var g : unit -> int;
for var i = 0 upto 10 do
val x = i;
fun f()() => x;
if i == 3 do
g = f();
done
done
for循环是平的,没有堆栈帧。 这里的“x”是一个值,但它不是不可变的! 如果你能预测出g()打印的值,那么你比我做得好(而我设计了这种语言:))
不幸的是,通过这些语义获得的优化是强制性的:我们不想最终性能像Haskell一样糟糕(无意冒犯)。
故事的寓意是:“如果您的代码取决于OP问题的答案,那就自负其责吧!编写语义确定的代码,如果您需要的话。”
...(variable)
)。 - om-nom-nom