В C # 4.0 есть ли способ сделать закрытый член одного класса доступным только для другого конкретного класса? - PullRequest
20 голосов
/ 07 июля 2011

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

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

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

Мы знаем, как это сделать двумя способами:

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

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

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

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

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

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

}

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

Ответы [ 9 ]

15 голосов
/ 07 июля 2011

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

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

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

Tricky.

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

Техника № 1:

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

Техника # 2:

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

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

Техника № 3:

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

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

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

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

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

5 голосов
/ 07 июля 2011

Вот способ, которым вы можете смоделировать friend в C #:

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

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

public class MyClass {
   // code...
}
5 голосов
/ 07 июля 2011

C # Не имеет ключевого слова 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);
    }
}
4 голосов
/ 07 июля 2011

Я могу придумать только две вещи:

One:

Используйте опцию номер 2, которую вы упомянули выше (которую я постоянно делаю сам) ... но сделайте реализацию интерфейса (Item) вложенным закрытым классом внутри ItemsCollection ... только таким образом ItemsCollection знает о сеттере. Интерфейс IItem объявляет только получатель для 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 установил элемент Parent, установите для экземпляра IItem Item (который действительно предоставляет сеттер). Только ItemsCollection может выполнить это приведение, поскольку реализация Item является приватной для ItemsCollection ... так что я думаю, что это выполняет то, что вы хотите.

Два:

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

0 голосов
/ 21 апреля 2013

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

Управляет видимостью члена в зависимости от выбранного пространства имен.

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

0 голосов
/ 07 февраля 2012

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

  1. Почему так небезопасно иметь интерфейс 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;
        }
    }

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

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

0 голосов
/ 17 июля 2011

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

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();
    }
0 голосов
/ 07 июля 2011

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

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

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

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

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

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

Если вы замените аргумент 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 (и даже если вы это сделаете, вы все равно не сможете изменить родителя элемента).

0 голосов
/ 07 июля 2011

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

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

...