[英]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:該類是不可變的,因為字典鍵需要避免在將對象添加到字典后更改哈希碼的潛在錯誤。 請參閱此處
如果性能非常重要,那么答案是您不應該使用任何一個選項,因為兩者都會在每次訪問時不必要地分配一個對象。
相反,您應該使用System.ValueTuple包中的自定義struct
或ValueTuple
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 (我是該庫的創建者)之類的東西來自動生成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
提供更多好處。
我想比較其他評論中描述的Tuple
與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();
}
}
我在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.