Система Orphus
Версия для печати

Закрытый конструктор базового класса

Автор: Тепляков Сергей Владимирович
Опубликовано: 18.11.2015
Исправлено: 10.12.2016
Версия текста: 1.1

Вопрос: может ли конструктор абстрактного базового класса Base быть закрытым? Возможно ли в этом случае создать класс-наследник Derived и его экземпляр?

      abstract class Base
{
    // WTF! Что с этим делать!
    private Base()
    {
        Console.WriteLine("B.ctor");
    }
}

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

Давайте перебирать варианты решения.

Рефлекшн? Не походит, нам же не просто нужно вызвать закрытый конструктор, нам нужно создать класс-наследник, который бы вызывал закрытый конструктор. Можно попробовать его сгенерировать в рантайме с помощью Reflection.Emit, но не факт, что это поможет (на самом деле, не поможет). Кто кроме текущего класса может иметь доступ к закрытым членам? Только он сам, рефлекшн, и ... вложенные классы.

Вменяемый вариант решения заключается в использовании вложенных (inner) классов.

      abstract class Base
{
    private Base()
    {
        Console.WriteLine("B.ctor");
    }
 
    class Derived : Base
    { }
 
    class AnotherDerived : Base
    { }
 
    // Фабричный метод скрывает наследников!
    public static Base Create(string args)
    {
        return (args.Length % 2 == 0) ? (Base)new Derived() : new AnotherDerived();
    }
}

Фабричный метод является очень полезным паттерном, который скрывает конкретные классы иерархии наследования, и, при ограниченном числе наследников, подобный подход вполне даже применим на практике.

Но это был вменяемый способ. Теперь переходим к невменяемому способу от @vreshetnikov.

Основан этот трюк на двух возможностях: воскрешении и ошибке в текущей реализации компилятора.

В настоящий момент компилятор языка C# не особо настаивает на вызове конструктора базового класса. Ну, он прилагает все усилия, чтобы вы таки это сделали, но остается небольшая лазейка. Вот она:

      class Derived : Base
{
    private Derived(int n)
        : this(n.ToString())
    { }
 
    private Derived(string s)
        : this(42)
    { }
} 

Класс Derived объявлен вне базового класса Base, поэтому он не имеет доступ к его закрытому конструктору. Но данный код успешно компилируется, поскольку с точки зрения компилятора все хорошо (точнее, с точки зрения текущей версии компилятора).

Ок. Первая задача, создать наследника класса Base, выполнена. Теперь нужно создать его экземпляр.

Тут нужен еще один трюк. Мы не можем обратиться к конструктору базового класса, но нужно как-то сохранить себя (this) во внешнем контексте. Есть один такой способ! Вот тут и пригодится воскрешение.

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

Теперь дело за малым: добавить финализатор, изменить вызов второго конструктора так, чтобы вместо 42 вызвался метод, гененерирующий исключение, ну и приправить все это статическим фабричным методом:

      class Derived : Base
{
    private Derived(int n)
        : this(n.ToString())
    { }
 
    private Derived(string s)
        : this(Throw())
    { }
 
    ~Derived()
    {
        _alive.Add(this);
    }
 
    public static Task<Derived> Create()
    {
        Action a = () =>
        {
            try { new Derived(42); }
            catch (CookieException) { }
        };
 
        // Избавляемся от локальной переменной!
        a();
 
        GC.Collect();
        GC.WaitForPendingFinalizers();
        GC.Collect();
 
        return Task.FromResult(_alive.Take());
    }
 
    private static int Throw()
    {
        throw new CookieException();
    }
 
    private static readonly BlockingCollection<Derived> _alive =
        new BlockingCollection<Derived>();
}

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

Наличие анонимного метода обусловлено тем, что при наличии отладчика время жизни локальных объектов продлевается до конца метода. А значит, даже при вызове конструктора в форме new Derived(42), и не сохраняя результат в локальных переменных, объект все равно будет жить до конца текущего метода. Выделение анонимного метода решает эту проблему. (Подробнее об этом можно прочитать в статейке О сборке мусора и достижимости объектов).

Теперь переходим к финализатору. Он использует BlockingCollection в качестве хранилища root-объектов: финализатор помещает объект в очередь, а метод Create ожидает его появления путем вызова метода Take.

Все, теперь код вида Derived derived = await Derived.Create() позволяет создавать невменяемый экземпляр класса наследника, базовый класс которого имеет закрытый конструктор!

P.S. Второй способ – это один большой хак, который перестанет работать в будущих версиях языка C#. Мы получаем экземпляр, который толком не сконструирован, поскольку не был вызван ни конструктор базового класса, ни инициализаторы класса наследника. По большому счету, он ничем не отличается от вызова FormatterServices.GetUnitializedObject, который применяется при десериализации объектов.


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