Вызываем конструктор базового типа в произвольном месте
Недавно проходил собеседование, и среди прочих был вопрос о порядке вызова конструкторов в C#. После ответа собеседующий решил продемонстрировать эрудицию и заявил, что вот в Java конструктор базового типа можно вызвать в любом месте конструктора производного типа, и C#, конечно, в этом проигрывает.
Но это уже не имело значения, потому что вызов был принят.
Приведенные ниже приемы не рекомендуется использовать в реальной жизни. Точнее даже рекомендуется не использовать. Это скорее тема для легкого светского разговора с коллегой. Или собеседующим.
Подготовка
Создаем цепочку наследования. Для простоты будем использовать конструкторы без параметров. В конструкторе будем выводить информацию о типе и идентификатор объекта, на котором он вызывается.
public class A
{
public A()
{
Console.WriteLine($"Type '{nameof(A)}' .ctor called on object #{GetHashCode()}");
}
}
public class B : A
{
public B()
{
Console.WriteLine($"Type '{nameof(B)}' .ctor called on object #{GetHashCode()}");
}
}
public class C : B
{
public C()
{
Console.WriteLine($"Type '{nameof(C)}' .ctor called on object #{GetHashCode()}");
}
}
Запускаем программу:
class Program
{
static void Main()
{
new C();
}
}
И получаем вывод:
Type 'A' .ctor called on object #58225482
Type 'B' .ctor called on object #58225482
Type 'C' .ctor called on object #58225482
public A() : this() { } // CS0516 Constructor 'A.A()' cannot call itself
и таким фокусом компилятор тоже не провести:
public A() : this(new object()) { }
public A(object _) : this(0) { }
public A(int _) : this() { } // CS0768 Constructor 'A.A(int)' cannot call itself through another constructor
Удаление дублирующегося кода
Добавляем вспомогательный класс:
internal static class Extensions
{
public static void Trace(this object obj) =>
Console.WriteLine($"Type '{obj.GetType().Name}' .ctor called on object #{obj.GetHashCode()}");
}
И заменяем во всех конструкторах
Console.WriteLine($"Type '{nameof(...)}' .ctor called on object #{GetHashCode()}");
на
this.Trace();
Однако теперь программа выводит:
Type 'C' .ctor called on object #58225482
Type 'C' .ctor called on object #58225482
Type 'C' .ctor called on object #58225482
В нашем случае можно использовать следующую хитрость. Кто знает о типах времени компиляции? Компилятор. А еще он выбирает перегрузки методов на основе этих типов. И для обобщенных типов и методов генерирует сконструированные сущности тоже он. Поэтому возвращаем правильный вывод типов, переписав метод Trace следующим образом:
public static void Trace(this T obj) =>
Console.WriteLine($"Type '{typeof(T).Name}' .ctor called on object #{obj.GetHashCode()}");
Получение доступа к конструктору базового типа
Здесь на помощь приходит рефлексия. Добавляем в Extensions метод:
public static Action GetBaseConstructor(this T obj) =>
() => typeof(T)
.BaseType
.GetConstructor(Type.EmptyTypes)
.Invoke(obj, Array.Empty
В типы B и C добавляем свойство:
private Action @base => this.GetBaseConstructor();
Вызов конструктора базового типа в произвольном месте
Меняем содержимое конструкторов B и C на:
this.Trace();
@base();
Теперь вывод выглядит так:
Type 'A' .ctor called on object #58225482
Type 'B' .ctor called on object #58225482
Type 'A' .ctor called on object #58225482
Type 'C' .ctor called on object #58225482
Type 'A' .ctor called on object #58225482
Type 'B' .ctor called on object #58225482
Type 'A' .ctor called on object #58225482
Изменение порядка вызова конструкторов базового типа
Внутри типа A создаем вспомогательный тип:
protected class CtorHelper
{
private CtorHelper() { }
}
Так как здесь важна только семантика, конструктор типа целесообразно сделать закрытым. Создание экземпляров не имеет смысла. Тип предназначен исключительно для различения перегрузок конструкторов типа A и производных от него. По этой же причине тип следует разместить внутри A и сделать защищенным.
Добавляем в A, B и C соответствующие конструкторы:
protected A(CtorHelper _) { }
protected B(CtorHelper _) { }
protected C(CtorHelper _) { }
Для типов B и C ко всем конструкторам добавляем вызов:
: base(null)
internal static class Extensions
{
public static Action GetBaseConstructor(this T obj) =>
() => typeof(T)
.BaseType
.GetConstructor(Type.EmptyTypes)
.Invoke(obj, Array.Empty
И вывод становится:
Type 'C' .ctor called on object #58225482
Type 'B' .ctor called on object #58225482
Type 'A' .ctor called on object #58225482
Осмысление результата
Добавив в Extensions метод:
public static void TraceSurrogate(this T obj) =>
Console.WriteLine($"Type '{typeof(T).Name}' surrogate .ctor called on object #{obj.GetHashCode()}");
и вызвав его во всех конструкторах, принимающих CtorHelper, мы получим вывод:
Type 'A' surrogate .ctor called on object #58225482
Type 'B' surrogate .ctor called on object #58225482
Type 'C' .ctor called on object #58225482
Type 'A' surrogate .ctor called on object #58225482
Type 'B' .ctor called on object #58225482
Type 'A' .ctor called on object #58225482
Порядок следования конструкторов по принципу базовый/производный, конечно, не изменился. Но все же порядок следования доступных клиентскому коду конструкторов, несущих смысловую нагрузку, удалось поменять благодаря перенаправлению через вызовы недоступных клиенту ничего не делающих вспомогательных конструкторов.