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

#c# #.net

#c# #.net

Вопрос:

Рассмотрим следующий шаблон кода:

 // Each foo keeps a reference to its manager
class Foo
{
    private FooManager m_manager;
}

// Manager keeps a list of all foos
class FooManager
{
    private List<Foo> m_foos;
}
  

Проблема: нет способа создать новый Foo и обновить как список m_foos в FooManager, так и ссылку m_manager в новом экземпляре Foo, не раскрывая некоторые личные данные публично (и рискуя, что кто-то отменит синхронизацию списка с реальными Foo).

Например. можно было бы реализовать конструктор Foo(менеджер FooManager) в Foo. Он мог бы установить ссылку m_manager, но у него нет способа получить доступ к списку m_foos. Или вы могли бы реализовать метод createFoo() в диспетчере. Он может получить доступ к списку m_foos, но у него нет способа установить m_manager в Foo.

В C , очевидно, можно было бы объявить FooManager другом Foo, чтобы выразить намерение проекта, но в C # это невозможно. Я также знаю, что я мог бы сделать Foo внутренним классом FooManager для получения доступа, но это тоже не решение (что, если Foo может принадлежать более чем одному классу manager?)

Кстати. Я знаю о «внутреннем» доступе в .NET, но для этого требуется, чтобы Foo и FooManager работали самостоятельно в отдельной сборке, что неприемлемо.

Есть ли обходные пути для этого, не делая частные материалы общедоступными?

Комментарии:

1. Всегда есть атрибут InternalsVisibleToAttribute, но это звучит как взлом. Как насчет того, чтобы скрыть материал за интерфейсом?

2. Как бы выглядел такой интерфейс, не позволяя пользователю синхронизировать m_foos и m_manager?

3. Можно ли превратить FooManager в фабрику для Foo? Тогда менеджер знает все Foo, которые он создал.

4. @Godeke: Но как FooManager может изменить m_manager Foo, не делая его общедоступным?

5. Почему разделение на большее количество сборок было бы неприемлемым? Даже если в сборке есть другие классы, они должны быть друзьями и враждебными (или другими, как некоторые их называют).

Ответ №1:

Если я все правильно понимаю:

 public abstract class FooBus
{
    protected static FooBus m_bus;
}

public sealed class Foo : FooBus
{
    private FooManager m_manager;

    public Foo(FooManager fm)
    {
        if (fm == null)
        {
            throw new ArgumentNullException("Use FooManager.CreateFoo()");
        }

        if (m_bus != fm)
        {
            throw new ArgumentException("Use FooManager.CreateFoo()");
        }

        m_manager = fm;
    }
}

public class FooManager : FooBus
{
    private List<Foo> m_foos = new List<Foo>();

    public Foo CreateFoo()
    {
        m_bus = this;
        Foo f = new Foo(this);
        m_foos.Add(f);
        m_bus = null;

        return f;
    }
}
  

Комментарии:

1. Что с FooManager fm в подписи для createFoo?

2. Ну, это не очень хорошее решение, потому что кто-то может вызвать new Foo() напрямую, и список m_foos будет недействительным. (Кстати, я понимаю, что перед конструктором Foo есть public)

3. @kaalus вы правы, это решение не работает, но пока не могу придумать что-то лучше.

4. @kaalus еще одна попытка, проверьте это.

5. @Petr Спасибо за ваши усилия, я ценю это. Пожалуйста, не воспринимайте мою критику как нечто негативное. Итак, вот оно: (1) это не потокобезопасно. (2) это приводит к ошибке времени выполнения вместо ошибки компиляции. (3) кто-то другой может наследовать от FooBus и при этом рассинхронизировать ссылки на список и m_manager (4) даже если бы это можно было заставить работать, это явно слишком сложный способ, чтобы быть действительной реализацией общего шаблона проектирования 🙂 (5) Нам нужны объявления друзей в C # 🙁

Ответ №2:

Одним из вариантов было бы использовать частный вложенный класс для Foo, который реализует общедоступный интерфейс:

 public interface IFoo
{
    // Foo's interface
}

public sealed class FooManager
{
    private readonly List<Foo> _foos = new List<Foo>();

    public IFoo CreateFoo()
    {
        var foo = new Foo(this);
        _foos.Add(foo);
        return foo;
    }

    private class Foo : IFoo
    {
        private readonly FooManager _manager;

        public Foo(FooManager manager)
        {
            _manager = manager;
        }
    }
}
  

Поскольку класс Foo является частным вложенным классом, его нельзя создать вне FooManager, и поэтому CreateFoo() метод FooManager гарантирует, что все остается синхронизированным.

Комментарии:

1. Это хорошая попытка. Но что, если бы я захотел добавить Foo более чем в 1 класс manager? Я не могу сделать Foo частным внутренним классом из двух других классов 🙁 Кроме того, это решение требует от меня дублирования объявлений (как в интерфейсе, так и в фактическом Foo), и это замедляет все вызовы, требуя, чтобы они проходили через интерфейс. Все это было бы излишним, если бы friend был доступен в C #.

2. Во-первых, вы можете сделать FooManager базовым классом для ваших других классов manager. Затем базовый класс мог бы предоставить метод createFoo() и сохранить взаимосвязь. Во-вторых, интерфейсы работают не так, и предоставление функциональности через интерфейсы является хорошей практикой программирования.

Ответ №3:

Что вы можете сделать, так это создать свои классы внутри пространства имен другого типа, давайте назовем это «module» (не обманывайтесь ключевым словом class, это не реальный класс):

 public static partial class FooModule {

  // not visible outside this "module"
  private interface IFooSink {
    void Add(Foo foo);
  }

  public class Foo {
    private FooManager m_manager;
    public Foo(FooManager manager) {
      ((IFooSink)manager).Add(this);
      m_manager = manager;
    }
  }

  public class FooManager : IFooSink {
    private List<Foo> m_foos = new List<Foo>();
    void IFooSink.Add(Foo foo) {
      m_foos.Add(foo);
    }
  }

}
  

Поскольку «модуль» является частичным классом, вы все равно можете создавать другие элементы внутри него в других файлах в том же модуле компиляции.

Ответ №4:

Как насчет наличия базового класса:

 class FooBase
{
     protected static readonly Dictionary<Foo,FooManager> _managerMapping = new Dictionary<Foo,FooManager>();
}
  

Тогда Foo и FooManager имеют FooBase в качестве базового класса и могут обновлять свое отображение, не предоставляя его извне. Тогда вы можете быть уверены, что никто не изменит эту коллекцию извне.

Тогда в Foo у вас есть свойство, Manager которое вернет связанного с ним менеджера и подобное, свойство Foos в Manager , которое выдаст все Foo.

Комментарии:

1. Та же проблема, что и с Petr в другом ответе: что мешает кому-то другому наследовать FooBase и возиться с _managerMapping? Кроме того, это довольно тяжелый с точки зрения производительности поиск по словарю вместо простого доступа к участникам, который у нас был бы, если бы friend был доступен в C#

Ответ №5:

В этом примере единственные, кто может нарушить синхронизацию отношений, — это Foo и сам менеджер. createFoo() вызывается менеджером для создания «управляемого foo». Кто-то другой может создать Foo, но он не может передать его в управление менеджеру без «согласия» менеджера.

 public class Foo
{
    private FooManager m_manager;

    public void SetManager(FooManager manager)
    {
        if (manager.ManagesFoo(this))
        {
            m_manager = manager;
        }
        else
        {
            throw new ArgumentException("Use Manager.CreateFoo() to create a managed Foo");
        }
    }
}

public class FooManager
{
    private List<Foo> m_foos = new List<Foo>();

    public Foo CreateFoo()
    {
        Foo foo = new Foo();
        m_foos.Add(foo);
        foo.SetManager(this);

        return foo;
    }

    public bool ManagesFoo(Foo foo)
    {
        return m_foos.Contains(foo);
    }
}
  

Ответ №6:

Я рассматриваю возможность использования отражения.

Не существует хорошего решения для неудачного дизайна (отсутствие друзей в среде CLR).

Комментарии:

1. Должен согласиться с вами здесь. В C # невозможно выразить фундаментальную идею о том, что классам иногда необходимо работать в тандеме. Тяжелые обходные пути (помещение их в новую отдельную сборку или использование отражения) не решают проблему. Кстати. Я вернулся к этому старому вопросу, потому что я снова столкнулся с той же проблемой! Нам нужны друзья в C #. В противном случае возникает неаккуратный дизайн.

Ответ №7:

Как насчет чего-то подобного:

 public class FriendClass
{
     public void DoSomethingInMain()
     {
         MainClass.FriendOnly(this);
     }
}

public class MainClass
{
    public static void FriendOnly(object Caller)
    {
        if (!(Caller is FriendClass) /* Throw exception or return */;

        // Code
    }
}
  

Конечно, это не мешает пользователю делать что-то подобное:

 public class NonFriendClass
{
    public void DoSomething()
    {
         MainClass.FriendOnly(new FriendClass());
    }
}
  

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