簡體   English   中英

元組vs字符串作為C#中的字典鍵

[英]Tuple vs string as a Dictionary key in C#

我有一個使用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));

我不會將這些參數與其他任何地方一起使用,因此沒有理由創建一個類來保持它們在一起。

我想知道哪種方法在性能和可維護性方面更好。

我想知道哪種方法在性能和可維護性方面更好。

和往常一樣,你有工具來解決它。 編寫兩種可能的解決方案並讓它們競爭 獲勝的是贏家,你不需要任何人在這里回答這個特定的問題。

關於維護,自動文檔更好,具有更好的可擴展性的解決方案應該是贏家。 在這種情況下,代碼是如此微不足道,以至於autodocumentation不是一個問題。 從可擴展性的角度來看,恕我直言,最好的解決方案是使用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幸福快樂生成密鑰上,你不知道發生什么事情,直到你的客戶打電話給你,因為他們的軟件沒有按預期工作。

您可以創建一個覆蓋GetHashCode和Equals的類(在此處僅使用它並不重要):

感謝Dmi(和其他人)的改進......

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取自此處

如果性能非常重要,那么答案是您不應該使用任何一個選項,因為兩者都會在每次訪問時不必要地分配一個對象。

相反,您應該使用System.ValueTuple包中的自定義structValueTuple struct

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

C#7.0還包含語法糖,使這個代碼更容易編寫(但你不需要等待C#7.0開始使用沒有糖的ValueTuple ):

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

實現自定義鍵類並確保它適用於此類用例,即實現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 (我是該庫的創建者)之類的東西來自動生成EqualsGetHashCode實現。 這將確保這些方法始終包含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提供更多好處。

我想比較其他評論中描述的TupleClass和“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();
    }
}

結果:

我在Release中運行了每個方法3次而沒有調試,每次運行都會注釋掉對其他方法的調用。 我拿了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類中添加更多功能(假設它不僅僅是一個鍵,它代表了一些東西)。

恕我直言,我更喜歡在這種情況下使用一些中間結構(在你的情況下它將是Tuple )。 這種方法在參數和結束目標字典之間創建了附加層。 當然,這取決於目的。 例如,這種方式允許您創建不是簡單的參數轉換(例如容器可能“扭曲”數據)。

我運行了Tomer的測試用例,添加了ValueTuples作為測試用例(新的c#值類型)。 對他們的表現印象深刻。

TestClass
initialization: 00:00:11.8787245
Retrieving: 00:00:06.3609475

TestTuple
initialization: 00:00:14.6531189
Retrieving: 00:00:08.5906265

TestValueTuple
initialization: 00:00:10.8491263
Retrieving: 00:00:06.6928401

TestFlat
initialization: 00:00:16.6559780
Retrieving: 00:00:08.5257845

測試代碼如下:

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

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

    stopwatch.Restart();

    for (var i = 0; i < n; i++)
    {
        var s = tupleDictionary[(i.ToString(), i.ToString(), i)];
    }

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

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM