.NET словарь с двумя ключами и одним значением

Есть ли доступный словарь в .NET, который может содержать 2 ключа и одно значение. Как

Dictionary(Of TKey, Of TKey, TValue)

Мне нужно хранить два ключа и в определенное время смотреть элемент клавишей 1 и в другое время клавишей 2.

Мое текущее решение состоит в том, чтобы поддерживать два словаря

Dictionary<string, long> Dict1 = new Dictionary<string, long>();
Dictionary<long, long> Dict2 = new Dictionary<long, long>();

и когда нужно добавить элемент, я добавлю его в оба словаря.

Dict1.Add("abc", 111);
Dict2.Add(345, 111);

а затем я просмотрю элемент из одного из этих словарей, в зависимости от того, какой из ключей мне нужно посмотреть.

То же самое я сделаю при удалении или обновлении элемента.

Я думал о составном ключе, но я не знаю, как его настроить, и я не хочу потерять скорость поиска элемента.

Есть ли какое-нибудь решение в .NET, чтобы иметь словарь, который может содержать несколько ключей?

Ответ 1

Как вы хотите, чтобы ваше значение "находилось" с любого из ключей, я бы просто использовал два словаря, как вы сейчас делаете. Однако я бы обернул это в класс, используя имена методов, такие как FindByXXX и FindByYYY.

Вопрос гораздо сложнее: как вы делаете удалять, так как вам нужно знать оба ключа во время удаления. Возможно, ваше значение хранит оба ключа, поэтому вы можете передать значение в свой метод удаления. Возможно, вам не нужно удалять предметы из словарей. Или код, который должен удалить элементы, знает оба ключа.

Следовательно, для этого нет стандартного словаря, так как требования различаются между каждым пользователем.

(Обратите внимание, что вам не нужен словарь с составным ключом, так как это потребует от вас знать оба ключа, когда вы хотите найти элемент.)

Ответ 2

Возможно, что-то вроде этого:

public class TwoKeyDictionary<Tkey1, Tkey2, TValue>
{
    private object m_data_lock = new object();
    private Dictionary<Tkey1, Tkey2> m_dic1 = new Dictionary<Tkey1, Tkey2>();
    private Dictionary<Tkey2, TValue> m_dic2 = new Dictionary<Tkey2, TValue>();

    public void AddValue(Tkey1 key1, Tkey2 key2, TValue value)
    {
        lock(m_data_lock)
        {
            m_dic1[key1] = key2;
            m_dic2[key2] = value;
        }
    }

    public TValue getByKey1(Tkey1 key1)
    {
        lock(m_data_lock)
            return m_dic2[m_dic1[key1]];
    }

    public TValue getByKey2(Tkey key2)
    {
        lock(m_data_lock)
            return m_dic2[key2];
    }

    public void removeByKey1(Tkey1 key1)
    {
        lock(m_data_lock)
        {
            Tkey2 tmp_key2 =   m_dic1[key1];
            m_dic1.Remove(key1);
            m_dic2.Remove(tmp_key2);
        }
    }

    public void removeByKey2(Tkey2 key2)
    {
        lock(m_data_lock)
        {
            Tkey1 tmp_key1 = m_dic1.First((kvp) => kvp.Value.Equals(key2)).Key;
            m_dic1.Remove(tmp_key1);
            m_dic2.Remove(key2);
        }
    }
}

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

public class TwoKeysDictionary<K1, K2, V>
{
    private class TwoKeysValue<K1, K2, V>
    {
        public K1 Key1 { get; set; }
        public K2 Key2 { get; set; }
        public V Value { get; set; }
    }

    private List<TwoKeysValue<K1, K2, V>> m_list = new List<TwoKeysValue<K1, K2, V>>();

    public void Add(K1 key1, K2 key2, V value)
    {
        lock (m_list)
            m_list.Add(new TwoKeysValue<K1, K2, V>() { Key1 = key1, Key2 = key2, Value = value });
    }

    public V getByKey1(K1 key1)
    {
        lock (m_list)
            return m_list.First((tkv) => tkv.Key1.Equals(key1)).Value;
    }

    public V getByKey2(K2 key2)
    {
        lock (m_list)
            return m_list.First((tkv) => tkv.Key2.Equals(key2)).Value;
    }

    public void removeByKey1(K1 key1)
    {
        lock (m_list)
            m_list.Remove(m_list.First((tkv) => tkv.Key1.Equals(key1)));
    }

    public void removeByKey2(K2 key2)
    {
        lock (m_list)
            m_list.Remove(m_list.First((tkv) => tkv.Key2.Equals(key2)));
    }
}

В очень плохом случае, когда Keys являются большими структурами (то есть большими типами значений), а Keys равны по размеру, а значения - это небольшие типы значений (например, байт), с первым решением, которое у вас было: one набор Key1, два набора Key2, один набор значений = 3 набора больших объектов и 1 набор небольших значений. Со вторым решением у вас был: один набор Key1, один набор Key2, один набор значений = 2 набора больших объектов и небольшой набор со значениями. То есть с использованием первого решения вам нужно на 50% (или ниже) больше пространства памяти против второго, но второе решение очень, очень медленно и первое.

Ответ 3

Ваше решение сильно влияет на объем памяти вашего приложения. По мере роста словаря потребуется как минимум удвоить объем памяти (для типов значений), необходимый для хранения фактических данных.

Возможно, вы могли бы подойти к этому под другим углом. Есть два словаря:

var lookupDictionary = new Dictionary<string, string>();
var valuesDictionary = new Dictionary<string, [YourValueType]>();

Отсюда довольно просто.

// Add a new entry into the values dictionary and give it a unique key
valuesDictionary.Add("FooBar", "FUBAR VALUE");

// Add any number of lookup keys with the same value key
lookupDictionary.Add("Foo", "FooBar");
lookupDictionary.Add("Bar", "FooBar");
lookupDictionary.Add("Rab", "FooBar");
lookupDictionary.Add("Oof", "FooBar");

Когда вам нужно найти что-то из valuesDictionary, сначала нажмите lookupDictionary. Это даст вам ключ от значения, которое вы ищете в valuesDictionary.

ИЗМЕНИТЬ

Я не обращался к проблеме с удалением в своем ответе, поэтому вот он: D

Вы бы нажали lookupDictionary, чтобы найти ключ значения, а затем удалить все записи из lookupDictionary, которые имеют значение.

Должно быть достаточно простым и безопасным, так как valuesDictionary гарантированно имеет уникальный ключ, поэтому вы случайно не удалите ключ поиска для другого значения.

Однако, как отметил в своем выступлении Ян Ринроуз, вы сделаете полное сканирование на lookupDictionary для удаления. Это может оказать нежелательное влияние на производительность в жестких петлях и т.д.

Я действительно не могу придумать хороший способ решить эту проблему на данный момент. Возможно, у кого-то может быть несколько идей о том, как это можно улучшить.

Надеюсь, это поможет.

Ответ 4

Если вы используете С# 7.0, лучший способ сделать это - использовать типы и литералы типов:

// Declare
var dict = new Dictionary<(string, long), long>();

// Add
dict.Add(("abc", 345), 111);

// Get
var searchedValue = dict[("abc", 345)];

Ответ 5

Вы не можете сделать это только с помощью одного Словаря, не теряя скорость поиска. Причина в том, что если вы должны были создать составной ключ, нет никакого значимого значения, которое вы можете вернуть при переопределении GetHashCode. Это означает, что сравнение равенства должно выполняться против каждого ключа до тех пор, пока не будет найдена запись словаря. В этом случае у вас также будет потенциальная проблема с составным ключом: поскольку ваш метод Equals будет проверять, одинаково ли одно свойство или другое, следующие ключи будут по существу дублированными ключами {Id = 1, Name= "Bob" } {Id = 1, Name= "Anna" }, что не дает мне теплого нечеткого чувства.

Это оставляет вас с переводом словаря или пары словарей с вашим собственным классом.

Ответ 6

интересный вопрос, здесь одно решение. Вы должны добавить индексатор для каждого типа ключа, который хотите поддержать.

public class NewDic<T>
{
    public void Add(string key1, long key2, T value)
    {
        mDic.Add(key1, value);
        mDic.Add(key2, value);
    }

    public T this[string s]
    {
        get { return mDic[s]; }
    }

    public T this[long l]
    {
        get { return mDic[l]; }
    }


    Dictionary<object, T> mDic = new Dictionary<object, T>();
}

        NewDic<long> dic = new NewDic<long>();

        dic.Add("abc", 20, 10);

        Console.WriteLine(dic["abc"]);
        Console.WriteLine(dic[20]);

Ответ 7

Это НЕ правильный словарь, но его можно использовать для простого добавления словарных функций для добавления слова.

Это можно сделать и общим, с надлежащей реализацией IComparable в типах ключей и соответствующим образом меняя словарный код. (Примечание. Значения по умолчанию для ключей не допускаются для двусмысленности!)

internal class KeyValueSet //this dictionary item is tailor made for this example
{
    public string KeyStr { get; set; }
    public int KeyInt { get; set; }
    public int Value { get; set; }

    public KeyValueSet() { }

    public KeyValueSet(string keyStr, int keyInt, int value)
    {
        KeyStr = keyStr;
        KeyInt = keyInt;
        Value = value;
    }
}

public class DoubleKeyDictionary
{
    List<KeyValueSet> _list = new List<KeyValueSet>();

    private void Add(KeyValueSet set)
    {
        if (set == null)
            throw new InvalidOperationException("Cannot add null");
        if (string.IsNullOrEmpty(set.KeyStr) && set.KeyInt == 0)
            throw new InvalidOperationException("Invalid key");
        if (!string.IsNullOrEmpty(set.KeyStr) && _list.Any(l => l.KeyStr.Equals(set.KeyStr))
            || set.KeyInt != 0 && _list.Any(l => l.KeyInt == set.KeyInt))
            throw new InvalidOperationException("Either of keys exists");
        _list.Add(set);
    }

    public void Add(string keyStr, int keyInt, int value)
    {
        Add(new KeyValueSet { KeyInt = keyInt, KeyStr = keyStr, Value = value });
    }

    public void Add(string key, int value)
    {
        Add(new KeyValueSet { KeyInt = 0, KeyStr = key, Value = value });
    }

    public void Add(int key, int value)
    {
        Add(new KeyValueSet { KeyInt = key, KeyStr = string.Empty, Value = value });
    }

    public void Remove(int key)
    {
        if (key == 0)
            throw new InvalidDataException("Key not found");
        var val = _list.First(l => l.KeyInt == key);
        _list.Remove(val);
    }

    public void Remove(string key)
    {
        if (string.IsNullOrEmpty(key))
            throw new InvalidDataException("Key not found");
        var val = _list.First(l => l.KeyStr == key);
        _list.Remove(val);
    }

    public void Remove(KeyValueSet item)
    {
        _list.Remove(item);
    }

    public int this[int index]
    {
        get
        {
            if (index != 0 && _list.Any(l => l.KeyInt == index))
                return _list.First(l => l.KeyInt == index).Value;
            throw new InvalidDataException("Key not found");
        }
        set
        {
            Add(index, value);
        }
    }

    public int this[string key]
    {
        get
        {
            if (!string.IsNullOrEmpty(key) && _list.Any(l => l.KeyStr == key))
                return _list.First(l => l.KeyStr == key).Value;
            throw new InvalidDataException("Key not found");
        }
        set
        {
            Add(key, value);
        }
    }
}

Тестирование DoubleKeyDictionary

var dict = new DoubleKeyDictionary();
dict.Add(123, 1);
dict.Add(234, 2);
dict.Add("k1", 3);
dict.Add("k2", 4);            
dict[456] = 5;
dict["k3"] = 6;
dict.Add("k4", 567, 7);
dict.Remove(123);

Console.WriteLine(dict[234]); //2
Console.WriteLine(dict["k2"]); //4
Console.WriteLine(dict[456]); //5
Console.WriteLine(dict[567]); //7
Console.WriteLine(dict["k4"]); //7
Console.WriteLine(dict[123]); //exception

Ответ 8

Как насчет Dictionary<Tuple<string, long>, long>? Кортежи сравниваются по значению, поэтому он должен индексировать однозначно ожидаемым образом. Кроме того, теперь вам не придется упаковывать значение long в два места (и иметь дело с замечательной болью синхронизации значений везде).

Как насчет этого подхода? В принципе, по-прежнему используйте стратегию, основанную на словарях, но создавайте ее через класс с перегруженными свойствами индексатора. Таким образом, он выглядит как словарь, выглядит как словарь, но поддерживает несколько ключей (не как словарь, LOL).

public class MultiKeyDictionary<TFirstKey, TSecondKey, TValue>
{
    private readonly Dictionary<TFirstKey, TValue> firstKeyDictionary = 
        new Dictionary<TFirstKey, TValue>();
    private readonly Dictionary<TSecondKey, TFirstKey> secondKeyDictionary = 
        new Dictionary<TSecondKey, TFirstKey>();

    public TValue this[TFirstKey idx]
    {
        get
        {
            return firstKeyDictionary[idx];
        }
        set
        {
            firstKeyDictionary[idx] = value;
        }
    }

    public TValue this[TSecondKey idx]
    {
        get
        {
            var firstKey = secondKeyDictionary[idx];
            return firstKeyDictionary[firstKey];
        }
        set
        {
            var firstKey = secondKeyDictionary[idx];
            firstKeyDictionary[firstKey] = value;
        }
    }

    public IEnumerable<KeyValuePair<TFirstKey, TValue>> GetKeyValuePairsOfFirstKey()
    {
        return firstKeyDictionary.ToList();
    }

    public IEnumerable<KeyValuePair<TSecondKey, TValue>> GetKeyValuePairsOfSecondKey()
    {
        var r = from s in secondKeyDictionary
            join f in firstKeyDictionary on s.Value equals f.Key
            select new KeyValuePair<TSecondKey, TValue>(s.Key, f.Value);

        return r.ToList();
    }

    public void Add(TFirstKey firstKey, TSecondKey secondKey, TValue value)
    {
        firstKeyDictionary.Add(firstKey, value);
        secondKeyDictionary.Add(secondKey, firstKey);
    }

    public bool Remove(TFirstKey firstKey)
    {
        if (!secondKeyDictionary.Any(f => f.Value.Equals(firstKey))) return false;

        var secondKeyToDelete = secondKeyDictionary.First(f => f.Value.Equals(firstKey));

        secondKeyDictionary.Remove(secondKeyToDelete.Key);
        firstKeyDictionary.Remove(firstKey);

        return true;
    }

    public bool Remove(TSecondKey secondKey)
    {
        if (!secondKeyDictionary.ContainsKey(secondKey)) return false;

        var firstKey = secondKeyDictionary[secondKey];
        secondKeyDictionary.Remove(secondKey);
        firstKeyDictionary.Remove(firstKey);

        return true;
    }
}

Проверьте код...

    static void Main(string[] args)
    {
        var dict = new MultiKeyDictionary<string, long, long>();
        dict.Add("abc", 111, 1234);
        dict.Add("def", 222, 7890);
        dict.Add("hij", 333, 9090);

        Console.WriteLine(dict["abc"]); // expect 1234
        Console.WriteLine(dict["def"]); // expect 7890
        Console.WriteLine(dict[333]); // expect 9090

        Console.WriteLine();
        Console.WriteLine("removing def");
        dict.Remove("def");

        Console.WriteLine();
        Console.WriteLine("now we have:");
        foreach (var d in dict.GetKeyValuePairsOfFirstKey())
        {
            Console.WriteLine($"{d.Key} : {d.Value}");
        }

        Console.WriteLine();
        Console.WriteLine("removing 333");
        dict.Remove(333);

        Console.WriteLine();
        Console.WriteLine("now we have:");
        foreach (var d in dict.GetKeyValuePairsOfSecondKey())
        {
            Console.WriteLine($"{d.Key} : {d.Value}");
        }

        Console.ReadLine();
    }

Ответ 9

Как было предложено в комментарии к вашему вопросу, вы можете просто использовать клавишу Object для своего Dictionary:

Dictionary<Object, long> dict = new Dictionary<Object, long>();
dict.Add("abc", 111);
dict.Add(345, 111);

Чтобы получить более чистое решение, вы можете перенести этот словарь в пользовательский класс и создать свою версию метода Add:

public void Add(ISet<Object> keys, T value){
    foreach(Object k in keys)
    {
        _privateDict.Add(k, value); 
    }
}

Ответ 10

Сначала мне показалось, что я мог бы создать класс, в котором были бы задействованы IDictionary<TKey1, TValue> и IDictionary<TKey2, TValue>, и просто иметь один словарь как поле и делегировать большинство методов единственному словарю с минимальной логикой.

Проблема с этим подходом заключается в том, что TKey1 и TKey2 могут быть одного типа, что является проблемой, потому что этот новый класс будет реализовывать один и тот же интерфейс дважды. Какой метод должен вызывать время выполнения, когда TKey1 является string и TKey2, также является string?

Как указывали выше, лучше всего создать свою собственную структуру данных, которая использует один или два словаря за кулисами. Например, если вы заранее знали, что хотите использовать string и int в качестве своих ключей, вы можете использовать этот подход:

public class StringIntDictionary<TValue> : IDictionary<string, TValue>, IDictionary<int, TValue>
{
    private IDictionary<object, TValue> _dictionary = new Dictionary<object, TValue>();
    // implement interface below, delegate to _dictionary
}

Это позволит вам использовать клавиши string и int:

var dict = StringIntDictionary<bool>();
dict["abc"] = true;
dict[123] = true;

Ответ 11

В качестве локального решения я использую простой подход:

Представьте, что у меня есть набор продуктов, обозначенных строкой и формой с кнопками для каждого из продуктов.

При управлении состоянием кнопок мне нужно найти кнопки с помощью строкового ключа. При обработке кликов мне нужно найти идентификаторы продуктов с помощью экземпляра кнопки.

Вместо сохранения двух отдельных словарей я делаю следующее:

public class SPurchaseOption
{
    public Button Button;
    public string ProductID;
    public string SomeOtherAssociatedData;
}

Dictionary<object, SPurchaseOption> purchaseOptions;

Когда кнопки инициализируются, я добавляю две записи в Dictionary i.e.

Key: ProductID, Value: "SPurchaseOption"
Key: Button,    Value: "SPurchaseOption"

Для более общего подхода и если вам нужен обычно используемый компонент, вам придется построить обертку вокруг двух словарей i.e:

public class DoubleKeyedDictionary<TKey1, TKey2, TValue>
{
    class SItem
    {
        public TKey1 key1;
        public TKey2 key2;
        public TValue value;
    }

    Dictionary<TKey1, SItem> dic1;
    Dictionary<TKey2, SItem> dic2;
}

это даст доступ как к значению, так и к альтернативному ключу с помощью любого из ключей.