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