Подтвердить что ты не робот

В С# 4.0 можно ли каким-либо образом сделать частным членом одного класса доступным только для определенного другого класса?

Мы создаем иерархию объектов, в которой каждый элемент имеет коллекцию других элементов, и каждый элемент также имеет свойство Parent, указывающее на его родительский элемент. Довольно стандартный материал. У нас также есть класс ItemsCollection, который наследует от Collection<Item>, который сам имеет свойство Owner, указывающее на элемент, которому принадлежит коллекция. Опять же, ничего интересного нет.

Когда элемент добавляется в класс ItemsCollection, мы хотим, чтобы он автоматически установил родительский элемент Item (используя свойство collection Owner), и когда элемент удален, мы хотим очистить родительский элемент.

Вот вещь. Мы хотим, чтобы набор Parent был доступен для ItemsCollection, больше ничего. Таким образом мы не только узнаем, кто является родителем элемента, но также можем гарантировать, что элемент не будет добавлен в несколько коллекций, проверив существующее значение в Parent или позволяя кому-то произвольно изменить его на что-то другое.

Мы знаем, как это сделать:

  • Отметьте установщик как приватный, а затем включите определение коллекции в область самого элемента. Pro: Полная защита. Con: Уродливый код с вложенными классами.

  • Используйте частный ISetParent интерфейс для элемента, о котором знает только ItemsCollection. Pro: Очень чистый код и легко следовать. Con: Технически любой, кто знает интерфейс, может отличить Item и попасть в сеттер.

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

Теперь я знаю, что есть функция в С++ под названием Friend или что-то, что позволяет вам назначить в качестве отдельного частного участника в одном классе доступным для другого, что было бы идеальным сценарием, но я не знаю ни одного такого вещь в С#.

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

public class Item
{
    public string Name{ get; set; }
    public Item Parent{ get; private set; }
    public ItemsCollection ChildItems;

    public Item()
    {
        this.ChildItems = new ItemsCollection (this);
    }
}

public class ItemsCollection : ObservableCollection<Item>
{
    public ItemsCollection(Item owner)
    {
        this.Owner = owner;
    }   

    public Item Owner{ get; private set; }

    private CheckParent(Item item)
    {
        if(item.Parent != null) throw new Exception("Item already belongs to another ItemsCollection");
        item.Parent = this.Owner; // <-- This is where we need to access the private Parent setter
    }

    protected override void InsertItem(int index, Item item)
    {
        CheckParent(item);
        base.InsertItem(index, item);
    }

    protected override void RemoveItem(int index)
    {
        this[index].Parent = null;
        base.RemoveItem(index);
    }

    protected override void SetItem(int index, Item item)
    {
        var existingItem = this[index];

        if(item == existingItem) return;

        CheckParent(item);
        existingItem.Parent = null;

        base.SetItem(index, item);
    }

    protected override void ClearItems()
    {
        foreach(var item in this) item.Parent = null; <-- ...as is this
        base.ClearItems();
    }

}

Любой другой способ сделать что-то подобное?

4b9b3361

Ответ 1

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

Сделайте шаг назад. Какова основная проблема, которую вы пытаетесь решить? Согласованность. Вы пытаетесь убедиться, что отношение "x является дочерним элементом y" и отношение "y является родительским элементом x" всегда согласованы. Это разумная цель.

Ваше предположение состоит в том, что каждый элемент напрямую знает как своих детей, так и родительский элемент, потому что коллекция детей и родительская ссылка хранятся локально в полях. Это логически требует, чтобы, когда элемент x становится дочерним элементом y, вы должны последовательно изменять как x.Parent, так и y.Children. Это представляет проблему, с которой вы столкнулись: кто может быть "ответственным" за то, чтобы убедиться, что оба изменения сделаны последовательно? И как вы гарантируете, что только код "подзарядки" получает мутацию родительского поля?

Tricky.

Предположим, мы отрицали ваше предположение. Не обязательно, чтобы каждый предмет знал как своих детей, так и своего родителя.

Техника № 1:

Например, вы можете сказать, что есть один специальный элемент, называемый "вселенной", который является предком каждого элемента, кроме самого себя. "Вселенная" может быть одноэлементной, хранящейся в известном месте. Когда вы запрашиваете элемент для своего родителя, реализация может найти юниверс, а затем выполнить поиск каждого потомка вселенной, ищущего предмет, отслеживая путь по мере продвижения. Когда вы найдете предмет, отлично, все готово. Вы смотрите на один шаг назад на "путь", который доставил вас туда, и у вас есть родитель. Более того, вы можете обеспечить всю цепочку родителей, если хотите; в конце концов, вы просто вычислили его.

Техника № 2:

Это может быть дорого, если вселенная велика, и для нахождения каждого элемента требуется некоторое время. Другое решение состояло бы в том, чтобы юниверс содержал хеш-таблицу, которая сопоставляет элементы своим родителям и вторую хэш-таблицу, которая отображает элементы в список своих детей. Когда вы добавляете дочерний элемент x в родительский y, метод "add" на самом деле вызывает Вселенную и говорит "hey, элемент x теперь зарегистрирован y", а Universe заботится об обновлении хеш-таблиц. Элементы не содержат никакой информации о своей "связности"; что ответственность Вселенной за соблюдение.

Нижняя сторона этого - это то, что вселенная может содержать циклы; вы могли бы рассказать вселенной о том, что x является родительским по y, а y является родителем x. Если вы хотите этого избежать, вам придется написать детектор циклов.

Техника № 3:

Можно сказать, что есть два дерева; "реальное" дерево и дерево "фасад". Реальное дерево является неизменным и постоянным. В реальном дереве каждый элемент знает своих детей, но не его родителя. Когда вы построите неизменяемое реальное дерево, вы создадите фасад node, который является прокси-сервером для корня реального дерева. Когда вы спросите, что node для своих детей, он делает новый фасад node обернут вокруг каждого ребенка и устанавливает родительское свойство фасада node в node, которое было запрошено для его детей.

Теперь вы можете обрабатывать дерево фасадов как родительское дерево, но родительские отношения вычисляются только по мере прохождения дерева.

Если вы хотите отредактировать дерево, вы создаете новое реальное дерево, повторно используя как можно больше старого реального дерева. Затем вы создаете новый корень фасада.

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

Мы используем этот последний подход в компиляторах С# и VB, потому что это именно та ситуация, в которой мы находимся: когда мы перестраиваем дерево синтаксиса после редактирования кода, мы можем повторно использовать большую часть существующего неизменяемого дерева синтаксиса из предыдущего текста, Мы всегда пересекаем дерево сверху вниз и хотим только вычислить исходные ссылки при необходимости.

Ответ 2

С# Не имеет ключевого слова friend, но имеет что-то близкое имя internal. Вы можете отметить методы, которые вы хотите открыть ограниченным образом, как внутренние, а затем только другие типы в этой сборке смогут их увидеть. Если весь ваш код находится в одной сборке, это не поможет вам, но если этот класс будет упакован в отдельную сборку, он будет работать.

public class Item
{
    public string Name{ get; set; }
    public Item Parent{ get; internal set; } // changed to internal...
    public ItemsCollection ChildItems;

    public Item()
    {
        this.ChildItems = new ItemsCollection (this);
    }
}

Ответ 3

Здесь вы можете имитировать friend в С#:

Отметьте свои свойства internal, а затем используйте этот атрибут, чтобы открыть их в friend ассамблеях:

[assembly: InternalsVisibleTo("Friend1, PublicKey=002400000480000094" + 
                              "0000000602000000240000525341310004000" +
                              "001000100bf8c25fcd44838d87e245ab35bf7" +
                              "3ba2615707feea295709559b3de903fb95a93" +
                              "3d2729967c3184a97d7b84c7547cd87e435b5" +
                              "6bdf8621bcb62b59c00c88bd83aa62c4fcdd4" +
                              "712da72eec2533dc00f8529c3a0bbb4103282" +
                              "f0d894d5f34e9f0103c473dce9f4b457a5dee" +
                              "fd8f920d8681ed6dfcb0a81e96bd9b176525a" +
                              "26e0b3")]

public class MyClass {
   // code...
}

Ответ 4

Только две вещи, о которых я могу думать:

Один:

Используйте опцию номер 2, о которой вы упомянули выше (что я делаю постоянно сам)... но сделайте реализацию интерфейса (Item) вложенным частным классом внутри ItemsCollection... только так ItemsCollection знает об установщике. Интерфейс IItem только объявляет getter для Parent... и никто не может передать его Item, потому что Item является закрытым для ItemsCollection. Итак, что-то вроде:

public class ItemsCollection : ObservableCollection<IItem>
{
    private class Item : IItem 
    {
        public object Parent { get; set; }
    }

    private CheckParent(IItem item)
    {
        if(item.Parent != null) throw new Exception("Item already belongs to another ItemsCollection");
        ((Item)item).Parent = this.Owner; // <-- This is where we need to access the private Parent setter
    }

    public static IItem CreateItem() { return new Item(); }
}

public interface IItem 
{
    object Parent {get; }
}

и если вы хотите, чтобы ItemsCollection установил элемент "Родительский", введите экземпляр IItem в Item (который выставляет сеттер). Только ItemsCollection может выполнить этот отбор, так как реализация элемента закрыта для ItemsCollection... поэтому я думаю, что выполняет то, что вы хотите.

Два:

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

Ответ 5

Первое, что поразило меня этим сценарием, - это то, что между ItemCollection и Item есть определенная зависть. Я понимаю ваше желание добавить дочерний элемент в коллекцию и установить родительскую роль автономной операции, но на самом деле я считаю, что ответственность за сохранение этих отношений заключается в Item, а не в ItemCollection.

Я бы рекомендовал разоблачить элементы ChildItems в элементе как коллекции только для чтения (возможно, с IEnumerable<Item>) и поместить методы AddChild(Item child), RemoveChild(Item child), ClearChildren() и т.д. на элемент. Это несет ответственность за поддержание Родитель с Элементом, в котором у вас нет проблем с утечкой в ​​другие классы.

Ответ 6

Как насчет того, чтобы убедиться, что только элемент текущей коллекции может осилить элемент. Таким образом, никакая другая коллекция не может установить родительский элемент, пока он принадлежит коллекции. Вы можете использовать уникальный ключ какого-то типа, чтобы третья сторона не могла принять участие:

public sealed class ItemsCollection : ObservableCollection<Item>
{
    private Dictionary<Item, Guid> guids = new Dictionary<Item, Guid>();

    public ItemsCollection(Item owner)
    {
        this.Owner = owner;
    }

    public Item Owner { get; private set; }

    private Guid CheckParent(Item item)
    {
        if (item.Parent != null)
            throw new Exception("Item already belongs to another ItemsCollection");
        //item.Parent = this.Owner; // <-- This is where we need to access the private Parent setter     
        return item.BecomeMemberOf(this);

    }

    protected override void InsertItem(int index, Item item)
    {
        Guid g = CheckParent(item);
        base.InsertItem(index, item);
        guids.Add(item, g);
    }

    protected override void RemoveItem(int index)
    {
        Item item = this[index];
        DisownItem(item);
        base.RemoveItem(index);
    }

    protected override void DisownItem(Item item)
    {
        item.BecomeOrphan(guids[item]);
        guids.Remove(item);            
    }

    protected override void SetItem(int index, Item item)
    {
        var existingItem = this[index];
        if (item == existingItem)
            return;
        Guid g = CheckParent(item);
        existingItem.BecomeOrphan(guids[existingItem]);
        base.SetItem(index, item);
        guids.Add(item, g);
    }

    protected override void ClearItems()
    {
        foreach (var item in this)
            DisownItem(item);
        base.ClearItems();
    }
}




public class Item
{
    public string Name { get; set; }
    public Item Parent { get; private set; }

    public ItemsCollection ChildItems;

    public Item()
    {
        this.ChildItems = new ItemsCollection(this);
    }

    private Guid guid;

    public Guid BecomeMemberOf(ItemsCollection collection)
    {
        if (Parent != null)
            throw new Exception("Item already belongs to another ItemsCollection");
        Parent = collection.Owner;
        guid = new Guid();
        return guid; // collection stores this privately         
    }

    public void BecomeOrphan(Guid guid) // collection passes back stored guid         
    {
        if (guid != this.guid)
            throw new InvalidOperationException("Item can only be orphaned by its current collection");
        Parent = null;
    }
}

Очевидно, там есть избыточность; в коллекции предметов хранится вторая коллекция предметов (словарь). Но есть множество вариантов преодоления того, что, как я предполагаю, вы можете придумать. Это рядом с пунктом здесь.

Однако я предлагаю вам переместить задачу управления дочерними элементами в класс item и сохранить коллекцию как "тупой", насколько это возможно.

РЕДАКТИРОВАТЬ: в ответ на ваш вопрос, как это предотвращает и элемент из двух ItemsCollection s:

Вы спрашиваете, какова точка указаний. Почему бы просто не использовать экземпляр коллекции?

Если вы замените аргумент guid аргументом коллекции, вы можете добавить элемент в две разные коллекции:

{
    collection1.InsertItem(item);  // item parent now == collection1
    collection2.InsertItem(item);  // fails, but I can get around it:
    item.BecomeOrphan(collection1); // item parent now == null 
    collection2.InsertItem(item);  // collection2 hijacks item by changing its parent (and exists in both collections)
}

Теперь представьте, что вы делаете это с аргументом guid:

{
    collection1.InsertItem(item);  // item parent now == collection1
    collection2.InsertItem(item);  // fails, so...
    item.BecomeOrphan(????); // can't do it because I don't know the guid, only collection1 knows it.
}

Таким образом, вы не можете добавить элемент в несколько ItemsCollection. И ItemsCollection запечатан, поэтому вы не можете подклассифицировать его и переопределить его метод Insert (и даже если вы это сделали, вы все равно не можете изменить родительский элемент).

Ответ 7

Вы можете делать такие вещи с помощью делегатов:

public delegate void ItemParentChangerDelegate(Item item, Item newParent);

public class Item
{
    public string Name{ get; set; }
    public Item Parent{ get; private set; }
    public ItemsCollection ChildItems;

    static Item()
    {
        // I hereby empower ItemsCollection to be able to set the Parent property:
        ItemsCollection.ItemParentChanger = (item, parent) => { item.Parent = parent };
        // Now I just have to trust the ItemsCollection not to do evil things with it, such as passing it to someone else...
    }
    public static void Dummy() { }

    public Item()
    {
        this.ChildItems = new ItemsCollection (this);
    }
}

public class ItemsCollection : ObservableCollection<Item>
{
    static ItemsCollection()
    {
        /* Forces the static constructor of Item to run, so if anyone tries to set ItemParentChanger,
        it runs this static constructor, which in turn runs the static constructor of Item,
        which sets ItemParentChanger before the initial call can complete.*/
        Item.Dummy();
    }
    private static object itemParentChangerLock = new object();
    private static ItemParentChangerDelegate itemParentChanger;
    public static ItemParentChangerDelegate ItemParentChanger
    {
        private get
        {
            return itemParentChanger;
        }
        set
        {
            lock (itemParentChangerLock)
            {
                if (itemParentChanger != null)
                {
                    throw new InvalidStateException("ItemParentChanger has already been initialised!");
                }
                itemParentChanger = value;
            }
        }
    }

    public ItemsCollection(Item owner)
    {
        this.Owner = owner;
    }   

    public Item Owner{ get; private set; }

    private CheckParent(Item item)
    {
        if(item.Parent != null) throw new Exception("Item already belongs to another ItemsCollection");
        //item.Parent = this.Owner;
        ItemParentChanger(item, this.Owner); // Perfectly legal! :)
    }

    protected override void InsertItem(int index, Item item)
    {
        CheckParent(item);
        base.InsertItem(index, item);
    }

    protected override void RemoveItem(int index)
    {
        ItemParentChanger(this[index], null);
        base.RemoveItem(index);
    }

    protected override void SetItem(int index, Item item)
    {
        var existingItem = this[index];

        if(item == existingItem) return;

        CheckParent(item);
        ItemParentChanger(existingItem, null);

        base.SetItem(index, item);
    }

    protected override void ClearItems()
    {
        foreach(var item in this) ItemParentChanger(item, null);
        base.ClearItems();
    }

Ответ 8

Мой ответ построен из двух частей

  • Почему так "небезопасно" иметь интерфейс ISetParent?

частные/внутренние модификаторы доступа предназначены для предотвращения ошибок, не действительно "защищенный код".

Помните... вы можете вызвать частные методы, используя некоторые Reflections/Invoke Etc...

.

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

Конечно, есть маленький пинг-понг, но требуется всего несколько циклов (в этом случае у меня есть NamedSet)

    private IPhysicalObject partOf;
    public IPhysicalObject PartOf
    {
        get { return partOf; }
        set
        {
            if (partOf != value)
            {
                if (partOf != null)
                    partOf.Children.Remove(this.Designation);

                partOf = value;

                if (partOf != null)
                    partOf.Children.Add(this.Designation);
            }
        }
    }

    public virtual void Add(String key, IPhysicalObject value)
    {
        IPhysicalObject o;
        if (!TryGetValue(key, out o))
        {
            innerDictionary.Add(key, value);
            value.PartOf = Parent;
        }
    }

    public virtual bool Remove(String key)
    {
        IPhysicalObject o;
        if(TryGetValue(key, out o))
        {
            innerDictionary.Remove(key);
            o.PartOf = null;
        }
    }

Надеюсь, что это поможет... Хорошего дня, Tomer W.

P.S. Я никогда не получу, как работает этот редактор... должен ли я HTML-код, или мне это не нужно?

Ответ 9

Одним из решений, которое я использовал для контроля видимости членов класса, является определение класса как частичного, а затем в другом пространстве имен объявите класс как частичное и определите специальные элементы видимости, которые вы хотите.

Это контролирует видимость элемента в зависимости от выбранного пространства имен.

Единственное, что вам нужно будет обернуть вокруг, - это ссылки. Он может стать сложным, но как только вы его выясните, он работает.