Итак, у меня есть вариант использования, похожий на Вот этот, но с некоторыми дополнительными особенностями, которые, как мне кажется, оправдывают новый вопрос. (Связанныйвопросы, для справки)
Я пишу структуру данных, реализующую цикл. Базовая конструкция примерно такая:
public class Cycle<T>
{
public Node<T> Origin { get; private set; }
public int Count { get; private set; }
}
public class Node<T>
{
public Cycle<T> Cycle { get; private set; }
public Node<T> Next { get; private set; }
public Node<T> Previous { get; private set; }
public T Value { get; set; }
}
Однако я хочу реализовать все следующего поведения:
Cycle (т. е. Origin = null) создать новый Node для OriginNode объектов за пределами случаев 1 и 2В C++ я бы просто сделал каждый класс friend другого. Но в С# я не вижу способа заставить это работать.
Я знаю, что могу выполнить все, кроме № 3, если вложу Node внутрь Cycle и выставлю только один конструктор для Node, например:
public class Cycle<T>
{
public Node Origin { get; private set; }
public int Count { get; private set; }
public class Node
{
public Cycle<T> Cycle { get; private set; }
public Node Next { get; private set; }
public Node Previous { get; private set; }
public T Value { get; set; }
internal Node<T>(Cycle<T> cycle)
{
if (cycle.Origin != null)
throw new InvalidOperationException();
else
{
Cycle = cycle;
Next = this;
Previous = this;
cycle.Origin = this;
cycle.Count = 1;
}
}
}
}
Но, как видите, я могу дойти только до internal, поэтому мне все еще нужно проверить целостность данных или нарушить инкапсуляцию.
У меня есть «умная» идея один, но это своего рода ответ черная магия:
public abstract class Cycle<T>
{
public Node Origin { get; private set; }
public int Count { get; private set; }
public sealed class Node
{
private CycleInternal _cycle;
public Cycle<T> Cycle { get { return _cycle; } }
public Node Next { get; private set; }
public Node Previous { get; private set; }
public T Value { get; set; }
// this constructor can be called by CycleInternal, but not other classes!
private Node(CycleInternal cycle)
{
Cycle = cycle;
Next = this;
Previous = this;
cycle.Origin = this;
cycle.Count = 1;
}
private sealed class CycleInternal : Cycle<T>
{
// this constructor can be called by Node, but not other classes!
public CycleInternal() {}
}
}
}
В этом случае меня беспокоит то, что что-то еще может наследоваться от Cycle; можно ли предотвратить это, сделав конструктор закрытым для цикла? Или я тут просто параноик?
Может я не понял тему, но зачем экземпляр Cycle внутри Node? Разве вам не нужен IEnumerable<Node<T>> в цикле?
@Matthew internal предоставляет его другим классам в той же сборке, чего я не хочу делать. Это часть библиотеки, и я не хочу рисковать что-нибудь, мне это не нужно. @DavideVitali Каждый Node отслеживает, частью которого Cycle он является, чтобы упростить расчет Cycle.Count, проверку того, являются ли два Node частью одного и того же Cycle, и обновление Cycle.Origin проще. Cycle нужна ссылка только на один узел, чтобы реализовать IEnumerable<Node<T>> себя.





internalexposes it to other classes in the same assembly, which I don't want to do
Мой совет: преодолейте этот страх и отметьте его internal.
Я слышал этот запрос на добавление функции — чтобы C# реализовывал дружественную семантику в стиле C++ — много, много раз. Эта функция обычно мотивирована опасением, что «если я позволю какому-либо классу в моей сборке участвовать во внутреннем состоянии, мои коллеги злоупотребят этой привилегией».
Но ваши коллеги уже могут злоупотреблять этой привилегией, потому что ваши коллеги уже могут использовать добавить друзей (в C++) или сделать внутренние методы, которые являются черным ходом (в C#).
Система специальных возможностей C# просто не предназначена для защиты вас от сценариев, в которых люди, имеющие доступ для записи к исходному коду, враждебно относятся к вашим интересам. Точно так же, как private означает «это деталь реализации этого класса», а protected означает «это деталь реализации этой иерархии», internal означает «это деталь реализации этой сборки». Если ваши коллеги, отвечающие за правильную работу этой сборки нельзя доверять, чтобы разумно использовать свои силы, найдите лучших сотрудников.Если вашим типам требуется доступ к внутренним деталям друг друга, чтобы сборка в целом работала правильно, это то, что internal для, поэтому используйте ее.
Или, другими словами, это социальная проблема, а не техническая проблема, поэтому вы должны решить ее, применяя социальное давление. Система специальных возможностей C# не предназначена для решения проблем, которые должны решаться путем проверки кода. Если ваши коллеги злоупотребляют своими привилегиями, время проверки кода — это время, чтобы заставить их остановиться, точно так же, как вы используете проверку кода, чтобы помешать им добавить какой-либо другой плохой код в ваш проект.
Чтобы ответить на ваши конкретные вопросы:
my worry is that something else could inherit from Cycle; could that be prevented by making the constructor to Cycle private?
Да, абстрактный класс может иметь частный конструктор, и тогда единственными производными типами являются вложенные типы.
Я использую этот шаблон довольно часто и делаю вложенные типы закрытыми. В C# нехорошо делать общедоступный вложенный тип.
В сторону: обычно я делаю это для создания «кейс-классов», например:
abstract class ImmutableStack<T>
{
private ImmutableStack() { }
private sealed class EmptyStack : ImmutableStack<T> { ... }
private sealed class NormalStack : ImmutableStack<T> { ... }
и так далее. Это хороший способ распределить детали реализации типа по нескольким классам, но при этом все инкапсулировать в один класс.
am I just being paranoid here?
Мне кажется, да.
Я не беспокоюсь о каком-либо злоупотреблении преднамеренный, я беспокоюсь, что кто-то (включая меня в будущем) неправильно поймет, как использовать класс, и сделает что-то (в другом файле), которое компилируется нормально, но терпит неудачу во время выполнения (возможно, молча) . Я бы предпочел сделать отказ невозможным (пока никто не редактирует сам класс).
@TravisReed поможет ли подробная документация? включить что можно и что нельзя делать. образцы кода. так далее
@TravisReed: В этом случае вам нужны две вещи: во-первых, набор тестов, который проверяет поведение типа публичный, а во-вторых, множество операторов Debug.Assert в вашем коде, которые проверяют внутренние инварианты, который должен поддерживаться кодом. Запустите набор тестов в режиме отладки, прежде чем проверять каждое изменение, и если в будущем вы нарушите общедоступное поведение или внутренний инвариант, вы получите уведомление и сможете исправить проблему.
Что не так с использованием
internal? Что вы пытаетесь предотвратить здесь?