闭包捕获与lazy val字段

几天前有同事聊起这个话题:大家在review一段Scala代码,里面有一个lazy val字段被用在一个闭包里,而这个闭包是要被序列化传到别的机器上去执行的。话题的重点就是在闭包前这段代码特意前访问了一下这个lazy val字段的值,引起同事讨论是否有必要。

其中一位同事对Scala不熟而对微软系技术更熟,于是我就把这个例子用C#来写了一次给他看。结果他没听说过C#里有System.Lazy<T>…不过还好C#的Lazy<T>是标准库层面的功能,毕竟magic少一些,还是比Scala的好解释一些。

C#和Scala的代码例子在这个gist里:demo C# and Scala's lazy val and lambda capturing

真正的重点是:

  • C#和Scala里如果“捕获了字段”的话,其实捕获的是this而不是单独捕获了一个字段,在lambda里访问字段其实是通过捕获的this来访问的。Java 8的lambda也是如此。
  • 对this的捕获并不会导致lazy val被求值,所以就算lambda里会用到一个lazy val字段,在闭包创建的时候并不会当时就导致该字段求值。如果要包装这个lazy val字段在lambda表达式实际被调用时得到闭包创建时的一些环境状态的话,得自己显式在闭包创建前对这个lazy val字段求一下值才行。

然后同事表示还是C++好,lambda的捕获列表很明确哪些是capture-by-value哪些是capture-by-reference。然而其实来个 [&] 就可以把按引用捕获this的事实给隐含起来了,对新手来说也没友好多少吧(ry

#include <iostream>
#include <functional>

class Foo {
  int val_ = 0;

public:
  std::function<int()> get_func() {
    return [&]() { return val_; };
  }

  void incr() { val_++; }
};

int main() {
  Foo foo;
  const auto& f = foo.get_func();
  std::cout << f() << std::endl;
  foo.incr();
  std::cout << f() << std::endl;
  return 0;
}

让不熟悉现代C++的人来读这段代码,能看出来那个lambda隐式按引用捕获了this么 >_<

编辑于 2017-06-30 13:35