问题
First let me explain why I'm using a KeyedCollection. I'm building a DLL and I have a list of items that I need to add to a collection and have them stay in the order I placed them but I also need to access them by both their index and by key (the key is a property of an object which I already defined). If there is any other simpler collection that does this, then please let me know.
Ok now, I need to be able to add items to this collection internally in the DLL but I need it to be publicly available to the DLL's end-user as read-only because I don't want them removing/altering the items I added.
I've searched all over this site, other sites, google in general and I have not been able to find a way to get some sort of read-only KeyedCollection. The closest I came was this page (http://www.koders.com/csharp/fid27249B31BFB645825BD9E0AFEA6A2CCDDAF5A382.aspx?s=keyedcollection#L28) but I couldn't quite get it to work.
UPDATE:
I took a look at those C5 classes. That, along with your other comments, helped me better understand how to create my own read-only class and it seems to work. However I have a problem when I try to cast the regular one to the read-only one. I get a compile-time cannot convert error. Here's the code I created (the first small class is what I originally had):
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."); }
}
If I have this variable defined:
private FieldCollection _fields;
then do this:
public ReadOnlyFieldCollection Fields;
Fields = (ReadOnlyFieldCollection)_fields;
it fails to compile. They're both inheriting from the same class, I thought they would be "compatible". How can I cast (or expose) the collection as the read-only type I just created?
回答1:
I don't know of any built-in solution as well. This is an example of the suggestion I gave on the comment:
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
{
//...
}
}
}
You'll be able to iterate through the KeyValuePair list and everything else, but it won't be available for writing operations. Just be sure to type it according to your needs.
EDIT
In order to have a sorted list, we can change the base class from Dictionary<T,T>
to a Hashtable
.
It would look like this:
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?
}
}
}
Usage:
LockedKeyCollection aLockedList = new LockedKeyCollection();
foreach (System.Collections.DictionaryEntry entry in aLockedList)
{
//entry.Key
//entry.Value
}
Unfortunately, we can't change the access modifier of the methods, but we can override them to do nothing.
回答2:
Edited to revise my original answer. Apparently, I've not had enough caffiene this AM.
You should look at the C5 collections library which offers read-only wrappers for collections and dictionaries, along with other stuff that the CLR team forgot:
http://www.itu.dk/research/c5/
You can also get the C5 Collections library via NuGet at http://www.nuget.org/packages/C5
回答3:
There are two additional options available to you, which might better suit similar needs in the future. The first option is the simplest and requires the least amount of code; the second requires more scaffolding, but is optimal for sharing read-only members with an existing KeyedCollection<TKey, TValue>
implementation.
Option 1: Derive from ReadOnlyCollection<TValue>
In this approach, you derive from ReadOnlyCollection
and then add the this[TKey key]
indexer. This is similar to the approach that @AndreCalil took above, except that ReadOnlyCollection
already throws a NotSupportedException
for each of the writable entry points, so you're covered there.
This is the approach Microsoft used internally with their ReadOnlyKeyedCollection<TKey, TValue>
in the System.ServiceModel.Internals
assembly (source):
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];
}
}
}
Option 2: Decorate an existing KeyedCollection<TKey, TValue>
The only real downside of the ReadOnlyCollection<TValue>
approach is that it doesn't inherit any logic from your KeyedCollection<TKey, TValue>
implementation. If you have a lot of custom logic, then you might instead choose to use a Decorator Pattern on your existing implementation. This would look something like:
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
}
That's obviously a lot more code to write, however, and really only makes sense if you have a fair amount of custom code on your existing KeyedCollection<TKey, TItem>
interface; otherwise, the first approach makes a lot more sense. Given that, it may instead be preferable to centralize that logic via either extension methods or, in C# 8.0, default interface methods.
回答4:
It's never too late for an answer!
The simplest path:
IReadOnlyCollection<T>
- the minimal setup to get it to work, i.e. no comparer and internal dictionary
Code:
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);
}
}
来源:https://stackoverflow.com/questions/11908527/how-can-i-get-a-keyedcollection-to-be-read-only