.NET 字典中的重复键?

Posted

技术标签:

【中文标题】.NET 字典中的重复键?【英文标题】:Duplicate keys in .NET dictionaries? 【发布时间】:2010-09-13 20:34:08 【问题描述】:

.NET 基类库中是否有允许使用重复键的字典类?我发现的唯一解决方案是创建一个类,例如:

Dictionary<string, List<object>>

但这对于实际使用来说是相当烦人的。在 Java 中,我相信 MultiMap 可以做到这一点,但在 .NET 中找不到类似物。

【问题讨论】:

这个重复键是怎么回事,它是重复值(列表),对吧? @ShamimHafiz,不,这些值不必重复。如果您必须将重复的 a, 1 a, 2 存储在以 a 为键的哈希表中,另一种方法是使用 a, [1, 2] 实际上,我相信这里真正想要的是一个集合,其中每个键都可以映射到一个或多个值。我认为“重复键”这个表述并没有真正传达这一点。 为了将来参考,您应该考虑保留 1 个键,只是将值添加到其中,而不是一遍又一遍地添加相同的键。 如果键和值都是字符串,则有NameValueCollection(可以将多个字符串值与每个字符串键关联)。 【参考方案1】:

由于新的 C#(我相信它是从 7.0 开始的),您还可以执行以下操作:

var duplicatedDictionaryExample = new List<(string Key, string Value)>  ("", "") ... 

您将它用作标准列表,但有两个值可以随意命名

foreach(var entry in duplicatedDictionaryExample)
 
    // do something with the values
    entry.Key;
    entry.Value;

【讨论】:

【参考方案2】:

您可以创建自己的字典包装器,就像这样,作为奖励,它支持空值作为键:

/// <summary>
/// Dictionary which supports duplicates and null entries
/// </summary>
/// <typeparam name="TKey">Type of key</typeparam>
/// <typeparam name="TValue">Type of items</typeparam>
public class OpenDictionary<TKey, TValue>

    private readonly Lazy<List<TValue>> _nullStorage = new Lazy<List<TValue>>(
        () => new List<TValue>());

    private readonly Dictionary<TKey, List<TValue>> _innerDictionary =
        new Dictionary<TKey, List<TValue>>();

    /// <summary>
    /// Get all entries
    /// </summary>
    public IEnumerable<TValue> Values =>
        _innerDictionary.Values
            .SelectMany(x => x)
            .Concat(_nullStorage.Value);

    /// <summary>
    /// Add an item
    /// </summary>
    public OpenDictionary<TKey, TValue> Add(TKey key, TValue item)
    
        if (ReferenceEquals(key, null))
            _nullStorage.Value.Add(item);
        else
        
            if (!_innerDictionary.ContainsKey(key))
                _innerDictionary.Add(key, new List<TValue>());

            _innerDictionary[key].Add(item);
        

        return this;
    

    /// <summary>
    /// Remove an entry by key
    /// </summary>
    public OpenDictionary<TKey, TValue> RemoveEntryByKey(TKey key, TValue entry)
    
        if (ReferenceEquals(key, null))
        
            int targetIdx = _nullStorage.Value.FindIndex(x => x.Equals(entry));
            if (targetIdx < 0)
                return this;

            _nullStorage.Value.RemoveAt(targetIdx);
        
        else
        
            if (!_innerDictionary.ContainsKey(key))
                return this;

            List<TValue> targetChain = _innerDictionary[key];
            if (targetChain.Count == 0)
                return this;

            int targetIdx = targetChain.FindIndex(x => x.Equals(entry));
            if (targetIdx < 0)
                return this;

            targetChain.RemoveAt(targetIdx);
        

        return this;
    

    /// <summary>
    /// Remove all entries by key
    /// </summary>
    public OpenDictionary<TKey, TValue> RemoveAllEntriesByKey(TKey key)
    
        if (ReferenceEquals(key, null))
        
            if (_nullStorage.IsValueCreated)
                _nullStorage.Value.Clear();
               
        else
        
            if (_innerDictionary.ContainsKey(key))
                _innerDictionary[key].Clear();
        

        return this;
    

    /// <summary>
    /// Try get entries by key
    /// </summary>
    public bool TryGetEntries(TKey key, out IReadOnlyList<TValue> entries)
    
        entries = null;

        if (ReferenceEquals(key, null))
        
            if (_nullStorage.IsValueCreated)
            
                entries = _nullStorage.Value;
                return true;
            
            else return false;
        
        else
        
            if (_innerDictionary.ContainsKey(key))
            
                entries = _innerDictionary[key];
                return true;
            
            else return false;
        
    

使用示例:

var dictionary = new OpenDictionary<string, int>();
dictionary.Add("1", 1); 
// The next line won't throw an exception; 
dictionary.Add("1", 2);

dictionary.TryGetEntries("1", out List<int> result); 
// result is  1, 2 

dictionary.Add(null, 42);
dictionary.Add(null, 24);
dictionary.TryGetEntries(null, out List<int> result); 
// result is  42, 24 

【讨论】:

您能否解释一下您的代码的作用、它如何回答问题以及一些示例用法? @Enigmativity,它完全符合要求,问题是建议一些支持重复的字典,所以我提出创建一个围绕.net 字典的包装器,它将支持此功能并作为示例创建这样的包装器,作为奖励,它可以将 null 作为键处理(即使这是一个不好的做法,当然)用法很简单:var dictionary = new OpenDictionary&lt;string, int&gt;(); dictionary.Add("1", 1); // The next line won't throw an exception; dictionary.Add("1", 2); dictionary.TryGetEntries("1", out List&lt;int&gt; result); // result is 1, 2 您可以在答案中添加详细信息吗? @Enigmativity,如果你的意思是原始答案,那么确定【参考方案3】:

我使用这个简单的类:

public class ListMap<T,V> : List<KeyValuePair<T, V>>

    public void Add(T key, V value) 
        Add(new KeyValuePair<T, V>(key, value));
    

    public List<V> Get(T key) 
        return FindAll(p => p.Key.Equals(key)).ConvertAll(p=> p.Value);
    

用法:

var fruits = new ListMap<int, string>();
fruits.Add(1, "apple");
fruits.Add(1, "orange");
var c = fruits.Get(1).Count; //c = 2;

【讨论】:

【参考方案4】:

我将 @Hector Correa 的答案更改为具有泛型类型的扩展,并为其添加了自定义 TryGetValue。

  public static class ListWithDuplicateExtensions
  
    public static void Add<TKey, TValue>(this List<KeyValuePair<TKey, TValue>> collection, TKey key, TValue value)
    
      var element = new KeyValuePair<TKey, TValue>(key, value);
      collection.Add(element);
    

    public static int TryGetValue<TKey, TValue>(this List<KeyValuePair<TKey, TValue>> collection, TKey key, out IEnumerable<TValue> values)
    
      values = collection.Where(pair => pair.Key.Equals(key)).Select(pair => pair.Value);
      return values.Count();
    
  

【讨论】:

【参考方案5】:

这是一种双向并发词典我认为这会对你有所帮助:

public class HashMapDictionary<T1, T2> : System.Collections.IEnumerable

    private System.Collections.Concurrent.ConcurrentDictionary<T1, List<T2>> _keyValue = new System.Collections.Concurrent.ConcurrentDictionary<T1, List<T2>>();
    private System.Collections.Concurrent.ConcurrentDictionary<T2, List<T1>> _valueKey = new System.Collections.Concurrent.ConcurrentDictionary<T2, List<T1>>();

    public ICollection<T1> Keys
    
        get
        
            return _keyValue.Keys;
        
    

    public ICollection<T2> Values
    
        get
        
            return _valueKey.Keys;
        
    

    public int Count
    
        get
        
            return _keyValue.Count;
        
    

    public bool IsReadOnly
    
        get
        
            return false;
        
    

    public List<T2> this[T1 index]
    
        get  return _keyValue[index]; 
        set  _keyValue[index] = value; 
    

    public List<T1> this[T2 index]
    
        get  return _valueKey[index]; 
        set  _valueKey[index] = value; 
    

    public void Add(T1 key, T2 value)
    
        lock (this)
        
            if (!_keyValue.TryGetValue(key, out List<T2> result))
                _keyValue.TryAdd(key, new List<T2>()  value );
            else if (!result.Contains(value))
                result.Add(value);

            if (!_valueKey.TryGetValue(value, out List<T1> result2))
                _valueKey.TryAdd(value, new List<T1>()  key );
            else if (!result2.Contains(key))
                result2.Add(key);
        
    

    public bool TryGetValues(T1 key, out List<T2> value)
    
        return _keyValue.TryGetValue(key, out value);
    

    public bool TryGetKeys(T2 value, out List<T1> key)
    
        return _valueKey.TryGetValue(value, out key);
    

    public bool ContainsKey(T1 key)
    
        return _keyValue.ContainsKey(key);
    

    public bool ContainsValue(T2 value)
    
        return _valueKey.ContainsKey(value);
    

    public void Remove(T1 key)
    
        lock (this)
        
            if (_keyValue.TryRemove(key, out List<T2> values))
            
                foreach (var item in values)
                
                    var remove2 = _valueKey.TryRemove(item, out List<T1> keys);
                
            
        
    

    public void Remove(T2 value)
    
        lock (this)
        
            if (_valueKey.TryRemove(value, out List<T1> keys))
            
                foreach (var item in keys)
                
                    var remove2 = _keyValue.TryRemove(item, out List<T2> values);
                
            
        
    

    public void Clear()
    
        _keyValue.Clear();
        _valueKey.Clear();
    

    IEnumerator IEnumerable.GetEnumerator()
    
        return _keyValue.GetEnumerator();
    

例子:

public class TestA

    public int MyProperty  get; set; 


public class TestB

    public int MyProperty  get; set; 


            HashMapDictionary<TestA, TestB> hashMapDictionary = new HashMapDictionary<TestA, TestB>();

            var a = new TestA()  MyProperty = 9999 ;
            var b = new TestB()  MyProperty = 60 ;
            var b2 = new TestB()  MyProperty = 5 ;
            hashMapDictionary.Add(a, b);
            hashMapDictionary.Add(a, b2);
            hashMapDictionary.TryGetValues(a, out List<TestB> result);
            foreach (var item in result)
            
                //do something
            

【讨论】:

【参考方案6】:

“滚动您自己的”版本的字典非常容易,它允许“重复键”条目。这是一个粗略的简单实现。您可能需要考虑在IDictionary&lt;T&gt; 上添加对基本上大多数(如果不是全部)的支持。

public class MultiMap<TKey,TValue>

    private readonly Dictionary<TKey,IList<TValue>> storage;

    public MultiMap()
    
        storage = new Dictionary<TKey,IList<TValue>>();
    

    public void Add(TKey key, TValue value)
    
        if (!storage.ContainsKey(key)) storage.Add(key, new List<TValue>());
        storage[key].Add(value);
    

    public IEnumerable<TKey> Keys
    
        get  return storage.Keys; 
    

    public bool ContainsKey(TKey key)
    
        return storage.ContainsKey(key);
    

    public IList<TValue> this[TKey key]
    
        get
        
            if (!storage.ContainsKey(key))
                throw new KeyNotFoundException(
                    string.Format(
                        "The given key 0 was not found in the collection.", key));
            return storage[key];
        
    

如何使用它的简单示例:

const string key = "supported_encodings";
var map = new MultiMap<string,Encoding>();
map.Add(key, Encoding.ASCII);
map.Add(key, Encoding.UTF8);
map.Add(key, Encoding.Unicode);

foreach (var existingKey in map.Keys)

    var values = map[existingKey];
    Console.WriteLine(string.Join(",", values));

【讨论】:

【参考方案7】:

你可以定义一个方法来构建一个复合字符串键 您想使用字典的每个地方都必须使用此方法来构建您的密钥 例如:

private string keyBuilder(int key1, int key2)

    return string.Format("0/1", key1, key2);

使用:

myDict.ContainsKey(keyBuilder(key1, key2))

【讨论】:

【参考方案8】:

这也是可能的:

Dictionary<string, string[]> previousAnswers = null;

这样,我们可以拥有唯一的键。希望这对你有用。

【讨论】:

OP 要求提供允许重复键的字典。【参考方案9】:

您可以添加具有不同大小写的相同键,例如:

key1 键1 KEY1 钥匙1 密钥1 关键1

我知道答案是假的,但对我有用。

【讨论】:

不,它不适合你。字典允许非常快速的查找 - 也归类为 O(1) - 通过键,当您添加不同大小写的多个键时,您会丢失它,因为您如何检索它们?尝试每个大写/小写组合?不管你怎么做,性能都不会像普通的单字典查找那样。除此之外,还有其他更明显的缺点,例如每个键的值的限制,具体取决于键中可大写字符的数量。【参考方案10】:

如果您使用 >= .NET 4,那么您可以使用 Tuple 类:

// declaration
var list = new List<Tuple<string, List<object>>>();

// to add an item to the list
var item = Tuple<string, List<object>>("key", new List<object>);
list.Add(item);

// to iterate
foreach(var i in list)

    Console.WriteLine(i.Item1.ToString());

【讨论】:

这看起来像上面的List&lt;KeyValuePair&lt;key, value&gt;&gt; 解决方案。我错了吗?【参考方案11】:

我使用的方式只是一个

Dictionary&lt;string, List&lt;string&gt;&gt;

这样你就有一个保存字符串列表的键。

例子:

List<string> value = new List<string>();
if (dictionary.Contains(key)) 
     value = dictionary[key];

value.Add(newValue);

【讨论】:

很好很干净。 这是处理我的用例的绝佳解决方案。【参考方案12】:

回答原始问题。 Dictionary&lt;string, List&lt;object&gt;&gt; 之类的东西在 Code Project 中名为 MultiMap 的类中实现。

您可以通过以下链接找到更多信息: http://www.codeproject.com/KB/cs/MultiKeyDictionary.aspx

【讨论】:

【参考方案13】:

当使用List&lt;KeyValuePair&lt;string, object&gt;&gt; 选项时,您可以使用 LINQ 进行搜索:

List<KeyValuePair<string, object>> myList = new List<KeyValuePair<string, object>>();
//fill it here
var q = from a in myList Where a.Key.Equals("somevalue") Select a.Value
if(q.Count() > 0) //you've got your value 

【讨论】:

是的,但这并不能让它更快(仍然没有散列)【参考方案14】:

这是使用 List >

的一种方法
public class ListWithDuplicates : List<KeyValuePair<string, string>>

    public void Add(string key, string value)
    
        var element = new KeyValuePair<string, string>(key, value);
        this.Add(element);
    


var list = new ListWithDuplicates();
list.Add("k1", "v1");
list.Add("k1", "v2");
list.Add("k1", "v3");

foreach(var item in list)

    string x = string.format("0=1, ", item.Key, item.Value);

输出 k1=v1,k1=v2,k1=v3

【讨论】:

【参考方案15】:

关于使用 Lookup 的重要说明:

您可以通过在实现IEnumerable(T) 的对象上调用ToLookup 来创建Lookup(TKey, TElement) 的实例

没有公共构造函数来创建Lookup(TKey, TElement) 的新实例。此外,Lookup(TKey, TElement) 对象是不可变的,也就是说,您不能在创建 Lookup(TKey, TElement) 对象后添加或删除元素或键。

(from MSDN)

我认为这对于大多数用途来说都是一个展示终结者。

【讨论】:

我能想到的用途很少,这将是一个展示终结者。但是,我认为不可变对象很棒。 @JoelMueller 我可以想到很多这样的情况。必须重新创建字典来添加项目并不是一个特别有效的解决方案......【参考方案16】:

我偶然发现了这篇文章以寻找相同的答案,但没有找到,所以我使用字典列表构建了一个简单的示例解决方案,覆盖 [] 运算符以在所有字典中添加一个新字典其他人有一个给定的键(集),并返回一个值列表(获取)。 它丑陋且效率低下,它只能通过键获取/设置,并且总是返回一个列表,但它可以工作:

 class DKD 
        List<Dictionary<string, string>> dictionaries;
        public DKD()
            dictionaries = new List<Dictionary<string, string>>();
        public object this[string key]
             get
                string temp;
                List<string> valueList = new List<string>();
                for (int i = 0; i < dictionaries.Count; i++)
                    dictionaries[i].TryGetValue(key, out temp);
                    if (temp == key)
                        valueList.Add(temp);
                return valueList;
            set
                for (int i = 0; i < dictionaries.Count; i++)
                    if (dictionaries[i].ContainsKey(key))
                        continue;
                    else
                        dictionaries[i].Add(key,(string) value);
                        return;
                dictionaries.Add(new Dictionary<string, string>());
                dictionaries.Last()[key] =(string)value;
            
        
    

【讨论】:

【参考方案17】:

如果您使用的是 .NET 3.5,请使用 Lookup 类。

编辑:您通常使用Enumerable.ToLookup 创建一个Lookup。这确实假设您以后不需要更改它 - 但我通常认为这已经足够了。

如果那个对你不起作用,我认为框架中没有任何东西可以帮助 - 使用字典是最好的:(

【讨论】:

感谢您对 Lookup 的提醒。它提供了一种对非标准 orderby 条件的 linq 查询结果进行分区(分组)的好方法。 @Josh:你使用 Enumerable.ToLookup 创建一个。 注意事项Lookup 不可序列化 我们应该如何向 Lookup 添加项目?【参考方案18】:

List 类实际上对于包含重复项的键/值集合非常有效,您希望在其中迭代集合。示例:

List<KeyValuePair<string, string>> list = new List<KeyValuePair<string, string>>();

// add some values to the collection here

for (int i = 0;  i < list.Count;  i++)

    Print(list[i].Key, list[i].Value);

【讨论】:

这个解决方案在功能上有效,但是 List 的实现不知道键或值,并且根本无法优化键的搜索 ModelStateDictionary 使用IEnumerable&lt;KeyValuePair&lt;string, ModelStateEntry&gt;&gt;,所以如果列表很小,我们不应该对优化感到难过。如果你想Add,你可以使用 IList。【参考方案19】:

NameValueCollection 支持一个键下的多个字符串值(这也是一个字符串),但这是我知道的唯一示例。

当我遇到需要这种功能的情况时,我倾向于创建类似于您示例中的构造。

【讨论】:

【参考方案20】:

我刚刚遇到了PowerCollections 库,其中包括一个名为 MultiDictionary 的类。这巧妙地包装了这种类型的功能。

【讨论】:

【参考方案21】:

如果您同时使用字符串作为键和值,则可以使用System.Collections.Specialized.NameValueCollection,它将通过 GetValues(string key) 方法返回一个字符串值数组。

【讨论】:

NameValueCollection 不允许多个键。 @Jenny O'Reilly:当然,您可以添加重复键。 严格来说@JennyO'Reilly 是正确的,因为链接的 MSDN 页面上的注释清楚地指出:这个类在一个键下存储多个字符串值 它会允许但会返回多个值,我尝试使用索引和键。 请注意NameValueCollection cannot be deserailized by System.Text.Json【参考方案22】:

你的意思是一致的,而不是实际的重复?否则哈希表将无法工作。

一致意味着两个单独的键可以散列到等效值,但键不相等。

例如:假设您的哈希表的哈希函数只是 hashval = key mod 3。1 和 4 都映射到 1,但是是不同的值。这就是您对列表的想法发挥作用的地方。

当您需要查找 1 时,将该值散列为 1,然后遍历列表,直到找到 Key = 1。

如果您允许插入重复的键,您将无法区分哪些键映射到哪些值。

【讨论】:

哈希表已经处理了碰巧哈希到相同值的键(这称为冲突)。我指的是您希望将多个值映射到同一个确切键的情况。【参考方案23】:

重复键会破坏字典的整个合同。在字典中,每个键都是唯一的并映射到单个值。如果您想将一个对象链接到任意数量的其他对象,最好的选择可能是类似于 DataSet(通常用表)的东西。将您的键放在一列中,将您的值放在另一列中。这比字典慢得多,但这是你失去散列关键对象能力的权衡。

【讨论】:

使用字典来提高性能不就是重点吗?使用 DataSet 似乎并不比 List> 好。【参考方案24】:

我认为像 List&lt;KeyValuePair&lt;object, object&gt;&gt; 这样的东西可以胜任。

【讨论】:

您如何通过它的键在该列表中查找某些内容? 你必须遍历它:但我不知道 .NET 3.5 的 LookUp-Class:也许这对于搜索它的内容更有用。 @wizlib:唯一的方法是遍历列表,它的效率不如散列。 -1 @petrk。这实际上取决于您的数据是什么。我使用这个实现是因为我只有很少的唯一键并且不想招致散列冲突的开销。 +1

以上是关于.NET 字典中的重复键?的主要内容,如果未能解决你的问题,请参考以下文章

从Python中的字典中删除键[重复]

Swift:为啥字典中的键类型必须是 Hashable 类型 [重复]

如何将重复键添加到字典中

如何对一个字典中的所有键进行汇总(求和),如果一些键已经重复了不止一次。

在python中的字典中切换键和值[重复]

如何在Python中找到字典中的最大值键[重复]