2012-01-25 3 views
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?

+1

Вы не называете 'B()', так почему вы ожидаете увидеть это на выходе? –

+0

Я хотел бы или ожидать, что это будет способ, чтобы пометить 'B()' как переопределение 'A()'. Я не могу вызывать 'B()' из 'B (int)', так как тогда ни один из конструкторов в 'A' не будет вызван. – Hugo

+0

Это неправда. В этом случае будет выполняться конструктор по умолчанию для A. –

ответ

1

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

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

5

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

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

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

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

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

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

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

Или я должен иметь OnInitialize() тип метода в том, что является либо абстрактным или виртуальным, который переопределен в В и вызывается из защищенного, параметр меньше конструктора 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 «s закончились, но не остальная часть его конструкторы. Поэтому виртуальный метод запускается на 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;} 
} 

(Если мы ожидали такие звонки быть дорогими иногда, мы все еще можем memoise результат по первому зову, потому что может произойти только в полностью сконструированном User, а состояния как до, так и после memoisation являются действительными и точными состояниями для User).

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

Для возврата к ожидаемому выходу:

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 защищенной.

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

+0

Одним из недостатков использования отдельного метода является инициализация (например, ваш 'repeatSetupCode'), что вы не можете инициализировать поля' readonly'. Это неважно, но хорошо знать об этом. – svick

+1

@svick Я бы сказал, что это сделка среднего размера, может быть. Можно использовать ее, если это действительно нужно, но опять же, это необходимо, только если вы собираетесь разоблачить ее в другом классе и вмешаться в строительство другого класса за исключением отправки значений на базу или заданных свойств, читаемых производным, является неприятным запахом. –

+0

+1 за отличное резюме! –

Смежные вопросы