随笔 - 42  文章 - 1 评论 - 10 trackbacks - 1

System.Object类型中Equals的方法
  

public virtual bool Equals(object obj);
    
//以下两个方法不能被重载,静态的缘故
    public static bool Equals(object objA, object objB);
    
public static bool ReferenceEquals(object objA, object objB); 


基类中System.Object类型中Equals实现
 

 class Object
  
{
     
public virtual Boolean Equals(Object obj)
     
{
    
//如果两个引用指向的是同一个对象,他们肯定相等
        if(this == obj)
      
return true;
    
//假定两个对象不等
    return false;
     }

  }


    该方法采取的策略可能是最简单的:如果进行比较的两个引用指向的是同一个对象,则方法返回true;否则返回false;

    下面的这个示例来自王涛的《你必须知道的.NET》

using System; 

class ClassA
{
  
public override bool Equals(Object obj)
  
{
      
return true;
  }

}
 

class ClassB
{
    
public override bool Equals(Object obj)
    
{
        
return false;
    }

}
 

class Test_Equlas
{
    
public static void Main()
    
{
        ClassA objA 
= new ClassA();
        ClassB objB 
= new ClassB(); 

        Console.WriteLine(Equals(objA,objB));
        Console.WriteLine(Equals(objB,objA));
        Console.ReadLine();
    }

}
 

//执行结果
True
False 

    由执行结果可知,静态Equals的执行取决于==操作符和Equals虚方法这两个因素。因此,决议静态Equlas方法的执行,就要在自定义类型中覆写Equals方法和重载==操作符。(why?)

    注:重写Equals方法而不重写GetHashCode方法会导致编译警告,但是重写GetHashCode方法却不重写Equals方法时却不会导致警告。故推荐重写Equals方法的时候重写GetHashCode方法。两个相等的对象应该具有相同的哈希代码。
 

ReferenceEquals静态方法

 ReferenceEquals方法为静态方法,因此不能在继承类中重写此方法,所以只能使用System.Object的实现代码,具体为:


 
public static bool ReferenceEquals(Object objA,Object objB)
  
{
     
return (objA == objB);
  }
 

    可见,ReferenceEquals方法用于判断两个引用是否指向同一个对象,也就是引用相等。因此ReferenceEquals方法比较同一个类型的两个实例将返回false,而.NET认为null等于null,因此下面的实例就很容易能得出结果:
 

public static void Main()
    

       MyClass mc1 
= new MyClass();
       MyClass mc2 
= new MyClass();
       MyClass mc3 
= mc1; 

       
//False
       Console.WriteLine(ReferenceEquals(mc1,mc2));
       
//True
       Console.WriteLine(ReferenceEquals(mc1,mc3));
       
//True
       Console.WriteLine(ReferenceEquals(null,null));
       
//False
       Console.WriteLine(ReferenceEquals(mc1,null));
    }



  因此,ReferenceEquals方法,只能用于比较两个引用类型,而以ReferenceEquals方法比较值类型,必然伴随着装箱操作,分配在不同地址的两个装箱的实例对象,肯定返回false结果:

 public static void Main()
  
{
     Console.WriteLine(ReferenceEqulas(
1,1));
  }

  
//执行结果:False 

   
    另外,应该关注.NET某些特殊类型的“意外”规则,例如下面的实现将突破常规,除了深刻地了解ReferenceEquals的实现规则,也应当理解某些特殊情况背后的秘密:

  public static void Main()
  
{
     String strA 
= "ABCDE";
     String strB 
= "ABCDE"

     Console.WriteLine(ReferenceEquals(strA,strB));
  }
 

  
//执行结果:True


    从结果可知,两次创建的string不仅内容相同,而且分享共同的内存空间,事实上确实如此,而这是由于System.String的字符串驻留机制造成的。

    比较可知,默认情况下,Equlas方法和ReferenceEquals方法是一样的,都是比较两个对象是否指向同一引用,是则返回true,反之返回false。然而这种方法并未达到Equals比较两个对象值相等的目标,因此System.Object将这个任务交给其派生类去重新实现,可以说Equals的比较结果取决于类的创建者如何实现,而非统一约定。


为基类没有重写Equals方法的引用类型实现Equals


    对于那些基类型直接继承了Object的Equals实现的类型,下面的代码展示了怎样为它们实现Equals方法:

//这是一个引用类型(class的缘故)
class MyRefType:BaseType
{
   RefType refobj;    
//该字段是一个引用类型字段
   ValType valobj;    //该字段是一个值类型 

   
public override Boolean Equlas(Object obj)
   

      
//因为'this'不为空,所以如果obj为null,那么两个对象不可能相等
      if(obb == null)
        
return false;
      
//如果两个对象的类型不同,那么也不可能相等
      if(this.GetType() != obj.GetType())
        
return false;
      
//将obj转型为定义的类型以访问其中的字段。注意这里的转型不会失败,因为已经知道两个对象是同一类型
      MyRefType other = (MyRefType) obj; 

      
//比较其中的引用类型字段
      if(!Object.Equals(refobj,other.refobj))
        
return false;
      
//比较其中的值类型字段
      if(!valobj.Equals(other.valobj))
        
return false

      
return true;    //直到这里,这两个对象才算相等
   }

}
 

     这里实现的Equals首先将obj和null比较。如果被比较的对象不为null,那么接着比较两个对象的类型。如果两个对象的类型不同,那么他们不可能相等。如果两个对象有着相同的类型,就将obj转换成MyRefType,这里的转型不可能抛出异常,因为我们已经知道两个对象是同一类型。等到上述所有步骤都正确执行完毕后,我们才开始比较两个对象中的字段。如果两个对象中的字段都相等,那么返回true。

    在比较两个对象中的字段时,我们必须非常仔细。前面的代码展示了根据字段类型的不同,所进行的两种不同的比较方式。

    比较引用类型的字段:要比较引用类型的字段,我们应该调用Object的静态Equals方法。Object的静态Equals方法是一个比较两个引用类型对象的辅助方法。下面展示了Object的静态Equals方法的内部实现:

public static Boolean Equals(Object objA,Object objB)
    
{
       
//如果objA和objB指向的是同一个对象,则返回true
       if(objA == objB)
         
return true;
       
//如果objA或objB为null,那么它们不可能相等
       if((objA == null||(objB == null))
         
return false;
       
//判断objA和objB是否相等,返回比较结果
       return objA.Equals(objB);
    }


    我们采用这种方法来比较引用类型字段是因为即使两个字段出现了值为null的情况,我们的代码仍会正常运行。例如,如果refobj为null,调用refobj.Equals(other.refobj)将会抛出NullReferenceException异常。Object的静态Equals这一辅助方法会为我们对出现null的情况做正确的检查。

  比较值类型字段:要比较两个值类型字段,我们应该调用该字段类型的Equals方法来比较它们。我们不应该用Object的静态Equals方法,因为值类型对象的值永远不可能为null,并且调用Object的静态Equals方法会对值类型对象执行装箱操作(ReferenceEqulas方法类似)。


为基类重写了Object.Equals方法的引用类型实现Equals


    对于那些基类型提供了非Object.Equals方法实现的引用类型,下面的代码展示了怎么为它们实现Equals方法:

//这是一个引用类型
   class MyRefType:Base
   
{
      RefType refobj;
      ValType valobj; 

      
public override Boolean Equals(Object obj)
      
{
     
//first call the Equals function in the base type to compare the two object 

     
if(!base.Equals(obj))
       
return false

     
if(obj == null)
       
return false

     
if(this.GetType() != obj.GetType())
       
return false

     MyRefType other 
= (MyRefType)obj; 

     
//比较引用类型字段是否相等
     if(!Object.Equals(refobj,other.refobj))
       
return false

    
//比较值类型字段是否相等
     if(!valobj.Equals(other.valobj))
       
return false

     
return true;
      }

   }
 

   这段代码和前面一节中展示的代码大体上是一样的。唯一的差别是这里还要求比较基类型中定义的字段。如果基类型认为它们不等,那么它们就不可能相等。
  如果调用base.Equals会导致调用Object.Equals方法,那么就不应该调用它,这一点很重要。因为只有在两个引用指向同一个对象时,Object.Equals方法才会返回true.如果两个引用不是指向同一个对象,那么它将返回false。这样我们实现的方法总会返回false。


 为值类型实现Equals方法

  所有的值类型都继承自System.ValueType。ValueType重写了System.Object提供的Equals方法实现。System.ValueType.Equals方法在内部首先使用反射机制来得到类型所有的实例字段,然后再比较它们是否相等。这种比较的效率很低,但却是一个所有的值类型都能继承的、相当不错的默认实现。至少,这样的做法意味着引用类型继承的Equals判断的是引用是否相等,而值类型继承的Equals判断的是值是否相等。

 下面代码展示了System.ValueType.Equlas方法的内部实现(Reflector .NET FrameWork 2.0 C#)

 

public override bool Equals(object obj)
{
    
if (obj == null)
    
{
        
return false;
    }

    RuntimeType type 
= (RuntimeType) base.GetType();
    RuntimeType type2 
= (RuntimeType) obj.GetType();
    
if (type2 != type)
    
{
        
return false;
    }

    
object a = this;
    
if (CanCompareBits(this))
    
{
        
return FastEqualsCheck(a, obj);
    }

    FieldInfo[] fields 
= type.GetFields(BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Instance);
    
for (int i = 0; i < fields.Length; i++)
    
{
        
object obj3 = ((RtFieldInfo) fields[i]).InternalGetValue(a, false);
        
object obj4 = ((RtFieldInfo) fields[i]).InternalGetValue(obj, false);
        
if (obj3 == null)
        
{
            
if (obj4 != null)
            
{
                
return false;
            }

        }

        
else if (!obj3.Equals(obj4))
        
{
            
return false;
        }

    }

    
return true;
}
 

    值类型的每一个简单类型(如Int32)都重载了这个方法
  Int32:
   public override bool Equals(object obj);

   public bool Equals(int obj);
    下面的代码展示了怎样为一个值类型实现Equals方法:

struct MyValType
{
   RefType refobj;
   ValType valobj; 

   
public override bool Equals(Object obj)
   
{
      
if(!(obj is MyValType))
        
return false 

      
return this.Equals((MyValType)obj);
   }
 

   
public bool Equals(MyValType obj)
   
{
      
if(!Object.Equals(this.refobj,obj.refobj))
        
return false

      
if(!this.valobj.Equals(obj.valobj))
        
return false

      
return true;
   }

}
 


     C#编译器看到我们使用==操作符来比较两个类型为Object的引用时,编译器会产生比较两个对象引用是否相同的IL代码。
  因此在编写代码时,我们也可以使用==操作符来代替调用Object.ReferenceEquals方法,这取决于个人喜好。然而在使用==操作符时,我们必须非常小心。只有==操作符两边的变量为Object类型时,它才会去比较两个对象的引用是否相同。如果有一个对象不为Object,那么将会调用我们重载的==操作符(如果没有重载,将会导致编译器报错)。所以,为了代码的清晰和保险起见,我们一般不要使用==操作符来判断两个对象引用是否相同,而应该使用Object的ReferenceEquals方法。

当我们实现自己的Equals方法时,必须确保它遵循以下4条规则:
1、Equals方法必须是自反的,也就是说,x.Equals(x)必须返回true;
2、Equals方法必须是对称的,也就是说,x.Equals(y)和y.Equals(x)必须返回同样的值。
3、Equals方法必须是可传递的,也就是说,如果x.Equals(y)和y.Equals(z)都返回true,那么x.Equals(z)也必须返回true.
4、Equals方法必须是前后一致,也就是说,如果两个对象的值没有发生改变,多次调用Equals方法将返回同样的值。
如果我们为Equals方法提供的实现滑遵循上述4条原则,我们的应用程序将发生一些奇怪的、不可预期的行为。

有不正确的地方,希望大家指正

posted on 2008-04-26 11:13 谢良威 阅读(220) 评论(0)  编辑 收藏 所属分类: C# Programing