对象判等
一直都认为对于对象判等自己明白了,可是真当与别人深入交流时,茫然了~~~痛定思痛,花了足足三个小时,整理了一番。要想深入理解这块知识,必须对CLR内存管理机制有一定的了解。废话不多说。
一、首先牢记两个基本概念:
(1)值相等:表示两个对象的数据成员按内存位分别相等,即两个对象类型相等,并且具有相等和相同的字段。
(2)引用相等:表示两个引用指向同一对象实例,也就是同一内存地址,因此可以由引用相等推出其值相等,反之则不然。
二、本质分析:
.NET对于对象判等总共有四个方法,虚拟的Equals()方法,静态的Equals()方法,静态的ReferenceEquals()方法,==操作符。现一一分析。
(1)Equals()虚方法:用于比较两个类型实例是否相等,也就是判断两个对象是否具有相同的“值”代码实现可以“表示”为
public virtual bool Equals(object obj)
{
return InernalEquals(this,obj);
}
//其中InternalEquals可以表示为
if(this==obj)
{
return true;
}
else
{
return false;
}
可见默认情况下,Equals方法和referenceEquals方法是一样的,object的equals虚方法仅仅提供了最最简单的比较策略:如果两个引用指向同一对象,true,否则false。也就是判断是否引用相等。然而这种方法并未达到Equals比较两个对象值相等的目标,所以System.Object将这个任务交个其派生对象去重新实现,可以说Equals的比较结果取决于类的创建者是如何实现的,而非统一性约定。事实上,框架类中很多引用类型的Equals方法用于比较值相等,例如最典型的String类型对象是否相等,肯定关注起内容是否相等,判断的是值相等语义。
(2)Equals()静态方法:实现了对两个对象的相等性判别,其在System.Object类型中实现过程可以"表示"为(而不是说其在Object类中就是这样实现的)
public static bool Equals(object objA, object objB)
{
if (objA == objB)
{
return true;
}
if ((objA != null) && (objB != null))
{
return objA.Equals(objB);
}
return false;
}
所有Equals()静态方法的执行结果依次取决于三个条件
①是否为同一实例
②是否都为null
③第一个参数的Equals()实现
故通常情况下Equals静态方法的执行结果常常受到“判等对象”的影响,如下测试。
namespace 类型判等
{
class Program
{
static void Main(string[] args)
{
MyClassA classA = new MyClassA();
MyClassB classB = new MyClassB();
Console.WriteLine(Equals(classA,classB));//true,实际上执行的是classA.Equals(classB);返回true
Console.WriteLine(Equals(classB, classA));//false,实际上执行的是classB.Equals(classA);返回false
Console.ReadLine();
}
}
class MyClassA
{
public override bool Equals(object obj)
{
return true;
}
}
class MyClassB
{
public override bool Equals(object obj)
{
return false;
}
}
}
执行结果为:
由执行结果知道,静态Equals方法的执行取决于==操作符,和Equals虚方法这两个因素,因此决议静态Equals方法的执行,就要在
自定义类型中重写虚拟的Equals方法和重载==操作符。还有静态Equals方法可以解决两个值为null的对象的判等问题,而是用objA.Equals(objB)来判断两个null对象会抛出NullReferenceException异常。
(3)静态ReferenceEquals()方法,因为ReferenceEquals为静态方法,所以不能重写该方法,只能使用System.Object中的实现代码,具体为
public static bool ReferenceEquals(object objA, object objB)
{
return (objA == objB);
}
如下示例:
namespace 类型判等
{
class Program
{
static void Main(string[] args)
{
MyClass classA = new MyClass();
MyClass classB = new MyClass();
//classA,classC指向同一对象实例
MyClass classC = classA;
Console.WriteLine(ReferenceEquals(classA,classB));//false
Console.WriteLine(ReferenceEquals(classA, classC));//true
Console.WriteLine(ReferenceEquals(null, null));//true
Console.WriteLine(ReferenceEquals(classA, null));//false
Console.ReadLine();
}
}
class MyClass
{
}
}
结果:
可见ReferenceEquals方法用于判断两个引用是否指向同一对象,也就强调的引用相等,因此ReferenceEquals比较同一类型的两个对象实例将范虎false而.NET认为null等于null。
(4)“==”操作符
值类型下:表示是否值相等,由值类型的根类System.ValueType提供了实现,
引用类型下:表示是否“引用相等”即两个引用指向同一个对象实例。牢记此话。
当然也有例外 还是string ==表示的是值相等,而非引用相等。
三、实际应用
值类型判等
①Equals,System.Valuetype重载了System.Object的Equals方法,用于实现对实例数据的判等。
②ReferenceEquals 永远返回false说明:用ReferenceEquals()方法比较两个值类型变量毫无意义,结果肯定是false,即使是2个值相等的变量,因为在ReferenceEquals()方法比较前,被比较的值类型变量将会被装箱操作,隐性地创建两个不同对象,所以其地址引用也将不同。
③== 为重载的==的值类型,将比较两个值是否“按位”相等(不懂,只知道是比较两个值是否相等,还请高人指点!)
引用类型判等
①静态的ReferenceEquals:两个实例对象是否指向同一引用地址。
②静态的Equals:
是否为同一实例
是否都为null
第一个参数的Equals()实现
③虚拟的Equals默认为引用地址比较。
④== 默认为引用地址比较
四、小结——重写Equals()方法 1、经过对四种不同类型判等方法的讨论,我发现不管是Equals静态方法,Equals虚拟方法,==操作符的执行结果,都可能受到重写Equals方法的影响,所以在对象判等时就必须注意自定义类型中如何实现Equals方法,以及实现怎么样的Equals方法,不同的类型,“相等”会有偏差。
2、因此Equals方法的执行结果往往取决于自定义类型的具体实现规则,而为什么.NET提供这种机制
(1)对象判等取决于需求,没必要为所有.NET类型完成逻辑判等,System.Object也无法满足各种需求的判等方法。
(2)不同类型的判等的处理不同,通过多态机制在派生类中处理各自的判等实现是明智的。
3、重写Equals要综合考虑值类型,引用类型的判等,同时要兼顾父类所带来的影响,另外遵循三个原则,自发,传递,对称,还要注意重写GetHashCode()方法。
另外关于对象判等还得注意String类型的字符串驻留机制导致的String判等的特殊性。
题外话:本贴主要参考园子里王涛老师写的那本《你必须知道的.NET》一书,推荐大家都去看看此书,绝对值得一看。另有不当之处,还望各位指正。