Дублікати ключів у .NET словниках?


256

Чи є в бібліотеці базового класу .NET якісь словники, які дозволяють використовувати повторювані ключі? Єдине знайдене нами рішення - створити, наприклад, клас типу:

Dictionary<string, List<object>>

Але це дуже дратує фактично використання. В Java я вважаю, що MultiMap цього виконує, але не може знайти аналог у .NET.


3
Як це дублікат ключа, це дублюючі значення (Список), правда?
Шамім Хафіз

1
@ShamimHafiz, ні, значення не повинні бути дублікатами. Якщо вам потрібно зберігати дублікати { a, 1 }і { a, 2 }в хеш-таблиці, де aце ключ, є одна альтернатива { a, [1, 2] }.
nawfal

1
Насправді, я вважаю, що насправді хочеться - це колекція, де кожен ключ може зіставити одне або більше значень. Я думаю, що вираз "дублюючі ключі" насправді цього не передає.
DavidRR

Для подальшої довідки слід розглянути можливість збереження 1 клавіші, просто додаючи до неї значення, а не додавання однакових ключів знову і знову.
Ya Wang

Якщо і ключі, і значення є рядками, існує NameValueCollection (яка може пов'язувати кілька рядкових значень з кожною рядковою клавішею).
AnorZaken

Відповіді:


228

Якщо ви використовуєте .NET 3.5, використовуйте Lookupклас.

EDIT: Ви взагалі створити з Lookupдопомогою Enumerable.ToLookup. Це припускає, що вам не потрібно це змінювати після цього - але я зазвичай вважаю, що це досить добре.

Якщо це не працює для вас, я не думаю, що в рамках є щось, що допоможе - і використання словника настільки ж добре, як і виходить :(


Дякуємо за підсумки пошуку. Він пропонує чудовий спосіб розподілу (групування) результатів за допомогою запиту linq, який не є стандартними критеріями порядку.
Роберт Полсон,

3
@Josh: Ви використовуєте Enumerable.ToLookup, щоб створити його.
Джон Скіт

29
Слово обережності : Lookupне піддається серіалізації
SliverNinja - MSFT

1
як ми повинні додати елементи до цього пошуку?
молдовану

171

Клас 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);
}

31
Це рішення працює функціонально, але реалізація List не знає ключа або значення і не може оптимізувати пошук ключів взагалі
Спенсер Роуз

41

Ось один із способів зробити це за допомогою Список <KeyValuePair <string, string>>

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


За моїм сценарієм чудово працює, а також простий у використанні.
користувач6121177

21

Якщо ви використовуєте рядки як клавіші, так і значення, ви можете використовувати System.Collections.Specialized.NameValueCollection , який поверне масив значень рядків методом GetValues ​​(string key).


6
NameValueCollection не дозволяє використовувати кілька клавіш.
Дженні О'Рейлі

@Jenny O'Reilly: Звичайно, ви можете додати повторювані ключі.
isHuman

Строго кажучи, @ JennyO'Reilly є правильним, оскільки зауваження на пов'язаній сторінці MSDN чітко стверджують: цей клас зберігає кілька рядкових значень під одним ключем .
Рональд

Це дозволить, але поверне кілька значень, я спробував використовувати індекс і ключ.
користувач6121177

18

Я щойно натрапив на бібліотеку PowerCollections, яка включає, серед іншого, клас під назвою MultiDictionary. Це акуратно огортає цей тип функціональності.


14

Дуже важлива примітка щодо використання Lookup:

Ви можете створити екземпляр a Lookup(TKey, TElement), зателефонувавши ToLookupна об’єкт, який реалізуєIEnumerable(T)

Немає публічного конструктора для створення нового екземпляра Lookup(TKey, TElement). Крім того, Lookup(TKey, TElement)об'єкти є незмінними, тобто ви не можете додавати або видаляти елементи чи ключі з Lookup(TKey, TElement)об'єкта після його створення.

(від MSDN)

Я думаю, що це було б пробкою для більшості застосувань.


6
Я можу придумати дуже мало застосувань, де це було б пробкою для шоу. Але тоді, я думаю, непорушні предмети чудові.
Джоел Мюллер

1
@JoelMueller Я можу придумати чимало випадків, коли це стоп-шоу. Необхідність заново створити словник для додавання елемента - не особливо ефективне рішення ...
reirab

12

Я думаю, що щось подібне List<KeyValuePair<object, object>>зробить роботу.


Як ви шукаєте щось у цьому списку за його ключовим?
Уейн Блос

Ви повинні повторити це: але я не знав про LookUp-клас .NET 3.5: можливо, це корисніше для пошуку в ньому вмісту.
MADMap

2
@wizlib: Єдиний спосіб - прокрутити список, який не настільки ефективний, як хешування. -1
Петр к.

@petrk. Це дійсно залежить від ваших даних. Я використовував цю реалізацію, тому що в мене було дуже мало унікальних ключів і не хотілося зазнавати накладних колізійних зіткнень. +1
Еван М

9

Якщо ви використовуєте> = .NET 4, ви можете використовувати TupleClass:

// 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<KeyValuePair<key, value>>рішення, як вище. Я помиляюся?
Натан їде

6

Досить просто "прокатати свою" версію словника, яка дозволяє "дублювати ключ" записи. Ось приблизна проста реалізація. Ви можете розглянути можливість додавання підтримки для більшості (якщо не всіх) IDictionary<T>.

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));
}

4

У відповідь на оригінальне запитання. Щось подібне Dictionary<string, List<object>>реалізується в класі, який називається MultiMapThe Code Project.

Додаткову інформацію можна знайти за посиланням нижче: http://www.codeproject.com/KB/cs/MultiKeyDictionary.aspx


3

NameValueCollection підтримує декілька значень рядків під однією клавішею (що також є рядком), але це єдиний мені відомий приклад.

Я схильний створювати конструкції, подібні до того, як у вашому прикладі, коли я стикаюся з ситуаціями, коли мені потрібна така функціональність.


3

Використовуючи List<KeyValuePair<string, object>>опцію, ви можете використовувати 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 }

2
Так, але це не робить його швидшим (все ще немає хешування)
Haukman

3

Оскільки новий 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

Ви маєте на увазі конгруентний, а не фактичний дублікат? Інакше хештел не зможе працювати.

Конгруентний означає, що два окремих ключа можуть мати хеш-еквівалентне значення, але вони не рівні.

Наприклад: скажіть, що хеш-функція вашого хештелю була просто hashval = key mod 3. І 1, і 4 відображають у 1, але мають різні значення. Тут іде ваша ідея списку.

Коли вам потрібно знайти 1, це значення хеширується на 1, список переміщується, поки не буде знайдено ключ = 1.

Якщо ви дозволили вставити повторювані ключі, ви не зможете розмежувати, які клавіші відображають і які значення.


2
Хеш-таблиця вже обробляє ключі, які трапляються з однаковим значенням (це називається зіткненням). Я маю на увазі ситуацію, коли ви хочете зіставити кілька значень в один і той же точний ключ.

2

Я використовую лише те, що я використовую

Dictionary<string, List<string>>

Таким чином у вас є одна клавіша, що містить список рядків.

Приклад:

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

Це приємно і чисто.
Джеррі Ніксон

Це чудове рішення для вирішення мого випадку використання.
dub stylee

1

Я наткнувся на цю публікацію в пошуках тієї самої відповіді, і не знайшов жодної, тому сфальсифікував рішення прикладу з голими кістками за допомогою списку словників, перемінивши оператора [], щоб додати новий словник до списку, коли всі інші мають заданий ключ (встановити) і повернути список значень (get).
Це некрасиво і неефективно, він ТІЛЬКИ отримує / встановлює за ключем, і завжди повертає список, але він працює:

 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;
            }
        }
    }

1

Я змінив відповідь @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();
    }
  }

0

Це буквений спосіб Спільний словник, я думаю, це допоможе вам:

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
            }

0

я використовую цей простий клас:

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;

0

Ви можете створити власну обгортку словника, подібну до цієї, як бонус, який підтримує нульове значення як ключ:

/// <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

@Enigmativity, він робить саме те, що було запропоновано. Питання полягало в тому, щоб запропонувати якийсь словник, який підтримує дублікати, тому я запропонував створити обгортку навколо .net словника, який буде підтримувати цю функціональність, і, як приклад, створив таку обгортку, як бонус це може обробляти null як ключ (навіть якщо це погана практика, напевно) Використання досить просте: 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 }
Олександр Толстиков

Чи можете ви додати деталі до своєї відповіді?
Enigmativity

@Enigmativity, якщо ви маєте на увазі оригінальну відповідь, то впевнений
Олександр Толстиков,

-1

Ви можете визначити спосіб побудови ключа "Складений рядок", де ви хочете використовувати словник. Ви повинні використовувати цей метод, наприклад, для створення вашого ключа:

private string keyBuilder(int key1, int key2)
{
    return string.Format("{0}/{1}", key1, key2);
}

для використання:

myDict.ContainsKey(keyBuilder(key1, key2))

-3

Дублюючі клавіші розривають весь контракт Словника. У словнику кожна клавіша є унікальною і відображається на одне значення. Якщо ви хочете пов’язати об'єкт з довільною кількістю додаткових об'єктів, найкращою ставкою може бути щось, що схоже на DataSet (спільно кажучи таблиця). Покладіть свої ключі в один стовпчик, а значення - в інший. Це значно повільніше, ніж словник, але це ваш компроміс за втрату можливості хешувати ключові об’єкти.


5
Чи не вся суть використання словника для підвищення продуктивності? Використання набору даних здається не кращим, ніж список <KeyValuePair <T, U >>.
Дуг

-4

Також це можливо:

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

Таким чином, ми можемо мати унікальні ключі. Сподіваюся, це працює для вас.


ОП попросило словник, який дозволяє повторювати ключі.
Скапарати

-10

Ви можете додати ті самі ключі з різними регістрами, як:

key1
Key1
ключ1
ключом1
ключом1
ключом1

Я знаю, що це фіктивна відповідь, але працював на мене.


4
Ні, це у вас не вийшло. Словники дозволяють дуже швидко шукати - також класифіковану як O (1) - за ключем, ви втрачаєте це, додаючи кілька різних клавіш, оскільки як їх отримати? Спробуйте кожну комбінацію верхнього та нижнього регістру? Незалежно від того, як ви це зробите, продуктивність не буде такою, як у звичайного, одного словника пошуку. Це на додаток до інших, більш очевидних недоліків, таких як обмеження значень на ключ, залежно від кількості змінних символів у ключі.
Євген Бересовський
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.