zoukankan      html  css  js  c++  java
  • 避免陷阱,重写Equals方法您需要注意的其中2个原则

    以下代码源自于真实项目,本人只是做了一点简化,大家来找碴,看看哪些地方不妥:

    PathedObject

    此前一文没有发布在首页浏览量果然少的可怜,而且没有得到反馈也不知是否大家都明白问题出在哪里,或者我贴的代码过于羞涩,难以理解。

    但我相信大部分熟读过《CLR Via C#》一书的人应该明白问题出在哪里,因为道理都在那本书里摆着。

    至于我为什么写此文重谈一遍,一个是因为读书归读书,碰到实际情况时就不见得也能保持冷静明白个所以然,能够避免踩此陷阱; 二则我也很难理解我们的架构师为什么会犯此错误,是故意的呢还是不够仔细踩了地雷。于是写此文记录一下,以免今后自己犯此错误。

    转入正题,如果不够仔细的话,如果不是提交了一个ChangeList 将存放PathedObject集合用Dictionary替换了先前的List造成了功能回退的话,也许我不会留意上面的代码。然当我仔细浏览上面代码的时候,造成功能回退的原因也就很好理解了。

    原则一:GetHashCode()方法跟Equals()方法应该保持一致

    我们都知道重写了Equals方法,必须要重写GetHashCode,要不然就会有个编译警告。如果两个对象视为相同, 它们就必须要返回相同的HashCode, 这个实际上是由HashTable原理决定的。

    Just Reflect 一问中我借助反射器简单阐述了Hashtable的Contains方法实现的方式:在调用Contains方法时,其首先会用参数对象的HashCode去判断对应的Hashtable槽有没有被占用,如果没有就会直接返回false,有则会调用Equals方法判断槽里的对象跟参数是不是一致,然后。。。

    了解这一点,我们也就不会对于下面的测试代码的结果感到诧异:

     static void Main(string[] args)
     
    {
         
    object a = "A";
         
    object b = "B";
         
    object c = "C";
         
    object d = "D";

         PathedObject pathOne 
    = new PathedObject(d, new object[] { a, b, d });
         PathedObject pathTwo 
    = new PathedObject(d, new object[] { a, c, d });
         PathedObject pathThree 
    = new PathedObject(pathOne.Object);

         Dictionary
    <PathedObject, object> dic = new Dictionary<PathedObject, object>();
         dic.Add(pathOne, pathOne.Object);

         List
    <PathedObject> list = new List<PathedObject>();
         list.Add(pathOne);

         Console.WriteLine(dic.ContainsKey(pathThree));   
    // 'False'

         Console.WriteLine(list.Contains(pathThree));     
    // 'True'

         Console.ReadLine();
     }

    用路径D 去Dictionary中查找时无一被命中,然而在List里却能到找。这是因为在于Dictionary和List 各自的Contains方法实现的方式不同, 在List里它只会逐一遍历调用Equals方法判等,找到就返回。

    这就解释了为什么在我们项目中将存放PathedObject的集合用Dictionary替换了先前的List造成了Regression。因为根据目前GetHashCode方法,路径A->B->D, 跟路径D的产生的HashCode是不同的,这将导致调用系统封装后的Contains方法返回值由原先的True变成了False,导致了Client端走了不同的流程从而造成了功能回退。

    既然我们不能违背原则一,是不是我们只要简单修改下GetHashCode的实现让路径A->B->D跟路径D的PathedObject返回相同的HashCode就能解决此问题,例如如下的方法:

    public override int GetHashCode()
    {
        return Object.GetHashCode();
    }

    问题是不是能解决了呢?如果重新Run我们的应用程序,你会惊喜的发现问题得到解决了,结果和我们期待的一样。于是提交代码,皆大欢喜?

    如果你多个心眼你会发现原来的GetHashCode实现视乎更合理些,那么是不是有其他地方不妥呢?仔细再审视一遍PathedObject实现,我们会发现其实真正的问题在于Equals实现的逻辑。

     

    原则二:Equals方法须满足传递性。假设调用A.Equals(B),B.Equals(C)均返回true, 那么调用A.Equals(C)也必须返回true

    显然根据目前的代码违背了这条原则:

    假设有: Path 1 = A->B->D
                Path 2 = A->C->D
                Path 3 = D

    根据上面Equals实现的方式可以得到 Path 1 == Path 3; Path 3 == Path 2, 而Path 1 != Path 2,这就是问题的所在。

     

    下面略带解释下PathedObject实现方式,和猜测下为什么Equals方法按目前的方式实现,希望对大家理解有帮助。

    我们的产品为一3D应用软件,拥有很多的Instance,一个Instance包含了大量的几何信息,Instance需要能被重用。这个关系好比一个汽车有4个轮子,而每个轮子的几何信息是相同的,我们没理由去保持四份这样的几何信息。4个轮子虽然本身构造没有区别,但他们出现位置是有区别的,前面,后面,左侧,右侧,于是我们引入了一个PathedObject标识他们,PathedObject对象分别包含了这个Instance和其的路径信息,这样既能确保了只需保存一份几何信息,又能标识不同的实例对象。我想这可能是很多三维软件的通用做法。

    而至于认为路径A->B->D, 跟路径D相同,我想这里可能是为了贪图方便。因为在系统底层我们保留的都是PathedObject对象,比方说我们用鼠标去Pick,那么是很容易得到当前位置对应的Instance的详细路径并加以保持。而对于上层可能希望更加关注于Instance对象本身而淡化其Path, 因此才会用如下的调用,并且希望能返回True。

    PathedObject pathThree = new PathedObject(pathOne.Object);

    PathedObjectList.Contains(pathThree)

     

    最后, 原则三,应避免将HashCode保存到持久层

    不幸的是我们的框架也违反了这一条,至于其中的道理还是请各位看官参阅《CLR Via C#》第五章吧,里面还有很多g更多重写Equals方法需要注意的地方,而且通俗易懂。

  • 相关阅读:
    September 29th 2017 Week 39th Friday
    September 28th 2017 Week 39th Thursday
    September 27th 2017 Week 39th Wednesday
    September 26th 2017 Week 39th Tuesday
    September 25th 2017 Week 39th Monday
    September 24th 2017 Week 39th Sunday
    angular2 学习笔记 ( Form 表单 )
    angular2 学习笔记 ( Component 组件)
    angular2 学习笔记 ( Http 请求)
    angular2 学习笔记 ( Router 路由 )
  • 原文地址:https://www.cnblogs.com/anders06/p/1586093.html
Copyright © 2011-2022 走看看