公司系统中有一接口访问量大,内部计算逻辑较为复杂。在优化时打算把Request中的参数做为Key,Response做为Value放到进程内缓存中,以降低服务器压力,提高接口响应速度。因为Response中一些数据时效性要求较高,所以缓存设置一个较短的过期时间(比如10s)。
但这里牵涉到一个问题,如何有效的判断两次请求的参数是相等的。C#中自定义类型会从Object类继承Equals和GetHashCode两个方法,可以根据实际需求来重写这两个方法实现对象相等性比较。
Object.Equals(Object)
.NET 中不同类型对于Equals方法的默认实现如下:
| Type category | Equality defined by | Comments |
|---|---|---|
| Class derived directly from Object | Object.Equals(Object) | Reference equality; equivalent to calling Object.ReferenceEquals. |
| Structure | ValueType.Equals | Value equality; either direct byte-by-byte comparison or field-by-field comparison using reflection. |
| Enumeration | Enum.Equals | Values must have the same enumeration type and the same underlying value. |
| Delegate | MulticastDelegate.Equals | Delegates must have the same type with identical invocation lists. |
| Interface | Object.Equals(Object) | Reference equality. |
Object
通过源码,可以看到Object中Equals方法的实现,即.NET中所有类型的默认实现:

ValueType
反编译之后,可以看到ValueType中Equals方法的实现,即值类型的默认实现,它重写了Object.Equals方法:

上面可以看到,ValueType中Equals实现思路如下:
-
obj==null返回false
-
若this和obj的运行时类型不同则返回false
-
如果值类型中包含的字段均是值类型则逐字节比较字段值
-
若含有引用类型字段,则使用使用反射获取字段信息,然后调用字段的Equals方法来逐字段比较相等性
重写Equals
Object的Equals仅通过引用来比较相等性。应该说是identity而非equality,与Python中的is、== 操作符类似;ValueType的Equals中使用了反射性能较差。这种默认实现通常不能满足需求,自定义实现Equals思路如下:
-
obj为null,返回false,因为Equals是实例方法,this不会为null
-
对于引用类型,this和obj引用同一个对象返回true
-
调用GetType方法来判断this和obj在运行时是否是相同类型
-
必要时调用基类的Equals方法来比较基类中字段的相等性(通常不调用Object类的Equals)
-
调用Equals方法逐字段进行比较
根据上述思路,实现自定义类型的Equals方法:
public class Entity { public Entity(string tag, int count, IDictionary<string, string> descriptioins) { this.Tag = tag; this.Count = count; this.Descriptions = descriptioins; } public string Tag { private set; get; } public int Count { private set; get; } public IDictionary<string, string> Descriptions { private set; get; } /// <summary> /// 逐字段比较相等性 /// </summary> public override bool Equals(object obj) { if (obj == null) { return false; } if (object.ReferenceEquals(this, obj)) { return true; } // 这里判断this与obj在运行时类型是否一样 // 使用is关键字进行类型判断的话,如果obj是Entity的子类也会返回true // 如果类型被标记为sealed,可以使用is来判断 if (this.GetType().Equals(obj.GetType()) == false) { return false; } var other = obj as Entity; if (other == null) { return false; } if (this.Tag != other.Tag) { return false; } if (this.Count != other.Count) { return false; } if (this.Descriptions.FieldsEquales(other.Descriptions) == false) { return false; } return true; } /// <summary> /// 得到的哈希值应在对象生命周期中保持不变 /// </summary> public override int GetHashCode() => this.ToString().GetHashCode(); /// <summary> /// 含义同Equals(object obj) /// </summary> public static bool operator ==(Entity left, Entity right) { // The null keyword is a literal that represents a null reference, one that does not refer to any object. // null is the default value of reference - type variables.Ordinary value types cannot be null, except for nullable value types. if (object.ReferenceEquals(left, null)) { return object.ReferenceEquals(right, null); } return left.Equals(right); } /// <summary> /// 含义与==相反 /// </summary> public static bool operator !=(Entity left, Entity right) => !(left == right); public override string ToString() => JsonConvert.SerializeObject(this); }
public static class DictionaryExtension { /// <summary> /// 调用Object.Equals(Object)方法逐个字段进行相等性比较 /// <para>双方均为null时返回true,一方为null是返回false</para> /// </summary> public static bool FieldsEquals<TKey, TValue>(this IDictionary<TKey, TValue> source, IDictionary<TKey, TValue> target) { if (source == null && target == null) { return true; } if (source == null || target == null) { return false; } if (object.ReferenceEquals(source, target)) { return true; } if (source.Keys.Count != target.Keys.Count) { return false; } foreach (var key in source.Keys) { if (target.ContainsKey(key) == false) { return false; } var sourceValue = source[key]; var targetValue = target[key]; if (object.ReferenceEquals(sourceValue, null)) { if (object.ReferenceEquals(targetValue, null)) { continue; } return false; } if (sourceValue.Equals(targetValue)) { continue; } return false; } return true; } }