我很好奇这样的功能需要经过哪些考虑。
首先,我写了一篇关于这个主题的博客,还有其他的。请看我的旧博客:
http://blogs.msdn.com/b/ericlippert/
和我的新博客:
http://ericlippert.com
有许多关于语言设计各个方面的文章。
其次,C#设计过程现在对公众开放,因此您可以自行查看语言设计团队在审核新功能建议时考虑的内容。详情请参见https://github.com/dotnet/roslyn/。
添加此类关键字的成本是什么?
这取决于很多因素。当然,没有便宜、易实现的功能,只有成本较低、难度较小的功能。一般来说,成本包括设计、规定、实现、测试、文档编写和维护功能。还有更多奇特的成本,比如错过了实现更好功能的机会成本,或者选择与未来想要添加的功能相互作用不良的功能的成本。
在这种情况下,该功能可能只是将“lazy”关键字作为使用Lazy<T>
的语法糖。这是一个非常直接的功能,不需要太多花哨的语法或语义分析。
这会在哪些情况下成为问题?
我可以想到许多因素会使我反对该功能。
首先,它并非必需;它只是一种方便的语法糖。它并没有真正为语言增加新的功能。好处似乎不值得成本。
其次,更重要的是,它将一种特定类型的惰性编码固定在语言中。有多种类型的惰性编码,我们可能会选择错误的类型。
有多种懒加载的方式,你需要一个被称为记忆化属性的属性。要实现它,我们需要什么保证呢?有很多可能性:1. 完全不支持多线程。如果在两个不同的线程上第一次调用该属性,那么任何事情都可能发生。如果想避免竞争条件,必须自己添加同步。2. 线程安全,这样在两个不同的线程上调用该属性时,它们都会调用初始化函数,然后竞争看谁填充缓存中的实际值。理论上,该函数将在两个线程上返回相同的值,因此额外的成本只是浪费了一次额外的调用。但是缓存是线程安全的,并且不会阻塞任何线程。(因为线程安全缓存可以使用低锁定或无锁代码编写)。实现线程安全的代码需要代价,即使是低锁定代码也是如此。这个代价是否可接受?大多数人编写的程序实际上是单线程的;是否应该将线程安全的开销添加到每个懒加载属性调用中,无论是否需要呢?
可能性3:线程安全,可以强有力地保证初始化函数只被调用一次;缓存没有竞争。用户可能会默认初始化函数只会被调用一次;这可能非常昂贵,两个不同线程上的两次调用可能是不可接受的。实现这种懒惰需要完全同步,其中一个线程在另一个线程上运行懒惰方法时可能会无限期地阻塞。这也意味着如果懒惰方法存在锁定顺序问题,则可能会出现死锁。
这增加了更多成本,这些成本由那些不利用它的人(因为他们编写单线程程序)平等承担。
那么我们该如何处理呢?我们可以添加三个特性:“不是线程安全的延迟”,“带有竞争的线程安全延迟”和“带有阻塞和可能死锁的线程安全延迟”。现在,该功能变得更加昂贵,而且难以记录。这会产生巨大的用户教育问题。每当您给开发人员提供这样的选择时,都会为他们提供编写糟糕错误的机会。
第三,该功能似乎很弱。为什么只将延迟应用于属性?似乎可以通过类型系统普遍应用:
lazy int x = M(); // doesn't call M()
lazy int y = x + x; // doesn't add x + x
int z = y * y; // now M() is called once and cached.
// x + x is computed and cached
// y * y is computed
我们试图避免小而弱的功能,如果有更一般的功能是它的自然扩展。但现在我们正在谈论真正严重的设计和实现成本。
“你觉得这个有用吗?”
“就我个人而言?并不是很有用。我写了很多简单的低锁定惰性代码,主要使用Interlocked.Exchange。(我不在乎懒惰方法被运行两次,其中一个结果被丢弃;我的惰性方法从来不那么昂贵。)模式很简单,我知道它是安全的,永远不会为委托或锁分配额外的对象,如果我有更复杂的东西,我总是可以使用Lazy来为我完成工作。这将是一个小便利。”