Обходной путь С# для классов общих друзей

Итак, у меня есть вариант использования, похожий на Вот этот, но с некоторыми дополнительными особенностями, которые, как мне кажется, оправдывают новый вопрос. (Связанныйвопросы, для справки)

Я пишу структуру данных, реализующую цикл. Базовая конструкция примерно такая:

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; }
}

Однако я хочу реализовать все следующего поведения:

  1. Обновлять Cycle.Count по мере вставки/удаления узлов
  2. Разрешить «пустой» Cycle (т. е. Origin = null) создать новый Node для Origin
  3. Предотвратить строительство новых Node объектов за пределами случаев 1 и 2
  4. Избегайте раскрытия методов, которые не нужно вызывать классами, отличными от этих двух.

В 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; можно ли предотвратить это, сделав конструктор закрытым для цикла? Или я тут просто параноик?

Что не так с использованием internal? Что вы пытаетесь предотвратить здесь?

Matthew 27.03.2019 21:29

Может я не понял тему, но зачем экземпляр Cycle внутри Node? Разве вам не нужен IEnumerable<Node<T>> в цикле?

Davide Vitali 27.03.2019 21:31

@Matthew internal предоставляет его другим классам в той же сборке, чего я не хочу делать. Это часть библиотеки, и я не хочу рисковать что-нибудь, мне это не нужно. @DavideVitali Каждый Node отслеживает, частью которого Cycle он является, чтобы упростить расчет Cycle.Count, проверку того, являются ли два Node частью одного и того же Cycle, и обновление Cycle.Origin проще. Cycle нужна ссылка только на один узел, чтобы реализовать IEnumerable<Node<T>> себя.

Travis Reed 27.03.2019 21:49
Стоит ли изучать PHP в 2026-2027 годах?
Стоит ли изучать PHP в 2026-2027 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
4
3
465
1

Ответы 1

internal exposes 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?

Мне кажется, да.

Я не беспокоюсь о каком-либо злоупотреблении преднамеренный, я беспокоюсь, что кто-то (включая меня в будущем) неправильно поймет, как использовать класс, и сделает что-то (в другом файле), которое компилируется нормально, но терпит неудачу во время выполнения (возможно, молча) . Я бы предпочел сделать отказ невозможным (пока никто не редактирует сам класс).

Travis Reed 27.03.2019 23:05

@TravisReed поможет ли подробная документация? включить что можно и что нельзя делать. образцы кода. так далее

Jan Paolo Go 27.03.2019 23:09

@TravisReed: В этом случае вам нужны две вещи: во-первых, набор тестов, который проверяет поведение типа публичный, а во-вторых, множество операторов Debug.Assert в вашем коде, которые проверяют внутренние инварианты, который должен поддерживаться кодом. Запустите набор тестов в режиме отладки, прежде чем проверять каждое изменение, и если в будущем вы нарушите общедоступное поведение или внутренний инвариант, вы получите уведомление и сможете исправить проблему.

Eric Lippert 28.03.2019 15:59

Другие вопросы по теме