在C#4.0中,有没有办法让一个类的私有成员只能用于特定的其他类?

我们正在创建一个对象层次结构,其中每个项目都有一个其他项目的集合,每个项目也有一个指向其父项目的Parent属性。 很标准的东西。 我们还有一个ItemsCollection类,它inheritance自Collection ,它本身有一个Owner属性,指向该集合所属的项目。 再一次,没有什么有趣的。

当一个项目被添加到ItemsCollection类时,我们希望它自动设置Item的父项(使用集合的Owner属性),当项目被删除时,我们想要清除父项。

这就是事情。 我们只希望Parent setter可用于ItemsCollection ,没有别的。 这样,我们不仅可以知道项目的父项是谁,还可以通过检查Parent的现有值或者让某人随意将其更改为其他内容来确保项目不会添加到多个集合中。

我们知道如何做到的两种方式是:

  1. 将setter标记为private,然后将集合定义包含在项本身的范围内。 亲:全面保护。 Con:嵌套类的丑陋代码。

  2. 在Item上使用私有ISetParent接口,只有ItemsCollection知道。 亲:更清晰的代码,易于遵循。 Con:从技术上讲,任何知道界面的人都可以投射Item并获得setter。

现在技术上通过反思任何人都可以得到任何东西,但仍然……试图找到最好的方法来做到这一点。

现在我知道C ++中有一个名为Friend的function,或者让你在一个类中指定一个私有成员可用于另一个类的完美场景,但我不知道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 { 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:

例如,你可以说有一个叫做“宇宙”的特殊项目,它是除了它自己以外的每个项目的祖先。 “宇宙”可以是存储在众所周知的位置的单身人士。 当您向项目询问其父项时,实现可以找到该Universe,然后搜索Universe的每个后代以查找该项目,并随时跟踪该路径。 当你找到这个项目时,很棒,你已经完成了。 你看起来又向前走了一条“路径”,那里有你的父母。 更好的是,如果你愿意,你可以提供整个父母 ; 毕竟,你只是计算它。

技术#2:

如果宇宙很大并且需要一段时间才能找到每个项目,这可能会很昂贵。 另一种解决方案是让Universe包含将项目映射到其父项的哈希表,以及将项目映射到其子项列表的第二个哈希表。 当你将child x添加到父y时,“add”方法实际上调用Universe并说“嘿,项目x现在是y的父级”,并且Universe负责更新哈希表。 项目不包含任何自己的“连通性”信息; 这是宇宙执行的责任。

另一方面,宇宙有可能包含循环; 你可以告诉宇宙x是y的父级,y是x的父级。 如果你想避免这种情况,那么你必须编写一个循环检测器。

技术#3:

你可以说有两棵树; “真正的”树和“门面”树。 真正的树是不可变的和持久的 。 在真正的树中,每个项目都知道它的子项而不是它的父项。 一旦构建了不可变的真实树,就可以创建一个Facade节点,它是真实树的根的代理。 当您向其子节点请求该节点时,它会围绕每个子节点创建一个新的Facade节点,并将facade节点的父属性设置为为其子节点查询的节点。

现在,您可以将外观树视为父级树,但父关系仅在遍历树时计算。

当您想要编辑树时,您将生成一个新的真实树,尽可能多地重用旧的真实树。 然后,您创建一个新的Facade根。

这种方法的缺点是,只有在每次编辑后通常从顶部向下遍历树时,它才有效。

我们在C#和VB编译器中使用后一种方法,因为这正是我们所处的情况:当我们在代码编辑后重建一个解析树时,我们可以重用前面文本中的大部分现有的不可变解析树。 我们总是从上到下遍历树,只想在必要时计算父引用。

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

这是一种可以在C#中模拟friend的方法:

internal标记您的属性,然后使用此属性将它们公开给friend程序集:

 [assembly: InternalsVisibleTo("Friend1, PublicKey=002400000480000094" + "0000000602000000240000525341310004000" + "001000100bf8c25fcd44838d87e245ab35bf7" + "3ba2615707feea295709559b3de903fb95a93" + "3d2729967c3184a97d7b84c7547cd87e435b5" + "6bdf8621bcb62b59c00c88bd83aa62c4fcdd4" + "712da72eec2533dc00f8529c3a0bbb4103282" + "f0d894d5f34e9f0103c473dce9f4b457a5dee" + "fd8f920d8681ed6dfcb0a81e96bd9b176525a" + "26e0b3")] public class MyClass { // code... } 

我能想到的唯一两件事:

一:

使用上面提到的2号选项(我自己不断做)…但是让接口(Item)的实现成为ItemsCollection的嵌套私有类…这样只有ItemsCollection知道setter。 接口IItem仅为Parent声明一个getter,并且没有人可以将它ItemsCollection为Item,因为Item对ItemsCollection是私有的。 所以,像:

 public class ItemsCollection : ObservableCollection { 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来表示内部成员对另一个程序集可见。

这个场景让我印象深刻的第一件事就是ItemCollection和Item之间有明确的特征羡慕。 我理解你希望将子项添加到集合中并将父项设置为自主操作,但实际上我认为维护该关系的责任在于Item,而不是ItemCollection。

我建议将Item上的ChildItems公开为只读集合(可能使用IEnumerable ),并在AddChild(Item child)放置AddChild(Item child)RemoveChild(Item child)ClearChildren()等方法。 这使得负责维护父项与您没有关注的项目泄漏到其他类。

如何确保只有项目的当前集合可以孤立该项目。 这样,当项目属于集合时,没有其他集合可以设置项目的父项。 您可以使用某种独特的密钥,以便第三方无法参与:

 public sealed class ItemsCollection : ObservableCollection { private Dictionary guids = new Dictionary(); 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

你问guids的重点是什么。 为什么不直接使用集合实例呢?

如果用集合引用替换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方法(即使您这样做,您仍然无法更改项目的父级)。

你可以使用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 { 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如此“不安全”?

私有/内部访问修饰符用于防止错误,而不是真正的“安全代码”。

记住……你可以使用一些Reflections / Invoke等来调用私有方法……

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它,还是我不应该?

我用来控制类成员可见性的一个解决方案是将类定义为partial,然后在不同的名称空间中将该类声明为partial,并定义所需的特殊可见性成员。

这将根据所选的命名空间控制成员可见性。

你唯一需要包围的就是参考。 它可能变得复杂,但是一旦你弄明白,它就可以了。