【问题标题】:Why are field initializers of a derived class are executed before base classes's initializers为什么派生类的字段初始值设定项在基类的初始值设定项之前执行
【发布时间】:2011-08-18 11:09:44
【问题描述】:

构造函数按从上到下的顺序执行基础的第一个,然后是派生的。这种安排基于一个重要的 OOP 保证,即必须始终初始化对象(此处为基类)才能使用(此处为派生类的构造函数)。

我想知道为什么字段初始化器在 C# 中不遵循这个原则?我在这里遗漏了什么吗?

我也发现了这个原则对字段初始化器的用处。我有一个带有返回 Identity 对象的属性的基类。每个派生类都有自己的存储库字段,我一直在使用字段初始化器(使用默认构造函数)对其进行初始化。最近我决定存储库类也必须提供 Identity 对象,所以我在存储库构造函数中引入了一个额外的参数。但我坚持要找出答案:

public class ForumController : AppControllerBase
{
        ForumRepository repository = new ForumRepository(Identity);
    // Above won't compile since Identity is in the base class.

   // ... Action methods.
}

现在我只剩下一个选项,即使用默认构造函数填充我的每个控制器,仅用于使用 Identity 初始化存储库对象。

【问题讨论】:

  • 不,它不编译的原因根本不是Identity在基类中,只是因为它是一个实例成员。因此,标题中的问题与您尝试做的事情无关......

标签: c# oop inheritance


【解决方案1】:

在 C# 中,构造函数运行序列:

执行所有字段初始化程序 链接到基类构造函数 为构造函数执行用户提供的代码

在vb.net中,序列为:

链接到基类构造函数 执行所有字段初始化程序 为构造函数执行用户提供的代码

没有基于框架的原因为什么 C# 以它的顺序执行操作,这一点可以从 vb.net 可以并且确实以不同的顺序执行它们的事实证明。 C# 方法的设计理由是,在所有字段初始值设定项(用于派生字段和基类字段)运行之前,对象不可能暴露给外部世界;由于基类构造函数可以将对象暴露给外部世界,因此强制执行该要求意味着字段初始化程序必须在基类构造函数之前运行。

就我个人而言,我认为这个理由并不特别有说服力。在许多情况下,如果没有在基本构造函数运行之前不可用的信息,就不可能将字段设置为有用的值。任何其基本构造函数可能暴露部分构造实例的代码都需要为这种可能性做好准备。虽然有时指定字段初始化程序应该“提前”运行会很有用,但我认为在更多情况下,它们能够访问羽翼未丰的对象是有帮助的(在某种程度上,因为我相信在类实例的生命周期内其值应被视为不变量的类字段应在实际情况下通过初始化程序以声明方式设置,而不是在构造函数中强制设置)。

顺便说一句,我希望在 vb.net 和 C# 中看到的一个特性是声明参数初始化字段和伪字段的一种方法。如果一个类具有特定名称和类型的参数初始化字段,则该类的每个不链接到同一类的另一个构造函数的构造函数都必须包含具有适当名称和类型的参数。这些字段的值将在其他任何事情完成之前在构造函数中设置,并且可以被其他字段初始化器访问。伪字段的行为在语法上类似于字段,除了它们在字段初始化器中可用,并且将在构造函数中作为局部变量实现。这样的特征将使许多类型的结构更方便。例如,如果一个类型应该在其整个生命周期中保存一个特定的数组实例,则可以说:

只读参数 int 长度; 只读 ThingType[] myArray = new ThingType[Length];

似乎比必须在类构造函数中构造数组(这在基本构造函数运行之后才会发生)或(对于 vb.net)必须将长度传递给基类构造函数更好然后可以使用它来设置一个字段(然后它会占用类中的空间,即使它的值——比如上面的Length——可能是多余的)。

【讨论】:

    【解决方案2】:

    字段初始化器在构造函数中执行,由于基类中的构造函数首先被调用,所以所有字段初始化器也会在派生构造函数执行之前执行。

    例子:

    public class Base {
    
      // field initialiser:
      private string _firstName = "Arthur";
    
      public string FirstName { get { return _firstName;}}
      public string LastName { get; private set; }
    
      // initialiser in base constructor:    
      public Base() {
        LastName = "Dent";
      }
    
    }
    
    public class Derived : Base {
    
      public string FirstNameCopy { get; private set; }
      public string LastNameCopy { get; private set; }
    
      public Derived() {
        // get values from base class:
        FirstNameCopy = FirstName;
        LastNameCopy = LastName;
      }
    
    }
    

    测试:

    Derived x = new Derived();
    Console.WriteLine(x.FirstNameCopy);
    Console.WriteLine(x.LastNameCopy);
    

    输出:

    Arthur
    Dent
    

    【讨论】:

    • 我想你没有明白我的意思。我已经知道你在说什么了。我的问题是,基类和派生类中的给定字段初始化器,为什么先执行派生类的字段初始化器,然后再执行基类?如果您看到构造函数,则完全相反。
    • @Varun K:我明白了,您正试图从字段初始化程序访问字段。你不能这样做,因为你不能使用初始化器中的实例成员,这与不同类中的字段无关。
    • 我知道我做不到 :-)。但我想知道背后的原因。
    • @Varun K:这只是因为初始化程序在基本构造函数之前运行,所以对象还没有准备好使用。
    【解决方案3】:

    字段初始值设定项并不是构造函数的替代品。

    根据 MSDN 文档,所有字段初始值设定项都在构造函数之前执行。然而,字段初始化器的一个限制是它们不能引用其他实例字段。 (http://msdn.microsoft.com/en-us/library/ms173118(v=vs.80).aspx)

    限制是由于无法在编译器级别识别正确的顺序和依赖项来执行字段初始值设定项。

    您必须编写一个默认构造函数来实现您想要的。但是,您可以尝试使用静态字段;这是一种不好的做法,甚至可能不适合您的设计。


    编辑以澄清评论中提出的问题:

    就派生类而言,无论是通过初始化器还是构造器初始化,基类字段看起来都是一样的。该信息不能提供给子类,因为这意味着暴露基类的实现(它对基类是严格私有的)。

    这意味着派生类 Initializer 在访问它们之前无法确定是否所有基类字段都已初始化。因此,该行为是不允许的。

    【讨论】:

    • 我已经知道这个限制点(正确的顺序和依赖),但是如果你看到了,这严格适用于同一类中的字段。鉴于基类中的字段初始化器与派生类中的字段初始化器,我在质疑
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2020-09-21
    • 2012-02-07
    • 1970-01-01
    • 1970-01-01
    • 2011-05-22
    相关资源
    最近更新 更多