Программно препятствовать инициализации конструктора?

У меня есть класс Foo и статический класс FooFactory, который используется для создания экземпляров производных классов Foo и Foo с помощью следующего API:

public static class FooFactory {
  public static T Create<T>() where T : Foo, new() { 
    ...
    return new T();
  }
}

Специализация new() параметра типа T требует, чтобы Foo имел конструктор по умолчанию public. Другими словами, ничто не мешает пользователю Foo инициализировать класс напрямую через конструктор.

Однако FooFactory предназначен для отслеживания всех экземпляров Foo, поэтому я хочу заставить пользователя создавать все производные экземпляры Foo и Foo через файл FooFactory.

Самое близкое, что мне удалось предотвратить прямую инициализацию конструктора, — это украсить конструктор по умолчанию Foo атрибутом [Obsolete("message", error: true)]:

public class Foo {      
  [Obsolete("Fail", true)]
  public Foo() { ... }
}

С этим украшением код не компилируется, когда я напрямую вызываю конструктор по умолчанию Foo, тогда как инициализация через FooFactory.Create<Foo>() работает.

Но с этим украшением [Obsolete] у меня все еще есть проблемы с производными классами. Следующее даже не скомпилируется из-за настройки error: true для конструктора по умолчанию Foo:

public class Bar : Foo { ... }

public class Baz : Foo { public Baz() : base() { ... } }

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

Если я установлю error в ObsoleteAttribute на false, вместо этого я получу предупреждения о компиляции, но я хотел бы иметь более сильное разочарование, чем предупреждения...

Можно ли программно предотвратить прямой вызов конструктора по умолчанию как для Foo, так и для производных классов, когда конструкторы по умолчанию должны быть объявлены public?


person Anders Gustafsson    schedule 04.09.2017    source источник
comment
Сделайте конструктор internal библиотекой и удалите ограничение new(). Теперь это означало, что вы несете ответственность за создание всех типов Foo.   -  person Nkosi    schedule 04.09.2017
comment
Если я не ошибаюсь, требование new() работает только в том случае, если конструктор объявлен public.   -  person Anders Gustafsson    schedule 04.09.2017
comment
Вам придется снять ограничение   -  person Nkosi    schedule 04.09.2017
comment
Это не вариант...   -  person Anders Gustafsson    schedule 04.09.2017
comment
ну вот как ограничение работает. Вы не можете получить то, что вы просите.   -  person Nkosi    schedule 04.09.2017
comment
Когда вы в яме, перестаньте копать. Заставьте конструктор Foo выполнять отслеживание экземпляров, вызывая статическое поле и this.GetType().   -  person Jeroen Mostert    schedule 04.09.2017
comment
Если у вас есть полный контроль над Foo и производными типами, вы можете сделать их внутренними и открыть интерфейсы, которые они реализуют. Ваш метод Create может принимать интерфейс в качестве параметра и посредством отражения определять, какой класс создавать. Однако: при этом вы теряете проверку времени компиляции типа параметра для Create.   -  person Johan Donne    schedule 04.09.2017
comment
Хороший вопрос, Йерун. Сначала я сделал это, но удалил его при рефакторинге класса. При повторном изучении текущего исходного кода я вижу, что может иметь смысл повторно ввести регистрацию в конструкторе. Спасибо!   -  person Anders Gustafsson    schedule 04.09.2017
comment
Отлично, потому что, как уже говорилось, ваши требования буквально невозможно удовлетворить иначе. В частности, невозможно запретить объявлять Baz, который может быть сконструирован напрямую — автор может всегда предоставить публичный конструктор для пользователей, который обходит любые ваши требования, независимо от того, как был настроен Foo. Если вы предполагаете, что авторы всегда находятся в сговоре и могут предположить, что они будут себя вести, моим вторым предложением было бы отказаться от ограничения new() и передать вызов конструктора как Func<T> внутреннему методу. Это обеспечивает безопасность типов, но обременительно для авторов.   -  person Jeroen Mostert    schedule 04.09.2017
comment
@JohanDonne Спасибо за ваше предложение, но на самом деле это не соответствует тому, что я ищу. Одна из причин сохранения специализации new() заключается в том, что она переносима на разные платформы. Отражение, в частности против методов, отличных от public, не всегда хорошо работает на всех платформах.   -  person Anders Gustafsson    schedule 04.09.2017
comment
Требуется ли, чтобы третьи лица могли создавать подклассы Foo? Если это не требование, то решить вашу проблему намного проще.   -  person Eric Lippert    schedule 04.09.2017
comment
Спасибо, что изучил это, Эрик. Да, мое намерение состоит в том, чтобы третьи стороны могли создавать подклассы Foo.   -  person Anders Gustafsson    schedule 04.09.2017


Ответы (1)


Вы не можете наследовать от класса только с частным конструктором, но вы можете сделать свой пустой конструктор частным и добавить конструктор со свойством, например createdInsideFooFactory. Вы также должны удалить ограничение new() на своей фабрике. Это хак, но он сообщит разработчику, что экземпляр должен быть создан внутри фабрики foo.

public class Foo
{
    private Foo()
    {
    }

    protected Foo(bool createdInsideFooFactory)
    {
        if (createdInsideFooFactory) return;

        throw new MyException();
    }
}
person Alex Zaitsev    schedule 04.09.2017
comment
Ваше утверждение о том, что вы не можете наследовать от класса с закрытым конструктором, неверно. Можете ли вы понять, как написать программу, имеющую класс с частным конструктором и производный класс? Есть как минимум два способа сделать это. - person Eric Lippert; 04.09.2017
comment
@EricLippert: я не могу, пожалуйста, просветите нас. Я могу придумать очевидные способы: 1) предоставить дополнительный конструктор, который не является закрытым (но в ответе указан частный конструктор только, так что это обман, и 2) сделать производный класс вложенным классом (но это требует контроля над определением Foo, что также является мошенничеством). - person Jeroen Mostert; 05.09.2017
comment
Да только приватный ctor. Вложенный класс — это самый простой способ. Есть и трудный путь. - person Eric Lippert; 05.09.2017
comment
Подсказка: должен ли производный класс должен вызывать ctor базового класса? - person Eric Lippert; 05.09.2017
comment
@EricLippert: если мы руководствуемся только спецификацией языка C #, то да. Он либо вызывает конструктор базового класса (явно или неявно), либо другой конструктор того же класса, а циклы (прямо или косвенно) запрещены, так что в конечном итоге должен быть вызван bcc. Конечно, во время выполнения вы можете не вызывать его через исключение, но это не обойдет объявление. Даже если бы существовал какой-то отвратительный способ сделать это, я уверен, что вы обязательно получили бы класс без экземпляров. Теперь уже проболтайтесь, некоторые из нас не написали компилятор. :-П - person Jeroen Mostert; 05.09.2017
comment
@AlexZaitsev, по нескольким причинам я не могу удалить ограничение new(). То, что вы предложили, позволит определить конструктор по умолчанию подкласса, это правильно, но я не вижу способа контролировать, вызывается ли этот конструктор по умолчанию напрямую или через фабрику? То есть у вас может быть public Bar() : Foo(true) или public Bar() : Foo(false), но я не вижу возможности проконтролировать, чтобы этот конструктор вызывался ни из фабрики, ни напрямую. - person Anders Gustafsson; 05.09.2017
comment
@JeroenMostert: Ах, вторая техника была удалена. Раньше было допустимо иметь цикл в вызовах этого конструктора. Это позволяет вам обойти вызов базового ctor, который позволяет компилировать программу. Затем вы вызываете исключение при построении, перехватываете исключение и воскрешаете объект с помощью финализатора, и у вас есть экземпляр производного класса, который никогда не вызывал ctor базового класса. - person Eric Lippert; 05.09.2017
comment
@EricLippert: господи, конечно, теперь я чувствую себя глупо, потому что не подумал об этом. :-P Тот факт, что вы можете воскресить частично построенные объекты в финализаторах, сам по себе достаточно пугает. Я могу подтвердить, что это все еще работает достаточно хорошо. - person Jeroen Mostert; 05.09.2017