回答您在评论中的问题:
我现在很好奇为什么它在构造函数中有效,而在我的示例中无效。
C#对象的构造顺序如下。首先,按从最多派生类到最少派生类的顺序执行所有字段初始化程序。因此,如果您有一个类B和一个派生类D,并创建一个新的D,则D的所有字段初始化程序都会在B的任何字段初始化程序之前运行。
一旦所有字段初始化程序都运行完毕,那么构造函数将按最少派生到最多派生的顺序运行。也就是说,首先运行B的构造函数体,然后运行D的构造函数体。
这可能看起来很奇怪,但推理很简单:
首先,显然基类ctor body必须在派生类ctor body之前运行。派生ctor可能依赖于由基类ctor初始化的状态,但反之不太可能成立; 基类通常不知道派生类。
其次,显然期望在构造函数体运行之前已经初始化了字段的值。当存在字段初始化程序时,构造函数观察未初始化的字段非常奇怪。
因此,我们编写代码生成ctor的方式是每个ctor遵循以下模式:“初始化我的字段,然后调用我的基类ctor,然后执行我的ctor”。由于每个人都遵循该模式,所有字段按从派生到基础的顺序进行初始化,并且所有ctor body都从基础运行到派生。
好的,既然我们已经确定了这一点,那么当字段初始化器显式或隐式地引用"this"时会发生什么?为什么要这样做?"this"可能被用来访问一个对象上的方法、字段、属性或事件,而该对象的字段初始化器尚未全部运行,并且没有任何构造函数体运行!显然这是极其危险的。类正确操作所需的大部分状态仍然缺失。
因此,在任何字段初始化器中,我们都不允许引用"this"。
当您到达特定的构造函数体时,您知道所有的字段初始化器都已运行,并且所有基类的构造函数都已运行。您有更多的证据表明对象很可能处于良好状态,因此在构造函数体中允许访问"this"。(您仍然可以做一些愚蠢危险的事情;例如,在基类构造函数内部调用在派生类中重写的虚拟方法;派生的构造函数尚未运行,因此派生方法可能失败。请小心!)
现在您可能会合理地说,以下两种情况之间存在很大的差异:
class D : B
{
int x = this.Whatever(); // call a method on the base class, whose ctor has not run!
并且
class D : B
{
Func<int> f = this.Whatever
或类似地:
class D : B
{
Func<int> f = ()=>this.Whatever();
这并没有调用任何东西。它也没有读取可能未初始化的任何状态。显然这是完全安全的。我们可以制定一条规则,即“当访问在方法组转换或 lambda 中时,在字段初始化程序中允许此访问”,对吗?
不行。这个规则会绕过安全系统:
class D : B
{
int x = ((Func<int>)this.Whatever)();
我们又回到了同样的困境。所以我们可以制定一条规则,即“当访问在方法组转换为委托或 Lambda 时,并且流分析器可以证明在构造函数运行之前未调用委托时,在字段初始化器中允许此访问”,嘿,现在语言设计团队和编译器实现、开发、测试和用户教育团队正在花费大量时间、金钱和精力来解决我们首先不想要解决的问题。
相比于允许复杂情况的复杂规则,更好的做法是使语言具有简单易懂的规则,促进安全并能够被正确实现、轻松测试和清晰记录。简单而安全的规则是:实例字段初始化器不能有任何显式或隐式引用“this”。
进一步阅读:
http://blogs.msdn.com/b/ericlippert/archive/2008/02/15/why-do-initializers-run-in-the-opposite-order-as-constructors-part-one.aspx
http://blogs.msdn.com/b/ericlippert/archive/2008/02/18/why-do-initializers-run-in-the-opposite-order-as-constructors-part-two.aspx
Lazy<T>
本身并不完美。从GetCartItems
返回null
是一个好的设计选择吗? - JonAddress
与购物车商品清单有何关系 :) 换句话说:如何可能不知道购物车中有哪些商品(“显然”,null
表示“不知道”,因为空购物车“显然”是一个空列表)?如果不知道,你是否会对此毫不在意,而不抛出异常? - Jon