元组vs字符串作为C#中的字典键

Sha*_*har 27 .net c# caching equality concurrentdictionary

我有一个使用ConcurrentDictionary实现的缓存,我需要保留的数据取决于5个参数.所以从缓存中获取它的方法是:(为简单起见,这里只显示3个参数,我更改了数据类型以表示CarData的清晰度)

public CarData GetCarData(string carModel, string engineType, int year);
Run Code Online (Sandbox Code Playgroud)

我想知道在我的ConcurrentDictionary中使用哪种类型的密钥会更好,我可以这样做:

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

或者像这样:

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

我不会将这些参数与其他任何地方一起使用,因此没有理由创建一个类来保持它们在一起.

我想知道哪种方法在性能和可维护性方面更好.

InB*_*een 21

我想知道哪种方法在性能和可维护性方面更好.

和往常一样,你有工具来解决它.编写两种可能的解决方案并让它们竞争.获胜的是赢家,你不需要任何人在这里回答这个特定的问题.

关于维护,自动文档更好,具有更好的可扩展性的解决方案应该是赢家.在这种情况下,代码是如此微不足道,以至于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";
    
    Run Code Online (Sandbox Code Playgroud)

    是的,远远不够,但理论上,完全有可能,你的问题恰恰是未来的未知事件,所以......

  • 最后,但并非最不重要的是,编译器可以帮助您维护代码.例如,如果明天你必须添加param4到你的密钥,Tuple<T1, T2, T3, T4>将强烈键入你的密钥.另一方面,你的字符串连接算法可以生活在幸福快乐的生成密钥上param4,你不会知道发生什么事情,直到你的客户打电话给你,因为他们的软件没有按预期工作.


Tim*_*ter 14

您可以创建一个覆盖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;
    }
}
Run Code Online (Sandbox Code Playgroud)

如果你不重写那些,ContainsKey会引用等于.

注意:Tuple该类确实有自己的相等函数,基本上与上面相同.使用定制类可以清楚地表明发生了什么 - 因此更易于维护.它还有一个优点,你可以命名属性,使其清晰

注意2:该类是不可变的,因为字典键需要避免在将对象添加到字典后更改哈希码的潜在错误.请参阅此处

GetHashCode取自此处

  • 虽然这个答案很好,但我建议`Key`实现`IEquatable <Key>`,它可以更好地自动记录等式检查的类型值语义. (4认同)
  • 这个类真的应该是不可变的. (4认同)
  • 我不太明白冗余类型如何更好地维护.你的例子对元组没有任何意义.你需要什么"完全控制比较"?你方便地跳过了一个非常重要的事情,即实现`GetHashCode`.它似乎是明确地重新实现元组的存根,只是为了冗长.在架构中引入不必要的实体并不是一件好事.这种方法很好,但前提是现有的标准类型还不够. (2认同)

svi*_*ick 10

如果性能非常重要,那么答案是您不应该使用任何一个选项,因为两者都会在每次访问时不必要地分配一个对象.

相反,您应该使用struct自定义或ValueTupleSystem.ValueTuple包:

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

C#7.0还包含语法糖,使这个代码更容易编写(但你不需要等待C#7.0开始使用ValueTuple没有糖):

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


the*_*Dmi 6

实现自定义键类并确保它适用于此类用例,即实现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;
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

这是GetHashCode()Resharper如何生成它的实现.这是一个很好的通用实现.根据需要进行调整.


或者,使用自动生成和实现的Equ(我是该库的创建者)之类的东西.这将确保这些方法始终包含类的所有成员,因此代码变得更容易维护.这样的实现就像这样:EqualsGetHashCodeCacheKey

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; }
}
Run Code Online (Sandbox Code Playgroud)

注意:您显然应该使用有意义的属性名称,否则引入自定义类并不比使用自定义类提供更多好处Tuple.