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

Tuple vs string как ключ словаря в С#

У меня есть кеш, который я реализую с помощью ConcurrentDictionary, Данные, которые мне нужно сохранить, зависят от 5 параметров. Таким образом, метод, чтобы получить его из кеша: (для простоты я показываю только 3 параметра, и я изменил тип данных для представления CarData для ясности)

public CarData GetCarData(string carModel, string engineType, int year);

Интересно, какой тип ключа лучше использовать в моем ConcurrentDictionary, я могу сделать это вот так:

var carCache = new ConcurrentDictionary<string, CarData>();
// check for car key
bool exists = carCache.ContainsKey(string.Format("{0}_{1}_{2}", carModel, engineType, year);

Или вот так:

var carCache = new ConcurrentDictionary<Tuple<string, string, int>, CarData>();
// check for car key
bool exists = carCache.ContainsKey(new Tuple(carModel, engineType, year));

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

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

4b9b3361

Ответ 1

Вы можете создать класс (не имеет значения, что он используется только здесь), который переопределяет GetHashCode и Equals:

Спасибо theDmi (и другим) за улучшения...

public class CarKey : IEquatable<CarKey>
{
    public CarKey(string carModel, string engineType, int year)
    {
        CarModel = carModel;
        EngineType= engineType;
        Year= year;
    }

    public string CarModel {get;}
    public string EngineType {get;}
    public int Year {get;}

    public override int GetHashCode()
    {
        unchecked // Overflow is fine, just wrap
        {
            int hash = (int) 2166136261;

            hash = (hash * 16777619) ^ CarModel?.GetHashCode() ?? 0;
            hash = (hash * 16777619) ^ EngineType?.GetHashCode() ?? 0;
            hash = (hash * 16777619) ^ Year.GetHashCode();
            return hash;
        }
    }

    public override bool Equals(object other)
    {
        if (ReferenceEquals(null, other)) return false;
        if (ReferenceEquals(this, other)) return true;
        if (other.GetType() != GetType()) return false;
        return Equals(other as CarKey);
    }

    public bool Equals(CarKey other)
    {
        if (ReferenceEquals(null, other)) return false;
        if (ReferenceEquals(this, other)) return true;
        return string.Equals(CarModel,obj.CarModel) && string.Equals(EngineType, obj.EngineType) && Year == obj.Year;
    }
}

Если вы не переопределите их, ContainsKey делает ссылку равным.

Примечание: класс Tuple имеет свои собственные функции равенства, которые в основном будут делать то же, что и выше. Использование класса на заказ дает понять, что это то, что должно произойти, и, следовательно, лучше для ремонтопригодности. Это также имеет то преимущество, что вы можете назвать свойства, чтобы было ясно.

Примечание 2: класс неизменен, поскольку словарные ключи должны быть во избежание возможных ошибок с изменением хэш-кодов после добавления объекта в словарь См. здесь

GetHashCode, взятый отсюда

Ответ 2

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

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

О техническом обслуживании должно быть лучшим решением, которое самодокументирует себя лучше и обладает лучшей масштабируемостью. В этом случае код настолько тривиален, что автодокументация не так уж и важна. С точки зрения масштабируемости, IMHO, лучшим решением является использование Tuple<T1, T2, ...>:

  • Вы получаете свободную семантику равенства, которую вам не нужно поддерживать.
  • Коллизии невозможны, что неверно, если вы выберете решение конкатенации строк:

    var param1 = "Hey_I'm a weird string";
    var param2 = "!"
    var param3 = 1;
    key = "Hey_I'm a weird string_!_1";
    
    var param1 = "Hey";
    var param2 = "I'm a weird string_!"
    var param3 = 1;
    key = "Hey_I'm a weird string_!_1";
    

    Да, далека, но, теоретически, вполне возможно, и ваш вопрос точно о неизвестных событиях в будущем, поэтому...

  • И последнее, но не менее важное: компилятор помогает вам поддерживать код. Если, например, завтра вы должны добавить param4 к вашему ключу, Tuple<T1, T2, T3, T4> будет сильно набирать ваш ключ. С другой стороны, ваш алгоритм конкатенации строк может жить блаженно счастливыми ключами генерации без param4, и вы не будете знать, что происходит до тех пор, пока ваш клиент не вызовет вас, потому что их программное обеспечение работает не так, как ожидалось.

Ответ 3

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

Вместо этого вы должны использовать struct, либо настраиваемый, либо ValueTuple из пакет System.ValueTuple:

var myCache = new ConcurrentDictionary<ValueTuple<string, string, int>, CachedData>();
bool exists = myCache.ContainsKey(ValueTuple.Create(param1, param2, param3));

С# 7.0 также содержит синтаксис сахара, чтобы сделать этот код проще для записи (но вам не нужно ждать, когда С# 7.0 начнет использовать ValueTuple без сахара):

var myCache = new ConcurrentDictionary<(string, string, int), CachedData>();
bool exists = myCache.ContainsKey((param1, param2, param3));

Ответ 4

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

public class CacheKey : IEquatable<CacheKey>
{
    public CacheKey(string param1, string param2, int param3)
    {
        Param1 = param1;
        Param2 = param2;
        Param3 = param3;
    }

    public string Param1 { get; }

    public string Param2 { get; }

    public int Param3 { get; }

    public bool Equals(CacheKey other)
    {
        if (ReferenceEquals(null, other)) return false;
        if (ReferenceEquals(this, other)) return true;
        return string.Equals(Param1, other.Param1) && string.Equals(Param2, other.Param2) && Param3 == other.Param3;
    }

    public override bool Equals(object obj)
    {
        if (ReferenceEquals(null, obj)) return false;
        if (ReferenceEquals(this, obj)) return true;
        if (obj.GetType() != GetType()) return false;
        return Equals((CacheKey)obj);
    }

    public override int GetHashCode()
    {
        unchecked
        {
            var hashCode = Param1?.GetHashCode() ?? 0;
            hashCode = (hashCode * 397) ^ (Param2?.GetHashCode() ?? 0);
            hashCode = (hashCode * 397) ^ Param3;
            return hashCode;
        }
    }
}

Это реализация GetHashCode(), которую создает Resharper. Это хорошая реализация общего назначения. При необходимости адаптируйте.


В качестве альтернативы, используйте что-то вроде Equ (я являюсь создателем этой библиотеки), который автоматически генерирует Equals и GetHashCode реализации. Это позволит убедиться, что эти методы всегда включают всех членов класса CacheKey, поэтому код становится намного проще поддерживать. Тогда такая реализация будет выглядеть следующим образом:

public class CacheKey : MemberwiseEquatable<CacheKey>
{
    public CacheKey(string param1, string param2, int param3)
    {
        Param1 = param1;
        Param2 = param2;
        Param3 = param3;
    }

    public string Param1 { get; }

    public string Param2 { get; }

    public int Param3 { get; }
}

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

Ответ 5

Я хотел сравнить методы Tuple versus Class и id_id_id, описанные в других комментариях. Я использовал этот простой код:

public class Key : IEquatable<Key>
{
    public string Param1 { get; set; }
    public string Param2 { get; set; }
    public int Param3 { get; set; }

    public bool Equals(Key other)
    {
        if (ReferenceEquals(null, other)) return false;
        if (ReferenceEquals(this, other)) return true;
        return string.Equals(Param1, other.Param1) && string.Equals(Param2, other.Param2) && Param3 == other.Param3;
    }

    public override bool Equals(object obj)
    {
        if (ReferenceEquals(null, obj)) return false;
        if (ReferenceEquals(this, obj)) return true;
        if (obj.GetType() != this.GetType()) return false;
        return Equals((Key) obj);
    }

    public override int GetHashCode()
    {
        unchecked
        {
            var hashCode = (Param1 != null ? Param1.GetHashCode() : 0);
            hashCode = (hashCode * 397) ^ (Param2 != null ? Param2.GetHashCode() : 0);
            hashCode = (hashCode * 397) ^ Param3;
            return hashCode;
        }
    }
}

static class Program
{

    static void TestClass()
    {
        var stopwatch = new Stopwatch();
        stopwatch.Start();
        var classDictionary = new Dictionary<Key, string>();

        for (var i = 0; i < 10000000; i++)
        {
            classDictionary.Add(new Key { Param1 = i.ToString(), Param2 = i.ToString(), Param3 = i }, i.ToString());
        }
        stopwatch.Stop();
        Console.WriteLine($"initialization: {stopwatch.Elapsed}");

        stopwatch.Restart();

        for (var i = 0; i < 10000000; i++)
        {
            var s = classDictionary[new Key { Param1 = i.ToString(), Param2 = i.ToString(), Param3 = i }];
        }

        stopwatch.Stop();
        Console.WriteLine($"Retrieving: {stopwatch.Elapsed}");
    }

    static void TestTuple()
    {
        var stopwatch = new Stopwatch();
        stopwatch.Start();
        var tupleDictionary = new Dictionary<Tuple<string, string, int>, string>();

        for (var i = 0; i < 10000000; i++)
        {
            tupleDictionary.Add(new Tuple<string, string, int>(i.ToString(), i.ToString(), i), i.ToString());
        }
        stopwatch.Stop();
        Console.WriteLine($"initialization: {stopwatch.Elapsed}");

        stopwatch.Restart();

        for (var i = 0; i < 10000000; i++)
        {
            var s = tupleDictionary[new Tuple<string, string, int>(i.ToString(), i.ToString(), i)];
        }

        stopwatch.Stop();
        Console.WriteLine($"Retrieving: {stopwatch.Elapsed}");
    }

    static void TestFlat()
    {
        var stopwatch = new Stopwatch();
        stopwatch.Start();
        var tupleDictionary = new Dictionary<string, string>();

        for (var i = 0; i < 10000000; i++)
        {
            tupleDictionary.Add($"{i}_{i}_{i}", i.ToString());
        }
        stopwatch.Stop();
        Console.WriteLine($"initialization: {stopwatch.Elapsed}");

        stopwatch.Restart();

        for (var i = 0; i < 10000000; i++)
        {
            var s = tupleDictionary[$"{i}_{i}_{i}"];
        }

        stopwatch.Stop();
        Console.WriteLine($"Retrieving: {stopwatch.Elapsed}");
    }

    static void Main()
    {
        TestClass();
        TestTuple();
        TestFlat();
    }
}

Результаты:

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

TestTuple:

initialization: 00:00:14.2512736
Retrieving: 00:00:08.1912167

TestClass:

initialization: 00:00:11.5091160
Retrieving: 00:00:05.5127963

TestFlat:

initialization: 00:00:16.3672901
Retrieving: 00:00:08.6512009

Я был удивлен, увидев, что подход класса был быстрее, чем подход кортежа и строковый подход. По-моему, это более читаемо и более безопасно в будущем, в том смысле, что в класс Key можно добавить больше функциональности (предполагая, что это не просто ключ, он представляет что-то).

Ответ 6

IMHO, я предпочитаю использовать в таких случаях некоторую промежуточную структуру (в вашем случае это будет Tuple). Такой подход создает дополнительный уровень между параметрами и конечным пользователем. Конечно, это будет зависеть от целей. Такой способ позволяет, например, создать не тривиальный переход параметров (например, контейнер может "исказить" данные).

Ответ 7

Наличие этих параметров, используемых в качестве сложного ключа, является хорошим оправданием, чтобы объединить их в класс по своему вкусу. Класс Tuple <... > as is не подходит для целей словаря, так как его реализация GetHashCode всегда возвращает 0. Поэтому вам нужно будет создать новый класс, который либо получен из Tuple, либо нового контейнера. С точки зрения производительности все зависит от эффективности реализации GetHashCode и Equals.