C#有提供一种不可变的字典吗?

75

核心C#库中是否有任何内置内容可以给我提供一个不可变的字典?

类似于Java的一些内容:

Collections.unmodifiableMap(myMap);

仅想澄清的是,我并不希望停止更改字典中的键/值本身,而是希望停止更改字典的结构。我需要一些东西,如果调用IDictionary的任何mutator方法(Add,Remove,Clear)会迅速且明确地失败。


6
似乎在 .Net 4.5 版本中将会添加 ReadOnlyDictionary<TKey,TValue>,与自 .Net 2.0 起就存在的 ReadOnlyCollection<T> 并列。详情请参考 http://msdn.microsoft.com/en-us/magazine/jj133817.aspx 。 - Gordon Gustafson
14个回答

52
不需要,但是包装器相当简单:
public class ReadOnlyDictionary<TKey, TValue> : IDictionary<TKey, TValue>
{
    IDictionary<TKey, TValue> _dict;

    public ReadOnlyDictionary(IDictionary<TKey, TValue> backingDict)
    {
        _dict = backingDict;
    }

    public void Add(TKey key, TValue value)
    {
        throw new InvalidOperationException();
    }

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

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

    public bool Remove(TKey key)
    {
        throw new InvalidOperationException();
    }

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

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

    public TValue this[TKey key]
    {
        get { return _dict[key]; }
        set { throw new InvalidOperationException(); }
    }

    public void Add(KeyValuePair<TKey, TValue> item)
    {
        throw new InvalidOperationException();
    }

    public void Clear()
    {
        throw new InvalidOperationException();
    }

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

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

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

    public bool IsReadOnly
    {
        get { return true; }
    }

    public bool Remove(KeyValuePair<TKey, TValue> item)
    {
        throw new InvalidOperationException();
    }

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

    System.Collections.IEnumerator 
           System.Collections.IEnumerable.GetEnumerator()
    {
        return ((System.Collections.IEnumerable)_dict).GetEnumerator();
    }
}

显然,如果你想允许修改值,可以修改上面的this[] setter函数。

3
你没有实现相等性检查,这对于不可变数据结构来说是一个非常重要的特性。 - Elazar Leibovich
2
看起来你只是拿了一个标准的字典,然后到处抛出异常...?“不可变”并不意味着“无用”。相反地,它非常有用。 - Richard Berg
6
这里有很多恶意,明明发布者想表达的是“只读”而不是“不可变”。我认为这个包装器可能符合他的需求,因此得分很高。我很高兴大家提出了更多理论性的观点,但我们不要忘记原帖作者实际需要什么。 - Egor
13
@其他人:如果你像我一样好奇,似乎有些人区分“只读”和“不可变”。一个“不可变”的字典可以有一个Add方法,它返回一个新的带有该元素添加的字典,而“只读”的字典则没有这个方法。获取一个有用的“只读”字典实例的唯一方法是构建一个普通字典,然后为其构造一个“只读”封装器。 - Stefan Monov
5
@Stefan: 我所看到的区别并不在于是否有任何方法可以从现有集合构造新集合。像这个答案中的集合具有只读接口:任何传递此实例的人都不能修改它,但不能保证没有人可以修改它(拥有原始backingDict的人可以修改集合)。另一方面,不可变集合则保证不会被任何人修改。 - Joren
显示剩余14条评论


16

.NET 4.5发布后,出现了一个新的ReadOnlyDictionary类。你只需将IDictionary传递给构造函数即可创建不可变字典。

这里有一个有用的扩展方法,可用于简化创建只读字典的过程。


1
此外,微软推出了一个名为Immutable collections的新附加组件(nuGet)库。 - sinelaw
ImmutableDictionary (来自不可变集合) 和此答案中提到的 ReadOnlyDictionary 的区别在于,你可以获得一个包含所需更改的新 ImmutableDictionary(而不是对现有对象进行修改)。 - sinelaw
警告!ReadOnlyDictionary 只能在创建时捕获底层字典属性的值。请查看http://www.codeproject.com/Tips/1103307/Use-of-IReadOnlyDictionary-and-IReadOnlyList-prope以获取更多信息。 - Michael Erickson

4

我知道这是一个非常老的问题,但是在2020年我某种方式找到了它,因此我认为值得注意的是,现在有一种创建不可变字典的方法:

https://learn.microsoft.com/en-us/dotnet/api/system.collections.immutable.immutabledictionary.toimmutabledictionary?view=netcore-3.1

用法:

using System.Collections.Immutable;

public MyClass {
    private Dictionary<KeyType, ValueType> myDictionary;

    public ImmutableDictionary<KeyType, ValueType> GetImmutable()
    {
        return myDictionary.ToImmutableDictionary();
    }
}

3

dbkk的答案的基础上,我希望能够在首次创建ReadOnlyDictionary时使用对象初始化器。我进行了以下修改:

private readonly int _finalCount;

/// <summary>
/// Takes a count of how many key-value pairs should be allowed.
/// Dictionary can be modified to add up to that many pairs, but no
/// pair can be modified or removed after it is added.  Intended to be
/// used with an object initializer.
/// </summary>
/// <param name="count"></param>
public ReadOnlyDictionary(int count)
{
    _dict = new SortedDictionary<TKey, TValue>();
    _finalCount = count;
}

/// <summary>
/// To allow object initializers, this will allow the dictionary to be
/// added onto up to a certain number, specifically the count set in
/// one of the constructors.
/// </summary>
/// <param name="key"></param>
/// <param name="value"></param>
public void Add(TKey key, TValue value)
{
    if (_dict.Keys.Count < _finalCount)
    {
        _dict.Add(key, value);
    }
    else
    {
        throw new InvalidOperationException(
            "Cannot add pair <" + key + ", " + value + "> because " +
            "maximum final count " + _finalCount + " has been reached"
        );
    }
}

现在我可以像这样使用该类:
ReadOnlyDictionary<string, string> Fields =
    new ReadOnlyDictionary<string, string>(2)
        {
            {"hey", "now"},
            {"you", "there"}
        };

3
开源的PowerCollections库包含一个只读的字典包装器(以及几乎所有其他东西的只读包装器),可以通过Algorithms类上的静态ReadOnly()方法访问。

2
一种解决方法可能是,从字典中抛出一个新的KeyValuePair列表以保持原始未修改。
var dict = new Dictionary<string, string>();

dict.Add("Hello", "World");
dict.Add("The", "Quick");
dict.Add("Brown", "Fox");

var dictCopy = dict.Select(
    item => new KeyValuePair<string, string>(item.Key, item.Value));

// returns dictCopy;

这样原始字典就不会被修改。

2

我不这么认为。有一种方法可以创建只读列表和只读集合,但我不认为内置了只读字典。System.ServiceModel有一个ReadOnlyDictinoary实现,但它是内部的。不过,使用反射器很容易复制它,或者从头开始创建自己的只读字典。它基本上包装了一个字典,并在调用修改器时抛出异常。


正如Dylan在下面的答案中提到的那样,自.NET 4.5以来,已经有了内置的解决方案。 - sinelaw

1
通常来说,如果没有必要的话最好不要在第一时间传递任何字典。

相反,创建一个域对象,其接口不提供任何修改包装的字典的方法。而是提供所需的LookUp方法,该方法通过键从字典中检索元素(额外的好处是它比字典更易于使用)。

public interface IMyDomainObjectDictionary 
{
    IMyDomainObject GetMyDomainObject(string key);
}

internal class MyDomainObjectDictionary : IMyDomainObjectDictionary 
{
    public IDictionary<string, IMyDomainObject> _myDictionary { get; set; }
    public IMyDomainObject GetMyDomainObject(string key)         {.._myDictionary .TryGetValue..etc...};
}

1
你可以尝试这样做:
private readonly Dictionary<string, string> _someDictionary;

public IEnumerable<KeyValuePair<string, string>> SomeDictionary
{
    get { return _someDictionary; }
}

这将解决可变性问题,而让您的调用者必须将其转换为自己的字典:

foo.SomeDictionary.ToDictionary(kvp => kvp.Key);

...或者使用键的比较运算符,而不是索引查找,例如:

foo.SomeDictionary.First(kvp => kvp.Key == "SomeKey");

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接