我很好奇这样的功能应该经过什么样的考虑。
首先,我写了一篇关于这个主题的博客。请参阅我的旧博客:
http://blogs.msdn.com/b/ericlippert/ http://blogs.msdn.com/b/ericlippert/
和我的新博客:
http://ericlippert.com http://ericlippert.com
关于语言设计各个方面的许多文章。
其次,C# 设计过程现已向公众开放,因此您可以亲自了解语言设计团队在审查新功能建议时的考虑因素。看https://github.com/dotnet/roslyn/ https://github.com/dotnet/roslyn/了解详情。
将此类关键字添加到库中会涉及哪些成本?
这取决于很多事情。当然,不存在廉价、简单的功能。只有更便宜、更简单的功能。一般来说,成本是涉及设计、指定、实施、测试、记录和维护功能的成本。还有更多奇特的成本,例如不做更好的功能的机会成本,或者选择与我们可能想要添加的未来功能交互不良的功能的成本。
在这种情况下,该功能可能只是简单地使“lazy”关键字成为使用的语法糖Lazy<T>
。这是一个非常简单的功能,不需要大量花哨的语法或语义分析。
在什么情况下这会出现问题?
我可以想到很多因素会导致我拒绝该功能。
首先,没有必要;它只是一种方便的糖。它并没有真正为该语言增添新的力量。所带来的好处似乎不值得所付出的代价。
其次,也是更重要的一点,它体现了特别的对语言的一种懒惰。懒惰有不止一种,我们可能会选择错误。
怎么会有不止一种懒惰呢?好吧,想想如何实施。属性已经是“惰性的”,因为它们的值在调用该属性之前不会计算,但您想要的不止于此;您想要一个被调用一次的属性,然后该值将被缓存以供下次调用。 “惰性”本质上是指记忆属性。我们需要做出哪些保证?有很多种可能性:
可能性#1:根本不是线程安全的。如果您在两个不同的线程上“第一次”调用该属性,则任何事情都可能发生。如果你想避免竞争条件,你必须自己添加同步。
可能性#2:线程安全,这样两个不同线程上对属性的两次调用都调用初始化函数,然后竞相查看谁填充了缓存中的实际值。据推测,该函数将在两个线程上返回相同的值,因此这里的额外成本仅在于浪费的额外调用。但缓存是线程安全的,不会阻塞任何线程。 (因为线程安全缓存可以用低锁或无锁代码编写。)
实现线程安全的代码是有代价的,即使它是低锁代码。这个成本可以接受吗?大多数人编写的都是有效的单线程程序;无论是否需要,将线程安全的开销添加到每个惰性属性调用中似乎是正确的吗?
情况#3:线程安全,有力保证初始化函数只会被调用一次;缓存上没有竞争。用户可能隐含地期望初始化函数只被调用一次;它可能非常昂贵,并且两个不同线程上的两次调用可能是不可接受的。实现这种惰性需要完全同步,其中一个线程可能会无限期地阻塞,而惰性方法正在另一个线程上运行。这也意味着如果惰性方法存在锁顺序问题,则可能会出现死锁。
这甚至增加了该功能的成本,而使用该功能的人同样承担这一成本not利用它(因为他们正在编写单线程程序)。
那么我们该如何处理这个问题呢?我们可以添加三个功能:“惰性非线程安全”、“具有竞争的惰性线程安全”和“具有阻塞和可能死锁的惰性线程安全”。现在这个功能变得更加昂贵并且way更难记录。这会产生一个enormous用户教育问题。每次你给开发人员这样的选择时,你就为他们提供了编写可怕错误的机会。
第三,正如所述,该功能似乎很弱。为什么惰性应该仅仅应用于属性?看起来这可以通过类型系统普遍应用:
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<T>
为我做工作。这将是一个小小的便利。