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

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

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

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?


person Hugo    schedule 25.01.2012    source источник
comment
Вы не вызываете B(), так почему вы ожидаете увидеть это в выводе?   -  person Brian Rasmussen    schedule 25.01.2012
comment
Я хотел бы или ожидаю, что будет способ пометить B() как переопределение A(). Я не могу вызвать B() из B(int), так как ни один из конструкторов в A тогда не будет вызван.   -  person Hugo    schedule 25.01.2012
comment
Это неправда. В этом случае будет запущен конструктор по умолчанию для A.   -  person Brian Rasmussen    schedule 25.01.2012


Ответы (2)


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

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

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

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

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

Строительство происходит по принципу base-first. Конструкторы 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 защищенным.

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

person Jon Hanna    schedule 25.01.2012
comment
Одним из недостатков наличия отдельного метода для инициализации (например, вашего repeatedSetupCode) является то, что вы не можете инициализировать поля readonly оттуда. Это не имеет большого значения, но хорошо знать об этом. - person svick; 25.01.2012
comment
@svick Я бы сказал, что это сделка среднего размера, может быть :) Можно было бы использовать его, если бы это действительно было необходимо, но опять же, это необходимо только в том случае, если вы собираетесь предоставить его другому классу и вмешиваться в конструкцию другого класса за пределами отправка значений в базу или установка свойств, доступных для чтения производным, является неприятным запахом. - person Jon Hanna; 25.01.2012

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

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

person Adam Robinson    schedule 25.01.2012