Chiavi duplicate nei dizionari .NET?


256

Esistono classi di dizionari nella libreria di classi di base .NET che consentono di utilizzare chiavi duplicate? L'unica soluzione che ho trovato è creare, ad esempio, una classe come:

Dictionary<string, List<object>>

Ma questo è abbastanza irritante da usare davvero. In Java, credo che una MultiMap riesca a farlo, ma non riesco a trovare un analogo in .NET.


3
Come è questa chiave duplicata, sono i valori duplicati (l'elenco), giusto?
Shamim Hafiz,

1
@ShamimHafiz, no, i valori non devono essere duplicati. Se devi archiviare duplicati { a, 1 }e { a, 2 }in una tabella hash in cui si atrova la chiave, un'alternativa è avere { a, [1, 2] }.
nawfal,

1
In realtà, credo che ciò che è veramente desiderato qui sia una raccolta in cui ogni chiave può essere associata a uno o più valori. Penso che l'espressione "chiavi duplicate" non lo trasmetta davvero.
DavidRR

Per riferimento futuro, dovresti considerare di mantenere 1 chiave semplicemente aggiungendo i valori ad essa invece di aggiungere ripetutamente le stesse chiavi.
Ya Wang,

Se sia le chiavi che i valori sono stringhe, c'è NameValueCollection (che può associare più valori di stringa a ciascuna chiave di stringa).
AnorZaken,

Risposte:


229

Se stai usando .NET 3.5, usa la Lookupclasse.

EDIT: generalmente si crea un Lookuputilizzo Enumerable.ToLookup. Ciò presume che non sia necessario modificarlo in seguito, ma in genere trovo che sia abbastanza buono.

Se questo non funziona per te, non credo che ci sia nulla nel framework che possa aiutare - e l'uso del dizionario è buono come si arriva :(


Grazie per l'heads-up su Ricerca. Offre un ottimo modo per partizionare (raggruppare) i risultati da una query linq che non sono criteri di ordinamento standard.
Robert Paulson,

3
@Josh: usi Enumerable.ToLookup per crearne uno.
Jon Skeet,

29
Parola di cautela : Lookupnon è serializzabile
SliverNinja - MSFT

1
come dovremmo aggiungere elementi a quella Ricerca?
Moldovanu,

171

La classe List in realtà funziona abbastanza bene per le raccolte chiave / valore contenenti duplicati in cui desideri scorrere la raccolta. Esempio:

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
Questa soluzione funziona funzionalmente, ma l'implementazione di List non ha alcuna conoscenza della chiave o del valore e non può affatto ottimizzare la ricerca delle chiavi
Spencer Rose,

41

Ecco un modo per farlo con List <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);
}

Uscite k1 = v1, k1 = v2, k1 = v3


Funziona benissimo nel mio scenario e anche facile da usare.
user6121177

21

Se si utilizzano stringhe sia come chiavi che come valori, è possibile utilizzare System.Collections.Specialized.NameValueCollection , che restituirà una matrice di valori stringa tramite il metodo GetValues ​​(chiave stringa).


6
NameValueCollection non consente più chiavi.
Jenny O'Reilly,

@Jenny O'Reilly: Certo, puoi aggiungere chiavi duplicate.
isHuman il

A rigor di termini @ JennyO'Reilly è corretto, come indicano chiaramente le osservazioni sulla pagina MSDN collegata: questa classe memorizza più valori di stringa in un'unica chiave .
Ronald

Permetterà ma restituirà più valori, ho provato a utilizzare l'indice e la chiave.
user6121177,

18

Mi sono appena imbattuto nella libreria di PowerCollections che include, tra le altre cose, una classe chiamata MultiDictionary. Questo avvolge ordinatamente questo tipo di funzionalità.


14

Nota molto importante sull'uso di Lookup:

È possibile creare un'istanza di a Lookup(TKey, TElement)chiamando ToLookupun oggetto che implementaIEnumerable(T)

Non esiste un costruttore pubblico per creare una nuova istanza di a Lookup(TKey, TElement). Inoltre, gli Lookup(TKey, TElement)oggetti sono immutabili, ovvero non è possibile aggiungere o rimuovere elementi o chiavi da un Lookup(TKey, TElement)oggetto dopo che è stato creato.

(da MSDN)

Penso che questo sarebbe un tappo per la maggior parte degli usi.


6
Riesco a pensare a pochissimi usi in cui questo sarebbe un punto fermo. Ma poi, penso che gli oggetti immutabili siano fantastici.
Joel Mueller,

1
@JoelMueller Mi viene in mente un sacco di casi in cui questo è un punto fermo. Dover ricreare un dizionario per aggiungere un elemento non è una soluzione particolarmente efficiente ...
reirab

12

Penso che qualcosa del genere List<KeyValuePair<object, object>>farebbe il lavoro.


Come cerchi qualcosa in quella lista dalla sua chiave?
Wayne Bloss,

Devi iterarlo attraverso di esso: ma non ero a conoscenza della classe LookUp di .NET 3.5: forse questo è più utile per la ricerca nel suo contenuto.
Mappa MAD

2
@wizlib: l'unico modo è di scorrere l'elenco, che non è altrettanto efficiente dell'hash. -1
petr k.

@petrk. Dipende davvero da quali sono i tuoi dati. Ho usato questa implementazione perché avevo pochissime chiavi uniche e non volevo sostenere il sovraccarico di collisioni di hash. +1
Evan M

9

Se si utilizza> = .NET 4, è possibile utilizzare 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());
}

Sembra una List<KeyValuePair<key, value>>soluzione come la precedente. Ho sbagliato?
Nathan Goings,

6

È abbastanza facile "rollare la propria" versione di un dizionario che consente voci "chiave duplicata". Ecco una semplice implementazione approssimativa. Potresti prendere in considerazione l'aggiunta di supporto per la maggior parte (se non tutti) su 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];
        }
    }
}

Un rapido esempio su come usarlo:

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


3

NameValueCollection supporta più valori di stringa in un'unica chiave (che è anche una stringa), ma è l'unico esempio di cui sono a conoscenza.

Tendo a creare costrutti simili a quello del tuo esempio quando incontro situazioni in cui ho bisogno di quel tipo di funzionalità.


3

Quando si utilizza l' List<KeyValuePair<string, object>>opzione, è possibile utilizzare LINQ per eseguire la ricerca:

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
Sì, ma questo non lo rende più veloce (ancora senza hashing)
Haukman,

3

Dal momento che il nuovo C # (credo sia dalla 7.0), puoi anche fare qualcosa del genere:

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

e lo stai usando come un elenco standard, ma con due valori denominati come preferisci

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

2

Intendi congruente e non un vero duplicato? Altrimenti una tabella hash non sarebbe in grado di funzionare.

Congruente significa che due chiavi separate possono eseguire l'hash sul valore equivalente, ma le chiavi non sono uguali.

Ad esempio: supponiamo che la funzione hash della tua hashtable fosse solo hashval = key mod 3. Sia 1 che 4 sono mappati su 1, ma hanno valori diversi. È qui che entra in gioco la tua idea di un elenco.

Quando è necessario cercare 1, quel valore viene ridotto a 1, l'elenco viene attraversato fino a quando non viene trovata la chiave = 1.

Se si consentisse l'inserimento di chiavi duplicate, non si sarebbe in grado di differenziare quali chiavi corrispondono a quali valori.


2
Una tabella di hash gestisce già le chiavi che hanno lo hash con lo stesso valore (questo si chiama collisione). Mi riferisco a una situazione in cui si desidera mappare più valori sulla stessa chiave esatta.

2

Il modo in cui uso è solo un

Dictionary<string, List<string>>

In questo modo hai una sola chiave che contiene un elenco di stringhe.

Esempio:

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

È bello e pulito.
Jerry Nixon,

Questa è un'ottima soluzione per gestire il mio caso d'uso.
dub stylee,

1

Mi sono imbattuto in questo post alla ricerca della stessa risposta e non ho trovato nessuno, quindi ho preparato una soluzione di esempio bare-bones usando un elenco di dizionari, ignorando l'operatore [] per aggiungere un nuovo dizionario all'elenco quando tutti gli altri hanno un data key (set) e restituisce un elenco di valori (get).
È brutto e inefficiente, ottiene / imposta SOLO dalla chiave e restituisce sempre un elenco, ma funziona:

 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

Ho modificato la risposta di @Hector Correa in un'estensione con tipi generici e ho anche aggiunto un TryGetValue personalizzato.

  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

Questo è un modo di rimorchio Dizionario simultaneo Penso che questo ti aiuterà:

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

esempi:

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

io uso questa semplice classe:

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

utilizzo:

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

0

Puoi creare il tuo wrapper dizionario, qualcosa del genere, come bonus supporta un valore null come chiave:

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

Il campione di utilizzo:

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 }

Puoi dare qualche spiegazione su ciò che fa il tuo codice, come risponde alla domanda e alcuni esempi di utilizzo?
Enigmativity,

@Enigmativity, fa esattamente quello che è stato chiesto, la domanda era quella di suggerire un dizionario che supporti i duplicati, quindi mi sono offerto di creare un wrapper attorno al dizionario .net che supporterà questa funzionalità e come esempio creato tale wrapper, come bonus può gestire null come chiave (anche se è una cattiva pratica, di sicuro) L'uso è abbastanza semplice: 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 }
Alexander Tolstikov,

Puoi aggiungere i dettagli alla tua risposta?
Enigmativity,

@Enigmativity, se vuoi dire la risposta originale, allora sicuro
Alexander Tolstikov,

-1

Puoi definire un metodo per costruire una chiave di stringa composta ogni volta che vuoi usare il dizionario, devi usare questo metodo per costruire la tua chiave, ad esempio:

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

per usare:

myDict.ContainsKey(keyBuilder(key1, key2))

-3

Le chiavi duplicate rompono l'intero contratto del dizionario. In un dizionario ogni chiave è unica e mappata su un singolo valore. Se si desidera collegare un oggetto a un numero arbitrario di oggetti aggiuntivi, la scommessa migliore potrebbe essere qualcosa di simile a un DataSet (nel linguaggio comune di un tavolo). Metti le tue chiavi in ​​una colonna e i tuoi valori nell'altra. Questo è significativamente più lento di un dizionario, ma questo è il tuo compromesso per perdere la capacità di hash degli oggetti chiave.


5
Non è il punto di usare un dizionario per il guadagno delle prestazioni? L'uso di un DataSet non sembra migliore di un Elenco <KeyValuePair <T, U >>.
Doug,

-4

Anche questo è possibile:

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

In questo modo, possiamo avere chiavi univoche. Spero che questo funzioni per te.


L'OP ha richiesto un dizionario che consenta chiavi duplicate.
Skaparate,

-10

Puoi aggiungere le stesse chiavi con casi diversi come:

key1
Key1
KEY1
key1
key1
key1

So che è una risposta fittizia, ma ha funzionato per me.


4
No, non ha funzionato per te. I dizionari consentono una ricerca molto veloce - anche classificata come O (1) - per chiave, la perdi quando aggiungi le tue chiavi multiple con caselle diverse, perché come le recuperi? Prova ogni combinazione maiuscola / minuscola? Indipendentemente da come lo fai, le prestazioni non saranno quelle della normale ricerca nel dizionario singolo. Questo è in aggiunta ad altre, più evidenti carenze come la limitazione dei valori per chiave, a seconda del numero di caratteri modificabili nella chiave.
Eugene Beresovsky,
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.