Как реализовать закрытый, публичный вложенный класс, который может быть создан только включающим его классом?
Цель
Моя цель - реализовать закрытый открытый вложенный класс, который может быть создан только включающим его классом - без использования отражения.
Это означает, что вложенный класс не может иметь никаких открытых или внутренних конструкторов или любых открытых или внутренних статических методов фабрики.
Предыдущая работа
Этот пост пару лет назад кажется ответом. (Вся эта тема содержит много информации о том, чего я пытаюсь достичь.)
Как это работает, довольно просто: он использует тот факт, что вложенный класс может обращаться к статическим полям своего включающего класса вместе со статическим конструктором для вложенного класса.
Вмещающий класс объявляет статический Func<NestedClassType, NestedClassCtorArgType>
делегат, который возвращает экземпляр вложенного класса, так что включающий класс может использовать этот делегат в качестве метода фабрики.
У самого вложенного класса есть статический конструктор, который инициализирует статический делегат статического фабрики включающего класса делегату, который создаст экземпляр вложенного класса.
Эта проблема
К сожалению, я не могу заставить его работать, как написано в этом ответе. Причина в том, что статический конструктор для вложенного класса не вызывается до того, как включающий класс использует фабричный метод, и, следовательно, существует исключение с нулевой ссылкой. (Если вы посмотрите на пример программы в конце этого вопроса, вы поймете, что я имею в виду.)
Мой обходной путь
Я работал над этой проблемой следующим образом:
- Добавил во вложенный класс внутреннюю статику
Initialise()
метод, который ничего не делает. - Добавлен в включающий класс статический конструктор, который вызывает вложенный класс
Initialise()
метод.
Это работает нормально, но оставляет немного карбункула в форме internal static void Initialise()
метод.
Мои вопросы
Есть ли способ избежать этого способа сделать это? Я не могу помочь, но думаю, что что-то упустил из оригинального поста, который я связал выше. Я неправильно понял ответ?
Есть ли умный способ принудительно запустить статический конструктор для вложенного класса, прежде чем я вызову код, который пытается создать экземпляр вложенного класса?
И есть ли другие проблемы с этим подходом?
(Я знаю, что могу просто написать открытый интерфейс для вложенного класса и вернуть его вместо этого. Этот вопрос не о том, чтобы решить его таким образом!)
Пример кода
Вот мой пример кода. Попробуйте запустить его, и он напечатает "Тест". Затем попробуйте закомментировать строку, отмеченную <--- If you comment this out, things won't work
и запустите его снова.
using System;
namespace ConsoleApplication1
{
class Program
{
static void Main()
{
Outer outer = new Outer();
Outer.Inner item = outer.Item("Test");
Console.WriteLine(item.Text);
}
}
public sealed class Outer
{
public Inner Item(string text)
{
return _nestedFactory(text);
}
// This static constructor calls the nested class's Initialise() method, which causes the
// nested class's static constructor to run, which then sets the enclosing class's
// _nestedFactory field appropriately.
static Outer()
{
Inner.Initialise(); // <--- If you comment this out, things won't work.
}
// This class has a private constructor.
// I only want class Outer to be able to create instances of it.
public sealed class Inner
{
private Inner(string value) // Secret private constructor!
{
text = value;
}
public string Text { get { return text; } }
static Inner()
{
_nestedFactory = text => new Inner(text);
}
internal static void Initialise(){}
readonly string text;
}
static Func<string, Inner> _nestedFactory;
}
}
3 ответа
Вы можете использовать этот код, если вам нужно заставить конструктор класса работать без ссылки на тип:
static Outer()
{
System.Runtime.CompilerServices.RuntimeHelpers.RunClassConstructor(typeof (Inner).TypeHandle);
}
C# не имеет функций "друга". Одним из прагматических подходов может быть просто попросить звонящего доказать, кто он. Одним из способов сделать это может быть предоставление объекта-ссылки, которую может знать только законный вызывающий класс, то есть object
то есть private
к внешнему классу. Предполагая, что вы не передадите этот объект, единственный способ обойти это было бы непубличным отражением, и если вам нужно защитить от непубличного отражения, то сценарий спорный, потому что любой, имеющий доступ к непубличному отражению, может уже получить доступ к таким вещам, как private
конструктор. Так что-то вроде:
class Outer {
// don't pass this reference outside of Outer
private static readonly object token = new object();
public sealed class Inner {
// .ctor demands proof of who the caller is
internal Inner(object token) {
if (token != Outer.token) {
throw new InvalidOperationException(
"Seriously, don't do that! Or I'll tell!");
}
// ...
}
}
// the outer-class is allowed to create instances...
private Inner Create() {
return new Inner(token);
}
}
Я также использовал статические конструкторы, чтобы строго контролировать доступность вложенного класса, и в итоге получился похожий слишком сложный код. Но в конце концов я нашел простое и понятное решение. Основой является явная реализация частного интерфейса. (Без статических конструкторов)
public sealed class Outer
{
private interface IInnerFactory
{
Inner CreateInner(string text);
}
private static IInnerFactory InnerFactory = new Inner.Factory();
public Inner Item(string text)
{
return InnerFactory.CreateInner(text);
}
public sealed class Inner
{
public class Factory : IInnerFactory
{
Inner IInnerFactory.CreateInner(string text)
{
return new Inner(text);
}
}
private Inner(string value)
{
text = value;
}
public string Text { get { return text; } }
readonly string text;
}
}
}
Это решение также обеспечивает безопасность времени компиляции. Хотя Outer.Inner.Factory можно создавать вне внешнего интерфейса, метод CreateInner можно вызывать только через интерфейс IInnerFactory, который означает только в Outer. Следующие строки не будут компилироваться (с разными ошибками) снаружи даже в одной сборке:
new Outer.Inner.Factory().CreateInner("");
((Outer.IInnerFactory)new Outer.Inner.Factory()).CreateInner("");