ITPub博客

首页 > Linux操作系统 > Linux操作系统 > 避免陷阱,重现Equals方法您需要注意的其中2个原则

避免陷阱,重现Equals方法您需要注意的其中2个原则

原创 Linux操作系统 作者:iDotNetSpace 时间:2009-10-22 16:15:11 0 删除 编辑

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


<!--

Code highlighting produced by Actipro CodeHighlighter (freeware)
http://www.CodeHighlighter.com/

--> public class PathedObject
 
{
     
public PathedObject(object obj)
     
{
         Object 
= obj;
         ObjectPath 
= new object[0];
     }


     
public PathedObject(object obj, object[] path)
     
{
         System.Diagnostics.Debug.Assert(path 
!= null);
         Object 
= obj;
         ObjectPath 
= path;
     }


     
public object Object
     
{
         
get;
         
set;
     }

     
public object[] ObjectPath
     
{
         
get;
         
internal set;
     }


     
/// 
     
/// Rules:
     
/// Suppose Path 1 = A->B->D
     
///         Path 2 = A->C->D
     
///         Path 3 = D
     
///There are :
     
///        Path 1 == Path 3
     
///        Path 1 != Path 2
     
/// 

     
/// 
     
/// 

     public override bool Equals(object obj)
     
{
         PathedObject other 
= obj as PathedObject;
         
if (other == null)
             
return false;

         
if (this.ObjectPath.Length > 0 && other.ObjectPath.Length > 0)
         
{
             
if (!this.Object.Equals(other.Object))
                 
return false;

             
for (int i = 0; i < ObjectPath.Length; i++)
             
{
                 
if (!ObjectPath[i].Equals(other.ObjectPath[i]))
                     
return false;
             }

             
return true;
         }

         
else
             
return this.Object.Equals(other.Object);
     }

    

     
public override int GetHashCode()
     
{
         
int hashCode = Object.GetHashCode();

         
if (ObjectPath != null && ObjectPath.Length > 0)
         
{
             
foreach (object path in ObjectPath)
                 hashCode 
= HashCodeUtil.MixCodes(hashCode, path.GetHashCode());
         }


         
return hashCode;
     }

 }


 
public class HashCodeUtil
 
{
     
public static int MixCodes(int code1, int code2)
     
{
         
int h = (code1 << 4) + code2;
         
int g = h & unchecked((int)0xF0000000);
         
if (g != 0)
             h 
^= g >> 24;
         h 
&= ~g;
         
return h;
     }

 }

 

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

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

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

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

 

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

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

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

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

<!--

Code highlighting produced by Actipro CodeHighlighter (freeware)
http://www.CodeHighlighter.com/

--> 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方法需要注意的地方,而且通俗易懂。

原文地址:http://www.cnblogs.com/anders06/archive/2009/10/19/1586093.html 

来自 “ ITPUB博客 ” ,链接:https://blog.itpub.net/12639172/viewspace-617202/ ,如需转载,请注明出处,否则将追究法律责任。

请登录后发表评论 登录
全部评论

注册时间:2008-01-04

  • 博文量
    2376
  • 访问量
    5777922