Кортежі (або масиви) як клавіші словника в C #


109

Я намагаюся зробити таблицю пошуку словника в C #. Мені потрібно розв’язати 3-ма значення значень на один рядок. Я спробував використовувати масиви як ключі, але це не вийшло, і я не знаю, що ще робити. На даний момент я розглядаю можливість створення словника словників словників, але це, мабуть, не дуже сильно дивитись, хоча саме так я б це робив у javascript.

Відповіді:


113

Якщо ви перебуваєте на .NET 4.0, використовуйте кортеж:

lookup = new Dictionary<Tuple<TypeA, TypeB, TypeC>, string>();

Якщо ні, ви можете визначити a Tupleі використовувати його як ключ. Кортеж потрібно перекрити GetHashCode, Equalsі IEquatable:

struct Tuple<T, U, W> : IEquatable<Tuple<T,U,W>>
{
    readonly T first;
    readonly U second;
    readonly W third;

    public Tuple(T first, U second, W third)
    {
        this.first = first;
        this.second = second;
        this.third = third;
    }

    public T First { get { return first; } }
    public U Second { get { return second; } }
    public W Third { get { return third; } }

    public override int GetHashCode()
    {
        return first.GetHashCode() ^ second.GetHashCode() ^ third.GetHashCode();
    }

    public override bool Equals(object obj)
    {
        if (obj == null || GetType() != obj.GetType())
        {
            return false;
        }
        return Equals((Tuple<T, U, W>)obj);
    }

    public bool Equals(Tuple<T, U, W> other)
    {
        return other.first.Equals(first) && other.second.Equals(second) && other.third.Equals(third);
    }
}

6
Ця структура також повинна реалізовувати IEquatable <Tuple <T, U, W >>. Таким чином ви можете уникнути боксу, коли викликається рівність () у випадку зіткнення хеш-коду.
Дастін Кемпбелл

16
@jerryjvl та всі інші, хто вважає це за допомогою Google, як і я,. Tuple реалізує .NET 4 дорівнює, тому його можна використовувати у словнику.
Скотт Чемберлен

30
Ваша GetHashCodeреалізація не дуже хороша. Це інваріант під перестановкою полів.
CodesInChaos

2
Кортеж не повинен бути структурою. У рамках Tuple є еталонним типом.
Майкл Грацик

5
@Thoraot - звичайно, твій приклад помилковий ... так і має бути. Чому б new object()дорівнювати іншому new object()? Це не просто використання прямого порівняння порівняння ... спробуйте:bool test = new Tuple<int, string>(1, "foo").Equals(new Tuple<int, string>(1, "Foo".ToLower()));
Майк Мариновський

35

Між кортежними та вкладеними словниками підходами майже завжди краще переходити на кортеж.

З точки зору ремонту ,

  • набагато простіше реалізувати функціонал, який виглядає так:

    var myDict = new Dictionary<Tuple<TypeA, TypeB, TypeC>, string>();

    ніж

    var myDict = new Dictionary<TypeA, Dictionary<TypeB, Dictionary<TypeC, string>>>();

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

  • Крім того, якщо в майбутньому для вашого складового ключа потрібне ще одне (або менше) поле, у другому випадку (вкладений словник) вам знадобиться змінити значну партію, оскільки вам доведеться додавати додаткові вкладені словники та наступні перевірки.

З точки зору ефективності , найкращий висновок, який ви можете зробити, - це виміряти його самостійно. Але є кілька теоретичних обмежень, які ви можете заздалегідь врахувати:

  • У випадку вкладеного словника мати додатковий словник для кожної клавіші (зовнішньої та внутрішньої) матиме дещо накладну пам’ять (більше, ніж те, що створить кортеж).

  • У вкладеному словниковому випадку всі основні дії, такі як додавання, оновлення, пошук, видалення тощо, повинні виконуватись у двох словниках. Зараз є випадок, коли підхід до вкладеного словника може бути швидшим, тобто коли дані, які шукаються, відсутні, оскільки проміжні словники можуть обійти повний обчислення та порівняння хеш-коду, але знову ж таки це має бути приурочено до впевненості. За наявності даних це повинно бути повільніше, оскільки пошук повинен здійснюватися двічі (або тричі залежно від введення).

  • Що стосується кортежного підходу, то .NET кортежі не є найефективнішими, коли вони призначені для використання в якості ключів у наборах, оскільки його Equalsі GetHashCodeреалізація спричиняє бокс для типів значень .

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


Зі сторони, мало косметики може зробити словник крутим:

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

    string foo = dict[a, b, c]; //lookup
    dict[a, b, c] = ""; //update/insertion

    Тому виставляйте необхідні індексатори у вашому класі словника, який внутрішньо обробляє вставки та пошукові запити.

  2. Крім того, реалізуйте відповідний IEnumerableінтерфейс і надайте Add(TypeA, TypeB, TypeC, string)метод, який дасть вам синтаксис ініціалізатора колекції, наприклад:

    new MultiKeyDictionary<TypeA, TypeB, TypeC, string> 
    { 
        { a, b, c, null }, 
        ...
    };

У випадку з вкладеними словниками, чи не синтаксис індексатора був би таким, як такий string foo = dict[a][b][c]:?
Стівен Рандс

@StevenRands так, так і буде.
nawfal

1
@nawfal Чи можу я шукати словник кортежу, коли у мене є лише один із ключових не всіх? або Чи можу я зробити такий дикт [a, b], то диктувати [a, c]?
Інженер-хан

@KhanEngineer Багато чого залежить від того, якою є цільова мета словника або як ви його плануєте використовувати. Для , наприклад, ви хочете отримати значення назад на частини ключа a. Ви можете просто повторити будь-який словник, як і будь-яку звичайну колекцію, і перевірити наявність ключових властивостей, якщо він є a. Якщо ви завжди хочете отримати цей предмет у дикті за першою властивістю, тоді ви можете краще спроектувати словник як словник словників, як показано у моїй відповіді та запиті як dict[a], що дає вам ще один словник.
nawfal

Якщо під "пошуком лише однієї клавіші" ви маєте намір повернути значення за допомогою будь-якого з ваших клавіш, то вам краще переробити свій словник як свого роду "будь-який словник ключових слів". Наприклад, якщо ви хочете отримати значення 4обох клавіш aі b, тоді ви можете зробити його стандартним словником і додати значення, такі як dict[a] = 4і dict[b] = 4. Це може не мати сенсу, якщо логічно ваш aі bмає бути одна одиниця. У такому випадку ви можете визначити звичай, IEqualityComparerякий прирівнює два ключові екземпляри до рівних, якщо будь-які їх властивості рівні. Все це можна зробити загалом за допомогою перегляду.
nawfal

30

Якщо ви перебуваєте на C # 7, вам слід розглянути можливість використання кортезів значення як складового ключа. Кортежі цінності, як правило, забезпечують кращу ефективність, ніж традиційні довідкові кортежі ( Tuple<T1, …>), оскільки кортежі значень - це типи значень (структури), а не типові типи, тому вони уникають витрат на розподіл пам'яті та витрати на збирання сміття. Крім того, вони пропонують стисліший та інтуїтивніший синтаксис, дозволяючи назвати їх поля, якщо ви цього хочете. Вони також реалізують IEquatable<T>інтерфейс, необхідний для словника.

var dict = new Dictionary<(int PersonId, int LocationId, int SubjectId), string>();
dict.Add((3, 6, 9), "ABC");
dict.Add((PersonId: 4, LocationId: 9, SubjectId: 10), "XYZ");
var personIds = dict.Keys.Select(k => k.PersonId).Distinct().ToList();

13

Хороший, чистий, швидкий, легкий і зрозумілий спосіб:

  • генерувати члени рівності (Equals () та GetHashCode ()) для поточного типу. Такі інструменти, як ReSharper, не тільки створюють методи, але і генерують необхідний код для перевірки рівності та / або для обчислення хеш-коду. Створений код буде більш оптимальним, ніж реалізація Tuple.
  • просто зробіть простий клас ключів, похідний від кортежу .

додати щось подібне:

public sealed class myKey : Tuple<TypeA, TypeB, TypeC>
{
    public myKey(TypeA dataA, TypeB dataB, TypeC dataC) : base (dataA, dataB, dataC) { }

    public TypeA DataA => Item1; 

    public TypeB DataB => Item2;

    public TypeC DataC => Item3;
}

Тож ви можете використовувати його зі словником:

var myDictinaryData = new Dictionary<myKey, string>()
{
    {new myKey(1, 2, 3), "data123"},
    {new myKey(4, 5, 6), "data456"},
    {new myKey(7, 8, 9), "data789"}
};
  • Ви також можете використовувати його в контрактах
  • як ключ для приєднання або групування в linq
  • ідучи таким чином, ви ніколи не вводите помилковий порядок Item1, Item2, Item3 ...
  • тобі не потрібно пам’ятати чи шукати код, щоб зрозуміти, куди дістатись, щоб отримати щось
  • немає потреби переосмислювати IStructuralEquatable, IStructuralComparable, IComparable, ITuple вони всі тут alredy

1
Тепер ви можете використовувати виразних членів його ще більш чистих, наприкладpublic TypeA DataA => Item1;
andrewtatham

7

Якщо ви чомусь хочете уникати створення власного класу Tuple або використання вбудованого в .NET 4.0, можливий ще один підхід; ви можете об'єднати три ключові значення разом в одне значення.

Наприклад, якщо три значення є цілими типами разом, які не беруть більше 64 біт, ви можете об'єднати їх у a ulong.

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

string.Format("{0}#{1}#{2}", key1, key2, key3)

Очевидно, в цьому підході є якась композиція, але залежно від того, що ви для цього використовуєте, це може бути досить банально, щоб не піклуватися про це.


6
Я б сказав, що це сильно залежить від контексту; якби у мене було три комбіновані цілі типи, а продуктивність не була критичною, це прекрасно працює з мінімальними шансами помилитися. Звичайно, все це є надлишком, ніж у .NET 4, оскільки Microsoft надасть нам (мабуть правильно!) Типи кортежу поза коробкою.
jerryjvl

Ви навіть можете використовувати цей метод у поєднанні з a JavaScriptSerializerдля об'єднання масиву рядків та / або цілих типів для вас. Таким чином, вам не потрібно самостійно придумувати роздільник.
бінкі

3
Це може отримати реальний Messy , якщо якісь - або з клавіша ( key1, key2, key3) були рядки , що містять deliminator ( "#")
Greg

4

Я б замінив ваш Tuple правильним GetHashCode і просто використав його як ключ.

Поки ви перевантажуєте належними методами, ви повинні бачити гідну продуктивність.


1
IComparable не впливає на те, як ключі зберігаються або розміщуються в словнику <TKey, TValue>. Все це робиться через GetHashCode () та IEqualityComparer <T>. Реалізація IEquatable <T> дозволить досягти кращої продуктивності, оскільки це полегшує бокс, викликаний типовим рівнем EquomComparer, який повертається до функції Equals (object).
Дастін Кемпбелл

Я збирався згадати GetHashCode, але я подумав, що Словник використовує НЕпорівнянний випадок, якщо HashCodes були ідентичними ... здогадуюсь, я помилявся.
Джон Гітцен

3

Ось .NET кортеж для довідки:

[Serializable] 
public class Tuple<T1, T2, T3> : IStructuralEquatable, IStructuralComparable, IComparable, ITuple {

    private readonly T1 m_Item1; 
    private readonly T2 m_Item2;
    private readonly T3 m_Item3; 

    public T1 Item1 { get { return m_Item1; } }
    public T2 Item2 { get { return m_Item2; } }
    public T3 Item3 { get { return m_Item3; } } 

    public Tuple(T1 item1, T2 item2, T3 item3) { 
        m_Item1 = item1; 
        m_Item2 = item2;
        m_Item3 = item3; 
    }

    public override Boolean Equals(Object obj) {
        return ((IStructuralEquatable) this).Equals(obj, EqualityComparer<Object>.Default);; 
    }

    Boolean IStructuralEquatable.Equals(Object other, IEqualityComparer comparer) { 
        if (other == null) return false;

        Tuple<T1, T2, T3> objTuple = other as Tuple<T1, T2, T3>;

        if (objTuple == null) {
            return false; 
        }

        return comparer.Equals(m_Item1, objTuple.m_Item1) && comparer.Equals(m_Item2, objTuple.m_Item2) && comparer.Equals(m_Item3, objTuple.m_Item3); 
    }

    Int32 IComparable.CompareTo(Object obj) {
        return ((IStructuralComparable) this).CompareTo(obj, Comparer<Object>.Default);
    }

    Int32 IStructuralComparable.CompareTo(Object other, IComparer comparer) {
        if (other == null) return 1; 

        Tuple<T1, T2, T3> objTuple = other as Tuple<T1, T2, T3>;

        if (objTuple == null) {
            throw new ArgumentException(Environment.GetResourceString("ArgumentException_TupleIncorrectType", this.GetType().ToString()), "other");
        }

        int c = 0;

        c = comparer.Compare(m_Item1, objTuple.m_Item1); 

        if (c != 0) return c; 

        c = comparer.Compare(m_Item2, objTuple.m_Item2);

        if (c != 0) return c; 

        return comparer.Compare(m_Item3, objTuple.m_Item3); 
    } 

    public override int GetHashCode() { 
        return ((IStructuralEquatable) this).GetHashCode(EqualityComparer<Object>.Default);
    }

    Int32 IStructuralEquatable.GetHashCode(IEqualityComparer comparer) { 
        return Tuple.CombineHashCodes(comparer.GetHashCode(m_Item1), comparer.GetHashCode(m_Item2), comparer.GetHashCode(m_Item3));
    } 

    Int32 ITuple.GetHashCode(IEqualityComparer comparer) {
        return ((IStructuralEquatable) this).GetHashCode(comparer); 
    }
    public override string ToString() {
        StringBuilder sb = new StringBuilder();
        sb.Append("("); 
        return ((ITuple)this).ToString(sb);
    } 

    string ITuple.ToString(StringBuilder sb) {
        sb.Append(m_Item1); 
        sb.Append(", ");
        sb.Append(m_Item2);
        sb.Append(", ");
        sb.Append(m_Item3); 
        sb.Append(")");
        return sb.ToString(); 
    } 

    int ITuple.Size { 
        get {
            return 3;
        }
    } 
}

3
Отримати хеш-код реалізовано як ((item1 ^ item2) * 33) ^ item3
Michael

2

Якщо ваш споживчий код може робити інтерфейс IDictionary <>, а не словник, моїм інстинктом було б використовувати SortedDictionary <> зі спеціальним порівняльником масиву, тобто:

class ArrayComparer<T> : IComparer<IList<T>>
    where T : IComparable<T>
{
    public int Compare(IList<T> x, IList<T> y)
    {
        int compare = 0;
        for (int n = 0; n < x.Count && n < y.Count; ++n)
        {
            compare = x[n].CompareTo(y[n]);
        }
        return compare;
    }
}

І створити таким чином (використовуючи int [] лише заради конкретного прикладу):

var dictionary = new SortedDictionary<int[], string>(new ArrayComparer<int>());
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.