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

Подготовка
Создаем цепочку наследования. Для простоты будем использовать конструкторы без параметров. В конструкторе будем выводить информацию о типе и идентификатор объекта, на котором он вызывается.
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<T>(this T obj) =>
Console.WriteLine($"Type '{typeof(T).Name}' .ctor called on object #{obj.GetHashCode()}");
Получение доступа к конструктору базового типа
Здесь на помощь приходит рефлексия. Добавляем в Extensions метод:
public static Action GetBaseConstructor<T>(this T obj) =>
() => typeof(T)
.BaseType
.GetConstructor(Type.EmptyTypes)
.Invoke(obj, Array.Empty<object>());
В типы 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<T>(this T obj) =>
() => typeof(T)
.BaseType
.GetConstructor(Type.EmptyTypes)
.Invoke(obj, Array.Empty<object>());
public static void Trace<T>(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

Осмысление результата
Добавив в Extensions метод:
public static void TraceSurrogate<T>(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
Порядок следования конструкторов по принципу базовый/производный, конечно, не изменился. Но все же порядок следования доступных клиентскому коду конструкторов, несущих смысловую нагрузку, удалось поменять благодаря перенаправлению через вызовы недоступных клиенту ничего не делающих вспомогательных конструкторов.
kekekeks
Вообще такие вещи обычно делают через самописный Fody-weaver на пару десятков строк, меняющий место вызова базового конструктора в сгенерированном MSIL.
nightwolf_du
А можно пример реальной ситуации, когда это нужно?
Мы с коллегой недавно с таким же энтузиазмом, как в этой статье обсуждали интернирование строк, и так и не придумали, где с пользой использовать эти знания.
Saladin
Сдаётся мне, что такое может понадобится в случае наследования от third-party класса. Например, если нужно внедрить свой hook до инициализации его состояния
kekekeks
Бывает нужно как минимум при необходимости преобразовать аргументы конструктора, передать дальше И сохранить у себя. Пока такой переданный дальше аргумент один и не зависим от остального — можно нагородить статический метод с приватным конструктором. Когда начинаются кросс-зависимости — становится грустно.
Второй сценарий — когда кто-то догадался в базовом классе из конструктора дергать виртуальный метод, а нам теперь позарез надо поведение этого метода менять в зависимости от того, что нам прилетело в конструктор.