Порядок конструктора в подклассах

В классе-потомке есть ли способ вызвать как открытый, параметризованный конструктор, так и защищенный / закрытый конструктор, при этом все еще вызывая конструктор базового класса?

Например, учитывая следующий код:

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?

2 ответа

В вашем комментарии есть два заблуждения.

Во-первых, конструкторы в производных классах не переопределяют конструкторы в базовых классах. Скорее они называют их. Переопределение метода означает, что он вызывается вместо метода, эквивалентного базовым классам, а не так.

Во-вторых, всегда есть базовый конструктор. Любой конструктор без явного вызова базового конструктора неявно вызывает базовый конструктор без параметров.

Вы можете продемонстрировать это, удалив A() конструктор, или сделать его частным. B() конструктор, который не вызывает базовый конструктор, теперь будет ошибкой компилятора, поскольку для неявного вызова нет доступного конструктора без параметров.

Так как B происходит от A, B это A, Следовательно B не может быть успешно построен без A быть построенным (например, построить спортивную машину без сборки машины).

Строительство происходит на первом месте. Это ответственность A конструкторы, чтобы убедиться, что экземпляр A полностью построен и в действительном состоянии. Это тогда отправная точка для B конструктор, чтобы поставить экземпляр B в действительное состояние.

Что касается идеи:

Или я должен иметь метод типа OnInitialize() в A, который является либо абстрактным, либо виртуальным, который переопределяется в 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 вызывает виртуальный метод, переопределенный в B, B Инициализаторы запускались, но не остальные его конструктора. Поэтому виртуальный метод запускается на B это не в действительном состоянии.

Конечно, такие подходы можно заставить работать, но их также очень легко причинить много боли.

Вместо:

Определите ваши классы так, чтобы каждый из них всегда оставлял свои конструкторы в полностью инициализированном и допустимом состоянии. Именно в этот момент вы устанавливаете инвариант для этого класса (инвариант - это набор условий, которые должны выполняться в любое время, когда класс наблюдается снаружи. Например, это нормально для класса дат, который внутренне удерживается месяц и день. значения в отдельных полях временно находятся в состоянии 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 защищенный.

Я был бы очень осторожен, хотя. Объекты, имеющие дело с конструкцией друг друга любым способом, кроме вызова единственного базового конструктора, необходимого для перевода этого базового класса в допустимое состояние в соответствии с инвариантом этого базового класса, - это неприятный запах, который предполагает, что другой подход может быть лучше.

Нет, то, что вы ищете, невозможно, используя только конструкторы. По сути, вы просите цепочку конструктора "переходить", что невозможно; каждый конструктор может вызывать один (и только один) конструктор либо в текущем классе, либо в родительском классе.

Единственный способ сделать это - (как вы предлагаете) - использовать виртуальную функцию инициализации, которую вы вызываете в базовом классе. Обычно вызов виртуальных членов из конструктора не одобряется, поскольку вы можете заставить код дочернего класса выполняться перед его конструктором, но, учитывая, что это именно то, что вам нужно, это ваш единственный реальный подход.

Другие вопросы по тегам