Как ограничить доступ к вложенному члену класса включающему классу?
-
12-09-2019 - |
Вопрос
Можно ли указать, что члены вложенного класса могут быть доступны включающему классу, но не другим классам?
Вот иллюстрация проблемы (конечно, мой реальный код немного сложнее...):
public class Journal
{
public class JournalEntry
{
public JournalEntry(object value)
{
this.Timestamp = DateTime.Now;
this.Value = value;
}
public DateTime Timestamp { get; private set; }
public object Value { get; private set; }
}
// ...
}
Я хотел бы запретить клиентскому коду создавать экземпляры JournalEntry
, но Journal
должен уметь их создавать.Если я сделаю конструктор общедоступным, каждый сможет создавать экземпляры...но если я сделаю это приватным, Journal
не смогу!
Обратите внимание, что JournalEntry
класс должен быть общедоступным, потому что я хочу иметь возможность предоставлять существующие записи клиентскому коду.
Любое предложение будет оценено по достоинству!
ОБНОВЛЯТЬ:Спасибо всем за ваш вклад, в конце концов я обратился к публике. IJournalEntry
интерфейс, реализованный частным JournalEntry
класс (несмотря на последнее требование в моем вопросе...)
Решение
Если ваш класс не слишком сложен, вы можете либо использовать общедоступный интерфейс и сделать фактический реализующий класс закрытым, либо создать защищенный конструктор для JornalEntry
класс и иметь частный урок JornalEntryInstance
полученный из JornalEntry
с общедоступным конструктором, экземпляр которого фактически создается вашим Journal
.
public class Journal
{
public class JournalEntry
{
protected JournalEntry(object value)
{
this.Timestamp = DateTime.Now;
this.Value = value;
}
public DateTime Timestamp { get; private set; }
public object Value { get; private set; }
}
private class JournalEntryInstance: JournalEntry
{
public JournalEntryInstance(object value): base(value)
{ }
}
JournalEntry CreateEntry(object value)
{
return new JournalEntryInstance(value);
}
}
Если ваш фактический класс слишком сложен, чтобы сделать что-либо из этого, и вам может сойти с рук конструктор, который не будет полностью невидимым, вы можете сделать конструктор внутренним, чтобы он был виден только в сборке.
Если это тоже невозможно, вы всегда можете сделать конструктор закрытым и использовать отражение для его вызова из класса журнала:
typeof(object).GetConstructor(new Type[] { }).Invoke(new Object[] { value });
Теперь, когда я думаю об этом, другая возможность заключается в использовании частного делегата в содержащем классе, который устанавливается из внутреннего класса.
public class Journal
{
private static Func<object, JournalEntry> EntryFactory;
public class JournalEntry
{
internal static void Initialize()
{
Journal.EntryFactory = CreateEntry;
}
private static JournalEntry CreateEntry(object value)
{
return new JournalEntry(value);
}
private JournalEntry(object value)
{
this.Timestamp = DateTime.Now;
this.Value = value;
}
public DateTime Timestamp { get; private set; }
public object Value { get; private set; }
}
static Journal()
{
JournalEntry.Initialize();
}
static JournalEntry CreateEntry(object value)
{
return EntryFactory(value);
}
}
Это должно дать вам желаемый уровень видимости без необходимости прибегать к медленному отражению или вводить дополнительные классы/интерфейсы.
Другие советы
На самом деле существует полное и простое решение этой проблемы, не требующее изменения клиентского кода или создания интерфейса.
В большинстве случаев это решение на самом деле быстрее, чем решение на основе интерфейса, и его проще кодировать.
public class Journal
{
private static Func<object, JournalEntry> _newJournalEntry;
public class JournalEntry
{
static JournalEntry()
{
_newJournalEntry = value => new JournalEntry(value);
}
private JournalEntry(object value)
{
...
Делать JournalEntry
а частный вложенный тип.Любые открытые члены будут видны только включающему типу.
public class Journal
{
private class JournalEntry
{
}
}
Если вам нужно сделать JournalEntry
объекты, доступные другим классам, предоставляйте их через общедоступный интерфейс:
public interface IJournalEntry
{
}
public class Journal
{
public IEnumerable<IJournalEntry> Entries
{
get { ... }
}
private class JournalEntry : IJournalEntry
{
}
}
Более простой подход – просто использовать internal
конструктор, но заставьте вызывающего абонента доказать, кто он, предоставив ссылку, которая только законный абонент могли бы знать (нам не нужно беспокоиться о закрытом размышлении, потому что, если у вызывающего есть доступ к закрытому отражению, то мы уже проиграли битву - он может получить доступ к 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 static Inner Create() {
return new Inner(token);
}
}
В этом случае вы можете:
- Сделайте конструктор внутренним — это не позволит тем, кто находится за пределами этой сборки, создавать новые экземпляры или...
- Рефакторинг
JournalEntry
класс, чтобы использовать общедоступный интерфейс и сделать фактическийJournalEntry
класс частный или внутренний.Затем интерфейс можно открыть для коллекций, в то время как фактическая реализация будет скрыта.
Выше я упомянул «внутренний» как допустимый модификатор, однако, в зависимости от ваших требований, «частный» может оказаться более подходящей альтернативой.
Редактировать: Извините, я упомянул частный конструктор, но вы уже рассмотрели этот вопрос в своем вопросе.Мои извинения, что не правильно прочитал!
Для общего вложенного класса =)
Я знаю, что это старый вопрос, и на него уже есть принятый ответ, тем не менее для тех пловцов Google, у которых может быть сценарий, аналогичный моему, этот ответ может оказать некоторую помощь.
Я столкнулся с этим вопросом, поскольку мне нужно было реализовать ту же функцию, что и ОП.Для моего первого сценария этот и этот ответы работали нормально.Тем не менее мне также нужно было предоставить вложенный общий класс.Проблема в том, что вы не можете предоставить поле типа делегата (поле фабрики) с открытыми универсальными параметрами, не создавая собственный универсальный класс, но, очевидно, это не то, что мы хотим, поэтому вот мое решение для такого сценария:
public class Foo
{
private static readonly Dictionary<Type, dynamic> _factories = new Dictionary<Type, dynamic>();
private static void AddFactory<T>(Func<Boo<T>> factory)
=> _factories[typeof(T)] = factory;
public void TestMeDude<T>()
{
if (!_factories.TryGetValue(typeof(T), out var factory))
{
Console.WriteLine("Creating factory");
RuntimeHelpers.RunClassConstructor(typeof(Boo<T>).TypeHandle);
factory = _factories[typeof(T)];
}
else
{
Console.WriteLine("Factory previously created");
}
var boo = (Boo<T>)factory();
boo.ToBeSure();
}
public class Boo<T>
{
static Boo() => AddFactory(() => new Boo<T>());
private Boo() { }
public void ToBeSure() => Console.WriteLine(typeof(T).Name);
}
}
У нас есть Бу как наш внутренний вложенный класс с частным конструктором, и мы поддерживаем в нашем родительском классе словарь с этими универсальными фабриками, используя преимущества динамичный.Итак, каждый раз TestMeЧувак вызывается, Foo ищет, была ли уже создана фабрика для T, если нет, то создает ее, вызывая статический конструктор вложенного класса.
Тестирование:
private static void Main()
{
var foo = new Foo();
foo.TestMeDude<string>();
foo.TestMeDude<int>();
foo.TestMeDude<Foo>();
foo.TestMeDude<string>();
Console.ReadLine();
}
Результат:
Решение, предложенное Гризли, действительно усложняет создание вложенного класса где-то еще, но не является невозможным, как писал Тим Полманн, кто-то все еще может наследовать его и использовать наследующий класс ctor.
Я использую тот факт, что вложенный класс может получить доступ к частным свойствам контейнера, поэтому контейнер хорошо запрашивает, а вложенный класс предоставляет доступ к ctor.
public class AllowedToEmailFunc
{
private static Func<long, EmailPermit> CreatePermit;
public class EmailPermit
{
public static void AllowIssuingPermits()
{
IssuegPermit = (long userId) =>
{
return new EmailPermit(userId);
};
}
public readonly long UserId;
private EmailPermit(long userId)
{
UserId = userId;
}
}
static AllowedToEmailFunc()
{
EmailPermit.AllowIssuingPermits();
}
public static bool AllowedToEmail(UserAndConf user)
{
var canEmail = true; /// code checking if we can email the user
if (canEmail)
{
return IssuegPermit(user.UserId);
}
else
{
return null
}
}
}
Это решение я бы не стал использовать в обычный рабочий день не потому, что оно приведет к проблемам в других местах, а потому, что оно нетрадиционное (я никогда раньше его не видел) и может причинить боль другим разработчикам.