首先让我解释一下为什么我使用KeyedCollection。我正在构建一个 DLL,我有一个项目列表,我需要添加到集合中并让它们保持我放置它们的顺序,但我还需要通过它们的索引和键访问它们(键是我已经定义的对象的属性)。如果还有其他更简单的集合可以做到这一点,那么请告诉我。
好的,现在,我需要能够在 DLL 内部向此集合添加项,但我需要它以只读形式公开提供给 DLL 的最终用户,因为我不希望他们删除/更改我添加的项目。
我已经搜索了这个网站,其他网站,谷歌,但我无法找到一种方法来获得某种只读的KeyedCollection。我最接近的是这个页面(http://www.koders.com/csharp/fid27249B31BFB645825BD9E0AFEA6A2CCDDAF5A382.aspx?s=keyedcollection#L28),但我无法完全让它工作。
更新:
我看了一下那些C5课程。这与您的其他评论一起,帮助我更好地了解如何创建自己的只读类,并且它似乎有效。但是,当我尝试将常规的转换为只读时,我遇到了问题。我得到一个编译时无法转换的错误。这是我创建的代码(第一个小类是我最初拥有的代码):
public class FieldCollection : KeyedCollection<string, Field>
{
protected override string GetKeyForItem(Field field)
{
return field.Name;
}
}
public class ReadOnlyFieldCollection : KeyedCollection<string, Field>
{
protected override string GetKeyForItem(Field field)
{ return field.Name; }
new public void Add(Field field)
{ throw new ReadOnlyCollectionException("This collection is read-only."); }
new public void Clear()
{ throw new ReadOnlyCollectionException("This collection is read-only."); }
new public void Insert(int index, Field field)
{ throw new ReadOnlyCollectionException("This collection is read-only."); }
new public bool Remove(string key)
{ throw new ReadOnlyCollectionException("This collection is read-only."); }
new public bool Remove(Field field)
{ throw new ReadOnlyCollectionException("This collection is read-only."); }
new public bool RemoveAt(int index)
{ throw new ReadOnlyCollectionException("This collection is read-only."); }
}
如果我定义了这个变量:
private FieldCollection _fields;
然后这样做:
public ReadOnlyFieldCollection Fields;
Fields = (ReadOnlyFieldCollection)_fields;
它无法编译。它们都继承自同一个类,我认为它们是"兼容的"。如何将集合强制转换(或公开)为刚创建的只读类型?
不知道任何内置解决方案。这是我在评论中给出的建议的一个例子:
public class LockedDictionary : Dictionary<string, string>
{
public override void Add(string key, string value)
{
//do nothing or log it somewhere
//or simply change it to private
}
//and so on to Add* and Remove*
public override string this[string i]
{
get
{
return base[i];
}
private set
{
//...
}
}
}
您将能够循环访问 KeyValuePair 列表和其他所有内容,但它不可用于写入操作。请务必根据需要键入它。
编辑为了获得排序列表,我们可以将基类从 Dictionary<T,T>
更改为 Hashtable
.
它看起来像这样:
public class LockedKeyCollection : System.Collections.Hashtable
{
public override void Add(object key, object value)
{
//do nothing or throw exception?
}
//and so on to Add* and Remove*
public override object this[object i]
{
get
{
return base[i];
}
set
{
//do nothing or throw exception?
}
}
}
用法:
LockedKeyCollection aLockedList = new LockedKeyCollection();
foreach (System.Collections.DictionaryEntry entry in aLockedList)
{
//entry.Key
//entry.Value
}
不幸的是,我们无法更改方法的访问修饰符,但我们可以覆盖它们而不执行任何操作。
编辑以修改我的原始答案。显然,我今天早上没有足够的咖啡。
您应该查看 C5 集合库,它为集合和字典提供了只读包装器,以及 CLR 团队忘记的其他内容:
http://www.itu.dk/research/c5/
还可以通过 NuGet 获取 C5 集合库,网址为 http://www.nuget.org/packages/C5
还有两个额外的选项可供您使用,它们可能更适合将来的类似需求。第一个选项是最简单的,需要的代码量最少;第二个需要更多的基架,但最适合与现有KeyedCollection<TKey, TValue>
实现共享只读成员。
选项 1:从ReadOnlyCollection<TValue>
派生
在此方法中,从ReadOnlyCollection
派生,然后添加this[TKey key]
索引器。这与@AndreCalil上面采用的方法类似,只是ReadOnlyCollection
已经为每个可写入口点抛出了一个NotSupportedException
,所以你在那里被覆盖了。
这是Microsoft内部使用的方法,System.ServiceModel.Internals
程序集中的ReadOnlyKeyedCollection<TKey, TValue>
(源):
class ReadOnlyKeyedCollection<TKey, TValue> : ReadOnlyCollection<TValue> {
KeyedCollection<TKey, TValue> innerCollection;
public ReadOnlyKeyedCollection(KeyedCollection<TKey, TValue> innerCollection) : base(innerCollection) {
Fx.Assert(innerCollection != null, "innerCollection should not be null");
this.innerCollection = innerCollection;
}
public TValue this[TKey key] {
get {
return this.innerCollection[key];
}
}
}
选项 2:装饰现有KeyedCollection<TKey, TValue>
ReadOnlyCollection<TValue>
方法的唯一真正缺点是它不会从KeyedCollection<TKey, TValue>
实现继承任何逻辑。如果您有很多自定义逻辑,则可以选择在现有实现上使用装饰器模式。这看起来像这样:
class MyReadOnlyKeyedCollection : MyKeyedCollection, IReadOnlyCollection<TValue> {
MyKeyedCollection innerCollection;
public MyReadOnlyKeyedCollection(MyKeyedCollection innerCollection) : base() {
this.innerCollection = innerCollection;
}
protected override InsertItem(Int32 index, TItem item) {
throw new NotSupportedException("This is a read only collection");
}
//Repeat for ClearItems(), RemoveItem(), and SetItem()
public override void YourWriteMethod() {
throw new NotSupportedException("This is a read only collection");
}
//Repeat for any other custom writable members
protected override IList<T> Items {
get {
return.innerCollection.ToList();
}
}
//This should cover all other entry points for read operations
public override Object YourReadMethod() {
this.innerCollection.YourReadMethod();
}
//Repeat for any other custom read-only members
}
然而,这显然需要编写更多的代码,并且只有在现有KeyedCollection<TKey, TItem>
接口上有相当数量的自定义代码时才有意义;否则,第一种方法更有意义。鉴于此,最好通过扩展方法或 C# 8.0 中的默认接口方法集中该逻辑。
答案永远不会太晚!
最简单的路径:
-
IReadOnlyCollection<T>
- 使其工作的最小设置,即没有比较器和内部字典
法典:
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using JetBrains.Annotations;
namespace Z
{
public abstract class ReadOnlyKeyedCollection<TKey, TValue> : IReadOnlyCollection<TValue>
{
private readonly IReadOnlyCollection<TValue> _collection;
protected ReadOnlyKeyedCollection([NotNull] IReadOnlyCollection<TValue> collection)
{
_collection = collection ?? throw new ArgumentNullException(nameof(collection));
}
public TValue this[[NotNull] TKey key]
{
get
{
if (key == null)
throw new ArgumentNullException(nameof(key));
foreach (var item in _collection)
{
var itemKey = GetKeyForItem(item);
if (Equals(key, itemKey))
return item;
}
throw new KeyNotFoundException();
}
}
#region IReadOnlyCollection<TValue> Members
IEnumerator IEnumerable.GetEnumerator()
{
return GetEnumerator();
}
public IEnumerator<TValue> GetEnumerator()
{
return _collection.GetEnumerator();
}
public int Count => _collection.Count;
#endregion
public bool Contains([NotNull] TKey key)
{
if (key == null)
throw new ArgumentNullException(nameof(key));
return _collection.Select(GetKeyForItem).Contains(key);
}
protected abstract TKey GetKeyForItem(TValue item);
}
}