Вызываем конструктор базового типа в произвольном месте

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

Утверждение оказалось ложью, враньем и провокацией
image


Но это уже не имело значения, потому что вызов был принят.

image

Дисклеймер

Приведенные ниже приемы не рекомендуется использовать в реальной жизни. Точнее даже рекомендуется не использовать. Это скорее тема для легкого светского разговора с коллегой. Или собеседующим.


Подготовка


Создаем цепочку наследования. Для простоты будем использовать конструкторы без параметров. В конструкторе будем выводить информацию о типе и идентификатор объекта, на котором он вызывается.

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());

    public static void Trace(this T obj) =>
        Console.WriteLine($"Type '{typeof(T).Name}' .ctor called on object #{obj.GetHashCode()}");
}

public class A
{
    protected A(CtorHelper _) { }

    public A()
    {
        this.Trace();
    }

    protected class CtorHelper
    {
        private CtorHelper() { }
    }
}

public class B : A
{
    private Action @base => this.GetBaseConstructor();

    protected B(CtorHelper _) : base(null) { }

    public B() : base(null)
    {
        this.Trace();
        @base();
    }
}

public class C : B
{
    private Action @base => this.GetBaseConstructor();

    protected C(CtorHelper _) : base(null) { }

    public C() : base(null)
    {
        this.Trace();
        @base();
    }
}

 


И вывод становится:

Type 'C' .ctor called on object #58225482
Type 'B' .ctor called on object #58225482
Type 'A' .ctor called on object #58225482

Наивный простачок думает, что обманул компилятор
image


Осмысление результата


Добавив в 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

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

© Habrahabr.ru