3

在后代类中,有没有一种方法可以调用公共的参数化构造函数以及受保护/私有的构造函数,同时仍然调用基类的构造函数?

例如,给定以下代码:

using System;

public class Test
{
  void Main()
  {
    var b = new B(1);
  }

  class A
  {
    protected A()
    {
      Console.WriteLine("A()");
    }


    public A(int val)
      : this()
    {
      Console.WriteLine("A({0})", val);
    }

  }

  class B : A
  {
    protected B()
    {
      Console.WriteLine("B()");
    }

    public B(int val)
      : base(val)
    {
      Console.WriteLine("B({0})", val);
    }

  }
}

给出的输出是:

A()
A(1)
B(1)

然而,这是我所期待的:

A()
B()
A(1)
B(1)

有没有办法通过构造函数链接来实现这一点?或者我应该有一个抽象或虚拟的OnInitialize()类型方法,A它被覆盖B并从受保护的无参数构造函数调用A

4

2 回答 2

5

你的评论有两个误解。

首先,派生类中的构造函数不会覆盖基类中的构造函数。相反,他们打电话给他们。覆盖方法意味着它被调用而不是基类等效方法,而不是那么好。

其次,总是有一个基础构造函数被调用。任何没有显式调用基本构造函数的构造函数都会隐式调用无参数的基本构造函数。

您可以通过删除A()构造函数或将其设为私有来证明这一点。B()不调用基本构造函数的构造函数现在将成为编译器错误,因为没有可用于隐式调用的无参数构造函数。

因为B是源自AB 一个A。因此B,不建造就无法成功建造A(就像建造跑车而不建造汽车)。

施工首先发生。的构造函数有责任A确保 的实例A完全构造并处于有效状态。B这是的构造函数将 的实例B置于有效状态的起点。

至于想法:

或者我应该在 A 中有一个 OnInitialize() 类型的方法,它要么是抽象的,要么是虚拟的,在 B 中被重写并从 A 的受保护的无参数构造函数中调用?

当然不。在A调用 的构造函数时,其余部分B尚未构造。但是,初始化程序已经运行。这会导致非常糟糕的情况,例如:

public abstract class A
{
  private int _theVitalValue;
  public A()
  {
    _theVitalValue = TheValueDecider();
  }
  protected abstract int TheValueDecider();
  public int TheImportantValue
  {
    get { return _theVitalValue; }
  }
}
public class B : A
{
  private readonly int _theValueMemoiser;
  public B(int val)
  {
    _theValueMemoiser = val;
  }
  protected override int TheValueDecider()
  {
    return _theValueMemoiser;
  }
}
void Main()
{
  B b = new B(93);
  Console.WriteLine(b.TheImportantValue); // Writes "0"!
}

A调用 中重写的虚拟方法时BB的初始化程序已经运行,但其构造函数的其余部分没有运行。因此,虚拟方法正在一个B不处于有效状态的设备上运行。

让这些方法发挥作用当然是可能的,但它也很容易引起很多痛苦。

反而:

定义您的类,以便它们每个都始终将其构造函数保持在完全初始化且有效的状态。正是在这一点上,您为该类设置了不变量(不变量是在从外部观察类时必须始终成立的一组条件。例如,对于内部保存月份和日期的日期类来说可以单独字段中的值暂时处于 2 月 31 日的状态,但它绝不能在构造结束时或在方法的开始或结束时处于这种状态,因为调用代码会期望它是一个日期)。

抽象类可以依赖派生类来履行其职责,但不能依赖其初始状态。如果一个设计正朝着这成为必要的方向发展,那么将对该状态的责任进一步向下移动。例如,考虑:

public abstract class User
{
  private bool _hasFullAccess;
  protected User()
  {
    _hasFullAccess = CanSeeOtherUsersItems && CanEdit;
  }
  public bool HasFullAccess
  {
    get { return _hasFullAccess; }
  }
  protected abstract bool CanSeeOtherUsersItems {get;}
  protected abstract bool CanEdit {get;}
}
public class Admin : User
{
  protected override bool CanSeeOtherUsersItems
  {
    get { return true; }
  }
  protected override bool CanEdit
  {
    get { return true; }
  }
}
public class Auditor : User
{
  protected override bool CanSeeOtherUsersItems
  {
    get { return true; }
  }
  protected override bool CanEdit
  {
    get { return false; }
  }
}

在这里,我们依赖派生类中的信息来设置基类的状态,并根据该状态来完成任务。这将在这里工作,但如果属性取决于派生类中的状态,则不会(对于创建新派生类的人来说,这是完全合理的假设是允许的)。User相反,我们通过更改为:使功能而不是状态依赖于派生类:

public abstract class User
{
  public bool HasFullAccess
  {
    get { return CanSeeOtherUsersItems && CanEdit; }
  }
  protected abstract bool CanSeeOtherUsersItems {get;}
  protected abstract bool CanEdit {get;}
}

(如果我们预计这样的调用有时会很昂贵,我们仍然可以在第一次调用时记住结果,因为这只能在完全构造的 中发生User,并且在记忆之前和之后的状态都是有效且准确的状态User。 )。

在定义了一个基类以使其构造函数处于有效状态之后,我们对派生类执行相同的操作。它可以依赖于其基类在其构造函数中的状态,因为它的基类是完全构造的,这是构造它的第一步。它在它的初始化程序中不能依赖于这个,尽管它真的没有像人们想要尝试的那样接近。

返回到预期的输出:

A()
B()
A(1)
B(1)

这意味着您想将某些东西置于有效的起始状态,然后再置于不同的有效起始状态。这没有任何意义。

现在,当然我们可以使用this()表单从构造函数中调用构造函数,但这应该被视为纯粹是为了节省打字的方便。它的意思是“这个构造函数完成了被调用者所做的一切,然后是一些”,仅此而已。对外界来说,只调用了一个构造函数。在没有这种便利性的语言中,我们要么复制代码,要么这样做:

private void repeatedSetupCode(int someVal, string someOtherVal)
{
  someMember = someVal;
  someOtherMember = someOtherVal;
}
public A(int someVal, string someOtherVal)
{
  repeatedSetupCode(someVal, someOtherVal);
}
public A(int someVal)
{
  repeatedSetupCode(someVal, null);
}
public A()
{
  repeatedSetupCode(0, null);
}

很高兴我们有this()。它并没有节省多少打字,但它确实清楚地表明,所讨论的内容仅属于对象生命周期的构造阶段。但是如果我们真的需要,我们可以使用上面的形式,甚至可以进行repeatedSetupCode保护。

不过我会非常谨慎。对象以任何方式处理彼此的构造,而不是调用根据该基类的不变量将该基类置于有效状态所必需的单个基构造函数,这是一种不好的气味,表明完全不同的方法可能会更好。

于 2012-01-25T01:13:40.240 回答
1

不,仅使用构造函数是不可能的。您实际上是在要求构造函数链“分支”,这是不可能的;每个构造函数都可以调用当前类或父类中的一个(并且只有一个)构造函数。

实现这一点的唯一方法是(如您所建议的)通过使用您在基类中调用的虚拟初始化函数。通常,从构造函数中调用虚拟成员是不受欢迎的,因为您可以使子类中的代码在其构造函数之前执行,但鉴于这正是您所追求的,这是您在这里唯一真正的方法。

于 2012-01-25T00:07:36.437 回答