Подтвердить что ты не робот

Использование класса или структуры в качестве словарного ключа

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

public class MyClass { }
public struct MyStruct { }

public Dictionary<MyClass, string> ClassDictionary;
public Dictionary<MyStruct, string> StructDictionary;

ClassDictionary = new Dictionary<MyClass, string>();
StructDictionary = new Dictionary<MyStruct, string>();

Почему это работает:

MyClass classA = new MyClass();
MyClass classB = new MyClass();
this.ClassDictionary.Add(classA, "Test");
this.ClassDictionary.Add(classB, "Test");

Но это приводит к сбоям во время выполнения:

MyStruct structA = new MyStruct();
MyStruct structB = new MyStruct();
this.StructDictionary.Add(structA, "Test");
this.StructDictionary.Add(structB, "Test");

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

4b9b3361

Ответ 1

  • new object() == new object() false, потому что ссылочные типы имеют ссылочное равенство, а два экземпляра не являются одинаковыми ссылками

  • new int() == new int() true, потому что типы значений имеют значение равенства, а значение двух значений по умолчанию - одно и то же значение. Обратите внимание, что если у вас есть ссылочные типы или значения по умолчанию, которые являются инкрементальными в вашей структуре, значения по умолчанию могут не сравнивать одинаковые значения для структур.

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

Кроме того, если вам нужен безопасный способ установить значение словаря, независимо от того, вы можете сделать dictionary[key] = value;, который добавит новые значения или обновит старые с помощью того же ключа.

Update

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

  • По умолчанию ссылочные типы 'Equals(object obj) и == вызывают оператор object.ReferenceEquals(this, obj) под капотом.

  • Операторы и методы экземпляра должны быть переопределены в конце концов для распространения поведения. (например, изменение реализации Equals не повлияет на реализацию ==, если только вложенный вызов не добавлен явно).

  • Все стандартные коллекторы .NET по умолчанию используют реализацию IEqualityComparer<T> для определения равенства (а не метода экземпляра). IEqualityComparer<T> может (и часто делает) вызов метода экземпляра в своей реализации, но это не то, на что вы можете рассчитывать. Для реализации IEqualityComparer<T> существует два возможных источника:

    • Вы можете явно указать его в конструкторе.

    • Он будет автоматически извлечен из EqualityComparer<T>.Default (по умолчанию). Если вы хотите настроить по умолчанию IEqualityComparer<T> глобально, к которому обращается EqualityComparer<T>.Default, вы можете использовать Undefault (на GitHub).

Ответ 2

Dictionary<TKey, TValue> использует IEqualityComparer<TKey> для сравнения ключей. Если вы не укажете явно компаратор при построении словаря, он будет использовать EqualityComparer<TKey>.Default.

Так как ни MyClass, ни MyStruct не реализуют IEquatable<T>, сопоставитель равенства по умолчанию вызовет Object.Equals и Object.GetHashCode для сравнения экземпляров. MyClass выводится из Object, поэтому реализация будет использовать ссылочное равенство для сравнения. MyStruct, с другой стороны, получен из System.ValueType (базовый класс всех структур), поэтому он будет использовать ValueType.Equals для сравнения экземпляров. В документации для этого метода указано следующее:

Метод ValueType.Equals(Object) переопределяет Object.Equals(Object) и предоставляет стандартную реализацию равенства значений для всех типов значений в .NET Framework.

Если ни одно из полей текущего экземпляра и obj не является ссылочным, метод Equals выполняет побайтное сравнение двух объектов в памяти. В противном случае он использует отражение для сравнения соответствующих полей obj и этого экземпляра.

Исключение происходит потому, что IDictionary<TKey, TValue>.Add выдает ArgumentException, если "Элемент с тем же ключом уже существует в [словаре]." При использовании structs побайтовое сравнение, выполняемое ValueType.Equals, приводит к тому, что оба вызова пытаются добавить один и тот же ключ.

Ответ 3

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

Для того, чтобы тип использовался в качестве словарного ключа, его методы Equals и GetHashCode должны иметь требуемую семантику, иначе конструктор Dictionary должен быть предоставлен IEqualityComparer<T>, который реализует желательная семантика. По умолчанию метод Equals и GetHashCode для классов будет указывать на идентификатор объекта (полезно, если вы хотите использовать идентификаторы изменяемых объектов, а не так полезно). Методы Equals и GetHashCode по умолчанию для типов значений обычно будут использовать методы Equals и GetHashCode своих членов, но с несколькими морщинами:

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

  • Структуры, которые содержат только примитивные типы, будут выполнять сравнения с плавающей запятой, отличные от тех, которые включают в себя другие типы. Например, значения posZero = (1.0/(1.0/0.0)) и negZero = (- 1.0/(1.0/0.0)) будут сравнивать друг с другом, но если они хранятся в структуре, содержащей только примитивы, они будут сравнивать неравные. Обратите внимание, что даже если сравнивать его с равными, они семантически не совпадают, так как вычисление 1.0/posZero даст положительную бесконечность, а 1.0/negZero даст отрицательную бесконечность.

Если производительность почти не критическая, можно определить простую структуру (просто объявить соответствующие общедоступные поля) и бросить ее в словарь и вести себя как ключ, основанный на значении. Это будет не очень эффективно, но это сработает. Словари, как правило, обрабатывают объекты неизменяемого класса несколько более эффективно, но определение и использование объектов неизменяемого класса иногда может быть более сложным, чем определение и использование "простых старых структур данных".

Ответ 4

Beause a struct не называется как class.

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

Поэтому, если вы попробуете это:

var a =  new MyStruct(){Prop = "Test"};
var b =  new MyStruct(){Prop = "Test"};

Console.WriteLine(a.Equals(b));

//будет печатать true

Если вы сделаете то же самое с классом:

var a =  new MyClass(){Prop = "Test"};
var b =  new MyClass(){Prop = "Test"};

Console.WriteLine(a.Equals(b));

//будет печатать false! (предполагая, что вы не реализовали некоторую функцию сравнения) beacuse ссылка не то же самое

Ответ 5

Ключ ссылочного типа (класс) указывает на отдельную ссылку; ключ типа значения (struct) указывает на одинаковые значения. Я бы подумал, почему вы получаете исключение.