.NET中是否存在只读通用词典?


185

我在只读属性中返回了对字典的引用。如何防止消费者更改我的数据?如果是这样,IList我可以简单地将其退回AsReadOnly。我可以用字典做类似的事情吗?

Private _mydictionary As Dictionary(Of String, String)
Public ReadOnly Property MyDictionary() As Dictionary(Of String, String)
    Get
        Return _mydictionary
    End Get
End Property

4
必须有这样做的一些方法,否则就不会有上IDictionary的一个IsReadOnly属性...(msdn.microsoft.com/en-us/library/bb338949.aspx
Powerlord

2
无需运行时就可以实现不变性的许多概念上的好处。如果这是私人项目,请考虑采用纪律严明的非正式方法。如果必须将数据提供给使用者,则应认真考虑深拷贝。当您认为一个不可变的集合需要1)对集合的不可变的引用2)防止对序列本身进行突变,以及3)防止对集合项的属性进行修改,并且其中某些行为可能会因反射而被违反时,运行时强制性为不切实际。
斯普拉,


2
现在也有通过NuGet提供的Microsoft不可变集合msdn.microsoft.com/zh-cn/library/dn385366%28v=vs.110%29.aspx
VoteCoffee 2014年

Answers:


156

这是包装字典的简单实现:

public class ReadOnlyDictionary<TKey, TValue> : IDictionary<TKey, TValue>
{
    private readonly IDictionary<TKey, TValue> _dictionary;

    public ReadOnlyDictionary()
    {
        _dictionary = new Dictionary<TKey, TValue>();
    }

    public ReadOnlyDictionary(IDictionary<TKey, TValue> dictionary)
    {
        _dictionary = dictionary;
    }

    #region IDictionary<TKey,TValue> Members

    void IDictionary<TKey, TValue>.Add(TKey key, TValue value)
    {
        throw ReadOnlyException();
    }

    public bool ContainsKey(TKey key)
    {
        return _dictionary.ContainsKey(key);
    }

    public ICollection<TKey> Keys
    {
        get { return _dictionary.Keys; }
    }

    bool IDictionary<TKey, TValue>.Remove(TKey key)
    {
        throw ReadOnlyException();
    }

    public bool TryGetValue(TKey key, out TValue value)
    {
        return _dictionary.TryGetValue(key, out value);
    }

    public ICollection<TValue> Values
    {
        get { return _dictionary.Values; }
    }

    public TValue this[TKey key]
    {
        get
        {
            return _dictionary[key];
        }
    }

    TValue IDictionary<TKey, TValue>.this[TKey key]
    {
        get
        {
            return this[key];
        }
        set
        {
            throw ReadOnlyException();
        }
    }

    #endregion

    #region ICollection<KeyValuePair<TKey,TValue>> Members

    void ICollection<KeyValuePair<TKey, TValue>>.Add(KeyValuePair<TKey, TValue> item)
    {
        throw ReadOnlyException();
    }

    void ICollection<KeyValuePair<TKey, TValue>>.Clear()
    {
        throw ReadOnlyException();
    }

    public bool Contains(KeyValuePair<TKey, TValue> item)
    {
        return _dictionary.Contains(item);
    }

    public void CopyTo(KeyValuePair<TKey, TValue>[] array, int arrayIndex)
    {
        _dictionary.CopyTo(array, arrayIndex);
    }

    public int Count
    {
        get { return _dictionary.Count; }
    }

    public bool IsReadOnly
    {
        get { return true; }
    }

    bool ICollection<KeyValuePair<TKey, TValue>>.Remove(KeyValuePair<TKey, TValue> item)
    {
        throw ReadOnlyException();
    }

    #endregion

    #region IEnumerable<KeyValuePair<TKey,TValue>> Members

    public IEnumerator<KeyValuePair<TKey, TValue>> GetEnumerator()
    {
        return _dictionary.GetEnumerator();
    }

    #endregion

    #region IEnumerable Members

    IEnumerator IEnumerable.GetEnumerator()
    {
        return GetEnumerator();
    }

    #endregion

    private static Exception ReadOnlyException()
    {
        return new NotSupportedException("This dictionary is read-only");
    }
}

11
+1用于发布完整的代码,而不仅仅是链接,但我很好奇,ReadOnlyDictionary中的空构造函数有什么意义?:-)
塞缪尔·内夫

20
注意该构造函数。如果您对传入的字典进行参考复制,则外部代码可能会修改您的“只读”字典。您的构造函数应该对参数进行完整的深层复制。
askheaves

25
@askheaves:很好的观察,但是实际上在Read类型中使用原始引用通常很有用-将原始变量保存在私有变量中并为外部使用者修改。例如,检查内置的ReadOnlyObservableCollection或ReadOnlyCollection对象:Thomas提供的功能与.Net框架固有的功能完全相同。谢谢托马斯!+1
马特·德克里

13
@ user420667:它的实现方式,是“非只读字典的只读视图”。其他一些代码可能会更改原始词典的内容,并且这些更改将反映在只读词典中。取决于您要实现的目标,它是否可能是所需的行为……
Thomas Levesque

6
@Thomas:与.NET BCL中的ReadOnlyCollection相同。这是一个可能可变的集合的只读视图。ReadOnly并不意味着不可变,并且不应该预期不可变。
杰夫·耶茨

229

.NET 4.5

.NET Framework 4.5 BCL引入了ReadOnlyDictionary<TKey, TValue>)。

由于.NET Framework 4.5 BCL不包含AsReadOnlyfor字典,因此您需要编写自己的字典(如果需要)。它将类似于以下内容,其简单性可能凸显了为什么它不是.NET 4.5的优先事项。

public static ReadOnlyDictionary<TKey, TValue> AsReadOnly<TKey, TValue>(
    this IDictionary<TKey, TValue> dictionary)
{
    return new ReadOnlyDictionary<TKey, TValue>(dictionary);
}

.NET 4.0及以下

在.NET 4.5之前,没有.NET框架类Dictionary<TKey, TValue>ReadOnlyCollection那样包装List。但是,创建一个并不难。

这是一个示例 -如果您使用Google的ReadOnlyDictionary,还有很多其他示例


7
看起来好像他们没有记得AsReadOnly()按通常的方法创建一个方法Dictionary<,>,所以我不知道有多少人会发现他们的新类型。但是,此堆栈溢出线程会有所帮助。
杰普·斯蒂格·尼尔森

@Jeppe:我怀疑这与记忆有关。每个功能都需要花费,我怀疑AsReadOnly是否在优先级列表中居高不下,特别是因为它很容易编写。
杰夫·耶茨

1
注意,这只是一个包装器;对基础字典的更改(传递给构造函数的字典)仍会更改只读字典。另请参见stackoverflow.com/questions/139592/...
TrueWill

1
@JeffYates考虑到它的简单性,与决定是否花时间编写它相比,花费的时间更少。因此,我的赌注是“他们忘了”。
Dan Bechard

如TrueWill所述,基础词典仍然可以被突变。您可能要考虑通过原始字典的克隆构造函数,如果你想真正的不变性(假设键和值类型也是不可改变的。)
user420667


18

随意使用我的简单包装器。它没有实现IDictionary,因此它不必在运行时为将更改字典的字典方法引发异常。根本不存在更改方法。我为此创建了自己的接口,称为IReadOnlyDictionary。

public interface IReadOnlyDictionary<TKey, TValue> : IEnumerable
{
    bool ContainsKey(TKey key);
    ICollection<TKey> Keys { get; }
    ICollection<TValue> Values { get; }
    int Count { get; }
    bool TryGetValue(TKey key, out TValue value);
    TValue this[TKey key] { get; }
    bool Contains(KeyValuePair<TKey, TValue> item);
    void CopyTo(KeyValuePair<TKey, TValue>[] array, int arrayIndex);
    IEnumerator<KeyValuePair<TKey, TValue>> GetEnumerator();
}

public class ReadOnlyDictionary<TKey, TValue> : IReadOnlyDictionary<TKey, TValue>
{
    readonly IDictionary<TKey, TValue> _dictionary;
    public ReadOnlyDictionary(IDictionary<TKey, TValue> dictionary)
    {
        _dictionary = dictionary;
    }
    public bool ContainsKey(TKey key) { return _dictionary.ContainsKey(key); }
    public ICollection<TKey> Keys { get { return _dictionary.Keys; } }
    public bool TryGetValue(TKey key, out TValue value) { return _dictionary.TryGetValue(key, out value); }
    public ICollection<TValue> Values { get { return _dictionary.Values; } }
    public TValue this[TKey key] { get { return _dictionary[key]; } }
    public bool Contains(KeyValuePair<TKey, TValue> item) { return _dictionary.Contains(item); }
    public void CopyTo(KeyValuePair<TKey, TValue>[] array, int arrayIndex) { _dictionary.CopyTo(array, arrayIndex); }
    public int Count { get { return _dictionary.Count; } }
    public IEnumerator<KeyValuePair<TKey, TValue>> GetEnumerator() { return _dictionary.GetEnumerator(); }
    IEnumerator IEnumerable.GetEnumerator() { return _dictionary.GetEnumerator(); }
}

4
+1(不违反IDictionary合同)。我认为从OOP角度IDictionary继承是更正确的IReadOnlyDictionary
山姆

@Sam Agreed,如果我们可以返回,我认为拥有IDictionary(对于current IReadOnlyDictionary)和IMutableDictionary(对于current IDictionary)将是最好和最正确的。
2014年

1
@MasterMastic这是一个奇怪的主张。我不记得有任何其他内置类依赖于相反的假设,即默认情况下用户期望不可变的集合。
Dan Bechard

11

IsReadOnly on IDictionary<TKey,TValue>继承自ICollection<T>IDictionary<TKey,TValue>扩展ICollection<T>ICollection<KeyValuePair<TKey,TValue>>)。它不以任何方式使用或实现(实际上,通过使用显式实现关联的ICollection<T>成员而被“隐藏”了)。

我至少可以看到3种解决问题的方法:

  1. IDictionary<TKey, TValue>如建议的那样,实现自定义只读并包装/委托给内部字典
  2. ICollection<KeyValuePair<TKey, TValue>>设置返回 为只读或 IEnumerable<KeyValuePair<TKey, TValue>>取决于值的使用
  3. 使用copy构造函数克隆字典.ctor(IDictionary<TKey, TValue>)并返回一个副本-这样,用户可以随意使用它,并且不会影响托管源字典的对象的状态。请注意,如果要克隆的字典包含引用类型(如示例中所示不是字符串),则需要“手动”复制并克隆引用类型。

作为旁白; 公开集合时,力图公开尽可能小的接口-在示例情况下,它应该是IDictionary的,因为这使您可以在不破坏类型公开的公共契约的情况下更改基础实现。


8

只读字典在某种程度上可以被替换为Func<TKey, TValue>-如果我只希望人们执行查找,我通常在API中使用它;这很简单,尤其是如果您愿意,可以很容易地替换后端。但是,它不提供键列表。这是否重要取决于您在做什么。


4

不,但是自己动手很容易。IDictionary确实定义了IsReadOnly属性。只需包装一个Dictionary并从适当的方法中抛出NotSupportedException即可。


3

BCL中没有可用的。但是,我在BCL Extras项目中发布了ReadOnlyDictionary(名为ImmutableMap)。

除了是完全不可变的字典外,它还支持生成实现IDictionary的代理对象,并且可以在采用IDictionary的任何地方使用它。每当调用其中一个变异API时,它将引发异常

void Example() { 
  var map = ImmutableMap.Create<int,string>();
  map = map.Add(42,"foobar");
  IDictionary<int,string> dictionary = CollectionUtility.ToIDictionary(map);
}

9
您的ImmutableMap被实现为平衡树。由于在.NET中,人们通常希望通过哈希实现“字典”,并展现相应的复杂性属性,因此在将ImmutableMap提升为“字典”时,您可能要格外小心。
格伦·斯莱登

似乎code.msdn.com网站已失效。BCLextras现在在这里github.com/scottwis/tiny/tree/master/third-party/BclExtras
BozoJoe 2014年

1

您可以创建一个仅实现字典的部分实现,并隐藏所有添加/删除/设置功能的类。

在内部使用词典,外部类将所有请求传递给该词典。

但是,由于您的词典可能包含引用类型,因此您无法阻止用户设置词典所包含的类的值(除非这些类本身是只读的)


1

我认为没有简单的方法...如果您的字典是自定义类的一部分,则可以使用索引器来实现:

public class MyClass
{
  private Dictionary<string, string> _myDictionary;

  public string this[string index]
  {
    get { return _myDictionary[index]; }
  }
}

我需要能够公开整个词典以及一个索引器。
罗布·索伯斯

这似乎是一个很好的解决方案。但是,类MyClass的客户端可能需要更多地了解字典,例如,通过字典进行迭代。如果不存在密钥(可能以某种形式公开TryGetValue()是一个好主意)怎么办?您能否使答案和示例代码更完整?
Peter Mortensen

1

+1辛勤工作,托马斯。我将ReadOnlyDictionary向前走了一步。

就像戴尔的解决方案,我想删除Add()Clear()Remove()从智能感知等。但是我希望我的派生对象得以实现IDictionary<TKey, TValue>

此外,我想破坏以下代码:(同样,Dale的解决方案也这样做)

ReadOnlyDictionary<int, int> test = new ReadOnlyDictionary<int,int>(new Dictionary<int, int> { { 1, 1} });
test.Add(2, 1);  //CS1061

Add()行导致:

error CS1061: 'System.Collections.Generic.ReadOnlyDictionary<int,int>' does not contain a definition for 'Add' and no extension method 'Add' accepting a first argument 

调用方仍然可以将其强制转换为IDictionary<TKey, TValue>,但是NotSupportedException如果您尝试使用非只读成员(来自Thomas的解决方案),则会引发。

无论如何,这是我想要的任何人的解决方案:

namespace System.Collections.Generic
{
    public class ReadOnlyDictionary<TKey, TValue> : IDictionary<TKey, TValue>
    {
        const string READ_ONLY_ERROR_MESSAGE = "This dictionary is read-only";

        protected IDictionary<TKey, TValue> _Dictionary;

        public ReadOnlyDictionary()
        {
            _Dictionary = new Dictionary<TKey, TValue>();
        }

        public ReadOnlyDictionary(IDictionary<TKey, TValue> dictionary)
        {
            _Dictionary = dictionary;
        }

        public bool ContainsKey(TKey key)
        {
            return _Dictionary.ContainsKey(key);
        }

        public ICollection<TKey> Keys
        {
            get { return _Dictionary.Keys; }
        }

        public bool TryGetValue(TKey key, out TValue value)
        {
            return _Dictionary.TryGetValue(key, out value);
        }

        public ICollection<TValue> Values
        {
            get { return _Dictionary.Values; }
        }

        public TValue this[TKey key]
        {
            get { return _Dictionary[key]; }
            set { throw new NotSupportedException(READ_ONLY_ERROR_MESSAGE); }
        }

        public bool Contains(KeyValuePair<TKey, TValue> item)
        {
            return _Dictionary.Contains(item);
        }

        public void CopyTo(KeyValuePair<TKey, TValue>[] array, int arrayIndex)
        {
            _Dictionary.CopyTo(array, arrayIndex);
        }

        public int Count
        {
            get { return _Dictionary.Count; }
        }

        public bool IsReadOnly
        {
            get { return true; }
        }

        public IEnumerator<KeyValuePair<TKey, TValue>> GetEnumerator()
        {
            return _Dictionary.GetEnumerator();
        }

        IEnumerator IEnumerable.GetEnumerator()
        {
            return (_Dictionary as IEnumerable).GetEnumerator();
        }

        void IDictionary<TKey, TValue>.Add(TKey key, TValue value)
        {
            throw new NotSupportedException(READ_ONLY_ERROR_MESSAGE);
        }

        bool IDictionary<TKey, TValue>.Remove(TKey key)
        {
            throw new NotSupportedException(READ_ONLY_ERROR_MESSAGE);
        }

        void ICollection<KeyValuePair<TKey, TValue>>.Add(KeyValuePair<TKey, TValue> item)
        {
            throw new NotSupportedException(READ_ONLY_ERROR_MESSAGE);
        }

        void ICollection<KeyValuePair<TKey, TValue>>.Clear()
        {
            throw new NotSupportedException(READ_ONLY_ERROR_MESSAGE);
        }

        bool ICollection<KeyValuePair<TKey, TValue>>.Remove(KeyValuePair<TKey, TValue> item)
        {
            throw new NotSupportedException(READ_ONLY_ERROR_MESSAGE);
        }
    }
}


0
public IEnumerable<KeyValuePair<string, string>> MyDictionary()
{
    foreach(KeyValuePair<string, string> item in _mydictionary)
        yield return item;
}

2
或者,你可以这样做:public IEnumerable<KeyValuePair<string, string>> MyDictionary() { return _mydictionary; }

0

这是一个不好的解决方案,请参阅底部。

对于那些仍在使用.NET 4.0或更早版本的人,我有一个可以像接受答案中的类一样工作的类,但是它的长度要短得多。它扩展了现有的Dictionary对象,覆盖(实际上隐藏了)某些成员,以使它们在调用时引发异常。

如果调用方尝试调用“内置字典”所具有的“添加”,“删除”或其他某些变异操作,则编译器将引发错误。我使用过时属性来引发这些编译器错误。这样,您可以用此ReadOnlyDictionary替换Dictionary,并立即查看可能出现的问题,而不必运行应用程序并等待运行时异常。

看一看:

public class ReadOnlyException : Exception
{
}

public class ReadOnlyDictionary<TKey, TValue> : Dictionary<TKey, TValue>
{
    public ReadOnlyDictionary(IDictionary<TKey, TValue> dictionary)
        : base(dictionary) { }

    public ReadOnlyDictionary(IDictionary<TKey, TValue> dictionary, IEqualityComparer<TKey> comparer)
        : base(dictionary, comparer) { }

    //The following four constructors don't make sense for a read-only dictionary

    [Obsolete("Not Supported for ReadOnlyDictionaries", true)]
    public ReadOnlyDictionary() { throw new ReadOnlyException(); }

    [Obsolete("Not Supported for ReadOnlyDictionaries", true)]
    public ReadOnlyDictionary(IEqualityComparer<TKey> comparer) { throw new ReadOnlyException(); }

    [Obsolete("Not Supported for ReadOnlyDictionaries", true)]
    public ReadOnlyDictionary(int capacity) { throw new ReadOnlyException(); }

    [Obsolete("Not Supported for ReadOnlyDictionaries", true)]
    public ReadOnlyDictionary(int capacity, IEqualityComparer<TKey> comparer) { throw new ReadOnlyException(); }


    //Use hiding to override the behavior of the following four members
    public new TValue this[TKey key]
    {
        get { return base[key]; }
        //The lack of a set accessor hides the Dictionary.this[] setter
    }

    [Obsolete("Not Supported for ReadOnlyDictionaries", true)]
    public new void Add(TKey key, TValue value) { throw new ReadOnlyException(); }

    [Obsolete("Not Supported for ReadOnlyDictionaries", true)]
    public new void Clear() { throw new ReadOnlyException(); }

    [Obsolete("Not Supported for ReadOnlyDictionaries", true)]
    public new bool Remove(TKey key) { throw new ReadOnlyException(); }
}

此解决方案有一个@supercat指出的问题,如下所示:

var dict = new Dictionary<int, string>
{
    { 1, "one" },
    { 2, "two" },
    { 3, "three" },
};

var rodict = new ReadOnlyDictionary<int, string>(dict);
var rwdict = rodict as Dictionary<int, string>;
rwdict.Add(4, "four");

foreach (var item in rodict)
{
    Console.WriteLine("{0}, {1}", item.Key, item.Value);
}

而不是像我期望的那样给出编译时错误,或者像我希望的那样给出运行时异常,该代码可以正确运行。它打印四个数字。这使我的ReadOnlyDictionary成为ReadWriteDictionary。


这种方法的问题在于,此类对象可以传递给期望a的方法,Dictionary<TKey,TValue>而不会引起任何编译器投诉,并且强制转换或强制引用纯字典类型的引用将删除所有保护。
2015年

@supercat,胡扯,你是对的。我以为我也有很好的解决方案。
user2023861 2015年

我记得DictionaryClone链接到的方法制作的派生形式MemberwiseClone。不幸的是,尽管应该可以通过克隆后备存储库来有效地克隆字典,但事实是,后备存储库不是private而不是protected意味着派生类无法克隆它们。使用MemberwiseClone而不克隆后备存储将意味着对原始字典进行的后续修改将破坏克隆,而对克隆进行的修改将破坏原始副本。
2015年
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.