簡體   English   中英

在 C# 4.0 中,有沒有辦法讓一個 class 的其他私有成員僅可用於特定的其他 class?

[英]In C# 4.0, is there any way to make an otherwise private member of one class available only to a specific other class?

我們正在創建一個 object 層次結構,其中每個項目都有其他項目的集合,並且每個項目還有一個指向其父項目的Parent屬性。 很標准的東西。 我們還有一個ItemsCollection class 繼承自Collection<Item> ,它本身有一個Owner屬性指向該集合所屬的項目。 同樣,那里沒有什么有趣的。

當一個項目被添加到ItemsCollection class 時,我們希望它自動設置 Item 的父項(使用集合的Owner屬性),當項目被刪除時,我們想要清除父項。

事情就是這樣。 我們只希望Parent設置器可用於ItemsCollection ,僅此而已。 這樣,我們不僅可以知道項目的父項是誰,而且我們還可以通過檢查Parent中的現有值或讓某人任意將其更改為其他值來確保不會將項目添加到多個 collections 中。

我們知道如何做到這一點的兩種方法是:

  1. 將 setter 標記為私有,然后將集合定義包含在項目本身的 scope 中。 優點:全面保護。 缺點:帶有嵌套類的丑陋代碼。

  2. 在 Item 上使用只有ItemsCollection知道的私有ISetParent接口。 優點:代碼更簡潔,易於理解。 缺點:從技術上講,任何知道界面的人都可以施放Item並獲得 setter。

現在從技術上講,通過反射,任何人都可以得到任何東西,但仍然......試圖找到最好的方法來做到這一點。

現在我知道 C++ 中有一個名為Friend的功能,或者可以讓您將一個 class 中的其他私有成員指定為另一個可用的功能,這將是完美的場景,但我不知道 ZD7EFA19FBE427D3972FD.ZADB6 中有任何這樣的事情。

在偽代碼中(例如,為簡潔起見,所有屬性更改通知等已被刪除,我只是在這里輸入,而不是從代碼中復制),我們有這個......

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

}

還有其他方法可以做類似的事情嗎?

我必須每天解決你的問題,但我不會按照你嘗試的方式去做。

退后一步。 你試圖解決的根本問題是什么? 一致性 您試圖確保“x 是 y 的子代”關系和“y 是 x 的父代”關系始終保持一致。 這是一個明智的目標。

您的假設是每個項目都直接知道其子項和其父項,因為子項集合和父項引用存儲在本地字段中。 這在邏輯上要求當項 x 成為項 y 的子項時,您必須始終更改 x.Parent 和 y.Children。 這就提出了您遇到的問題:誰來“負責”確保這兩項更改始終如一地進行? 以及如何確保只有“負責”代碼才能改變父字段?

棘手。

假設我們否定了你的假設。 不一定每個項目都知道它的子項和它的父項。

技術#1:

例如,你可以說有一個特殊的項目叫做“宇宙”,它是除了它自己之外的所有項目的祖先。 “宇宙”可能是存儲在知名位置的 singleton。 當您向項目詢問其父項時,實現可以找到宇宙,然后搜索宇宙的每個后代以尋找該項目,並在您 go 時跟蹤路徑。 當您找到該項目時,太好了,您完成了。 你回頭看看讓你到達那里的“路徑”,你有父母。 更好的是,如果您願意,您可以提供整個父母 畢竟,你只是計算出來的。

技術#2:

如果宇宙很大並且需要一段時間才能找到每個項目,那可能會很昂貴。 另一種解決方案是讓 Universe 包含一個將項目映射到其父項的 hash 表,以及將項目映射到其子項列表的第二個 hash 表。 當您將子 x 添加到父 y 時,“add”方法實際上調用 Universe 並說“嘿,項目 x 現在是 y 的父項”,並且 Universe 負責更新 hash 表。 項目不包含任何它們自己的“連接性”信息; 這是宇宙強制執行的責任。

不利的一面是宇宙可能包含循環。 你可以告訴宇宙 x 是 y 的父級,y 是 x 的父級。 如果你想避免這種情況,那么你必須編寫一個循環檢測器。

技巧#3:

你可以說有兩棵樹; “真實”樹和“立面”樹。 真正的樹是不可變的和持久的 在真實的樹中,每個項目都知道它的孩子,但不知道它的父母。 一旦你構建了不可變的真實樹,你就可以創建一個外觀節點作為真實樹根的代理。 當您詢問該節點的子節點時,它會在每個子節點周圍創建一個新的外觀節點,並將外觀節點的 parent 屬性設置為為其子節點查詢的節點。

現在您可以將外觀樹視為父樹,但父關系僅在您遍歷樹時計算。

當您想要編輯樹時,您會生成一棵新的真實樹,並盡可能多地重復使用舊的真實樹。 然后創建一個新的外觀根。

這種方法的缺點是,它僅在您通常在每次編輯后從上到下遍歷樹時才有效。

我們在 C# 和 VB 編譯器中使用后一種方法,因為這正是我們所處的情況:當我們在代碼編輯后重建解析樹時,我們可以重用前面文本中的大部分現有不可變解析樹。 我們總是從上到下遍歷樹,並且只希望在必要時計算父引用。

C# 沒有friend關鍵字,但它有一個接近的東西,稱為internal 您可以將要以有限方式公開的方法標記為內部方法,然后只有該程序集中的其他類型才能看到它們。 如果您的所有代碼都在一個程序集中,這對您沒有多大幫助,但如果將此 class 打包在一個單獨的程序集中,它將起作用。

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

這是一種可以在 C# 中模擬friend的方法:

將您的屬性標記為internal ,然后使用此屬性將它們公開給friend程序集:

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

public class MyClass {
   // code...
}

我能想到的只有兩件事:

一:

使用您上面提到的選項編號 2(我自己經常這樣做)......但使接口(項目)的實現成為 ItemsCollection 內的嵌套私有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(它確實公開了一個 setter)。 只有ItemsCollection可以執行此轉換,因為 Item 實現是ItemsCollection私有的……所以我認為這可以實現您想要的。

二:

將其設置為內部而不是私有...您不會得到您想要的,但您可以使用InternalsVisibleToAttribute來表示內部成員對另一個程序集可見。

我用來控制 class 成員的可見性的一種解決方案是將 class 定義為部分,然后在不同的命名空間中將 class 聲明為部分,並定義您想要的特殊可見性成員。

這取決於選擇的命名空間來控制成員的可見性。

您唯一需要考慮的就是引用。 它可能會變得復雜,但一旦你弄清楚了,它就會起作用。

這個場景讓我印象深刻的第一件事是,ItemCollection 和 Item 之間存在一定的功能嫉妒。 我理解您希望將子項添加到集合中並將父項設置為自主操作,但實際上我認為維護這種關系的責任在於 Item,而不是 ItemCollection。

我建議將 Item 上的 ChildItems 公開為只讀集合(可能使用IEnumerable<Item> ),並將AddChild(Item child)RemoveChild(Item child)ClearChildren()等方法放在 Item 上。 這使您有責任在不擔心泄漏到其他類的情況下使用 Item 維護父級。

您如何確保只有該項目的當前集合可以孤立該項目。 這樣,當它屬於一個集合時,沒有其他集合可以設置該項目的父項。 您可以使用某種唯一鍵,這樣第三方就無法參與其中:

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

顯然那里有冗余; 項目集合正在存儲第二個項目集合(字典)。 但是有很多選擇可以克服我認為你能想到的問題。 這里不重要。

但是,我建議您考慮將子項目管理任務移至項目 class,並盡可能保持收藏“啞”。

編輯:針對您的問題,這如何防止和 item 出現在兩個ItemsCollection中:

你問向導的意義是什么。 為什么不直接使用集合實例本身呢?

如果將 guid 參數替換為集合引用,則可以將一個項目添加到兩個不同的 collections 中,如下所示:

{
    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 方法(即使您這樣做了,您仍然無法更改項目的父項)。

您可以使用 Delegates 執行以下操作:

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

我的答案由兩部分組成

  1. 為什么擁有接口 ISetParent 如此“不安全”?

私有/內部訪問修飾符是為了防止錯誤,而不是真正的“安全代碼”。

請記住...您可以使用一些反射/調用等調用私有方法...

.

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。

PS我永遠不會知道這個編輯器是如何工作的......我應該 HTML 它,還是不應該?

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM