public class B
{
public B() { Console.WriteLine("B constructor"); }
}
public class C : B
{
public static int Get1() { Console.WriteLine("Getting 1"); return 1; }
int X = Get1();
public C()
{
Console.WriteLine("C Constructor");
}
}
C从IL代码来看的构造函数如下:
X = Get1();
B::ctor();
Console.WriteLine("C Constructor");
和输出,分别
Getting 1
B constructor
C Constructor
如果将初始化X放在 C 构造函数中,或者放在另一个辅助类构造函数C中,则只会在类构造函数结束后执行B。也就是说,代码的含义会有所不同。
更糟糕的是,这样的转变并不总是可能的!例如,考虑类System.Exception。
[Serializable]
public class CustomException : Exception
{
readonly int ErrorCode;
public CustomException(string message) : base(message) { }
protected CustomException(SerializationInfo info, StreamingContext context) :
base(info, context) { }
}
我不排除 C# 的作者甚至可以在实践中测试此解决方案并得出没有区别且不值得的结论。好吧,即使现在有这样的证据,也为时已晚,任何人都不太可能改变任何东西,因为它是向后兼容的。是的,编译器构建者并不急于破坏基本代码生成的东西,因为世界上有许多检查 IL 的工具,并且会非常惊讶地看到那里调用 left 方法。
首先,问题的技术方面。
是的,它是(对于非委托构造函数,正如@PetSerAl 建议的那样,即没有指定
this(...)而不是的构造函数base(...))。现代版本的 C# 编译这个类进入这个 IL:
我们看到一系列命令
初始化
X两个构造函数中的字段。为什么我们不把它移到一个单独的私有构造函数中,然后从每个公共构造函数中自己调用它呢?(私有方法不起作用,因为它无法初始化
readonly-fields。)这在技术上是可行的,但不是一回事。不同之处在于我们有一个带有非平凡构造函数的基类。关键是派生类初始值设定项在基构造函数执行之前执行。但是派生类构造函数本身是在基构造函数执行之后执行的。
考虑这段代码:
C从IL代码来看的构造函数如下:和输出,分别
如果将初始化
X放在 C 构造函数中,或者放在另一个辅助类构造函数C中,则只会在类构造函数结束后执行B。也就是说,代码的含义会有所不同。更糟糕的是,这样的转变并不总是可能的!例如,考虑类
System.Exception。不可能将公共部分推入“通用”构造函数,因为通用构造函数将无法调用正确的基本构造函数。
一个漏洞是声明构造函数,以便除其中一个之外的所有构造函数都调用同一类的其他构造函数,而将字段初始化留在原处。例如,如果您添加一个构造函数
调用它时,我们得到
在这种情况下,字段初始化仅存在于最后一个构造函数的代码中。然而,这个技巧也有同样的缺点:并不总是可以从“通用”构造函数中调用所需的基本构造函数!
从技术层面来看,我们似乎已经弄清楚了。现在进行实际应用。
我个人不会打扰,并且写的不是“如何更经济”,而是更清晰。将三个或四个初始化器组合成一个通用方法的好处是一分钱,而且代码变得更加复杂,而且你必须重写它而不需要读者理解。此外,您可以假设编译器本身对您的代码应用了一种称为方法内联的优化:)
对字段进行内联初始化的另一个论点是,在调用父类型构造函数之前发生内联初始化这一事实减少了访问未初始化对象的机会。示例(从附近的问题借来):
Eric Lippert 描述了为什么初始化程序运行到基本构造函数的调用:为什么初始化程序作为构造函数以相反的顺序运行? 第一部分,第二部分。
我非常尊重里希特的深度和技术性,但他的书中有一点让我非常担心。这是他对风格或设计方面什么是好什么是坏的建议。这是他们的提示之一。
有许多技巧,尤其是在效率问题上,很难正确回答,因为“正确性”高度依赖于您的应用程序。例如,一个从事过负载相当高的应用程序的人可能会给出“永远不要使用 LINQ”的建议。当涉及到高负载应用程序的关键区域时,该建议非常合理,但在一般情况下非常糟糕,因为它仅适用于可怜的一半应用程序。
“单独的构造函数并且不要使用类似字段的初始化程序”的建议更有趣,因为它适用于更少的用例。
在过去的几年里,我一直在研究一个高负载的应用程序,在使用 LINQ 时我真的必须小心,但我从来没有想过将代码移动到一个通用的构造函数中。
这个建议在今天与10 年前一样无关紧要。更准确地说,它与地球上十几个参与 .net 框架、corefx 的人有关,也许还有为统一而开发可重用组件的开发人员。
建议的想法非常有效:在每个构造函数中复制 IL 代码会导致程序集的大小增加(这将增加其加载时间)并增加 JIT 编译的持续时间。
理论上 - 问题存在。在实践中,在决定是否使用类字段初始化时,应该从代码的可读性开始,而不是从生成的 IL 代码的大小开始。
我不排除 C# 的作者甚至可以在实践中测试此解决方案并得出没有区别且不值得的结论。好吧,即使现在有这样的证据,也为时已晚,任何人都不太可能改变任何东西,因为它是向后兼容的。是的,编译器构建者并不急于破坏基本代码生成的东西,因为世界上有许多检查 IL 的工具,并且会非常惊讶地看到那里调用 left 方法。
(*) 我不同意 uv。@VladD 关于私有方法在这里不起作用的事实。我不确定 CLR 是否真的强制执行只读字段只能在构造函数中初始化的规则,因此编译器生成的私有方法可以很好地初始化这些字段。