提问人:Shahar 提问时间:1/30/2017 最后编辑:Shahar 更新时间:2/28/2018 访问量:15325
元组与字符串作为 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));
我不会在其他地方同时使用这些参数,因此没有理由仅仅为了将它们放在一起而创建一个类。
我想知道哪种方法在性能和可维护性方面更好。
答:
恕我直言,在这种情况下,我更喜欢使用一些中间结构(在您的情况下将是).这种方法在参数和最终目标字典之间创建了额外的层。当然,这将取决于目的。例如,这种方式允许您创建不平凡的参数转换(例如,容器可能会“扭曲”数据)。Tuple
您可以创建一个重写 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:该类是不可变的,因为字典键需要避免在将对象添加到字典后哈希码更改的潜在错误 请参阅此处
评论
Key
IEquatable<Key>
GetHashCode
我想知道哪种方法在性能和可维护性方面更好。
与往常一样,您拥有解决问题的工具。对两种可能的解决方案进行编码,并使它们竞争。赢家就是赢家,你不需要任何人在这里回答这个特定的问题。
关于维护,能够更好地自动记录自己并具有更好可扩展性的解决方案应该是赢家。在这种情况下,代码是如此微不足道,以至于自动文档并不是一个大问题。从可扩展性的角度来看,恕我直言,最好的解决方案是使用: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
评论
实现自定义键类并确保它适用于此类用例,即实现 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;
}
}
}
这是 Resharper 生成它的实现方式。这是一个很好的通用实现。根据需要进行调整。GetHashCode()
或者,使用像 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
Item1
Item2
Param1
Param2
GetHashCode
IStructuralEquatable
Tuple<>
我想比较其他评论中描述的与“id_id_id”方法。我使用了这个简单的代码:Tuple
Class
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 次运行的平均值,但无论如何都没有太大的差异。
测试元组:
initialization: 00:00:14.2512736
Retrieving: 00:00:08.1912167
测试类:
initialization: 00:00:11.5091160
Retrieving: 00:00:05.5127963
测试平面:
initialization: 00:00:16.3672901
Retrieving: 00:00:08.6512009
我很惊讶地发现类方法比元组方法和字符串方法都快。在我看来,它更具可读性,更面向未来,从某种意义上说,可以向类中添加更多功能(假设它不仅仅是一个键,它代表了一些东西)。Key
评论
如果性能真的很重要,那么答案是不应使用任何一个选项,因为这两个选项都会在每次访问时不必要地分配一个对象。
相反,应使用 ,可以是自定义的,也可以是 System.ValueTuple 包中的:struct
ValueTuple
var myCache = new ConcurrentDictionary<ValueTuple<string, string, int>, CachedData>();
bool exists = myCache.ContainsKey(ValueTuple.Create(param1, param2, param3));
C# 7.0 还集成了语法 sugar,使此代码更易于编写(但您不需要等待 C# 7.0 在没有 sugar 的情况下开始使用):ValueTuple
var myCache = new ConcurrentDictionary<(string, string, int), CachedData>();
bool exists = myCache.ContainsKey((param1, param2, param3));
评论
我运行了 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}");
}
评论
GetHashCode
Equals
GetHashCode