引言
Object类是C#中所有类型的基类,但由于对它的继承是隐式的,故大多数人对它并不太在意,其实object中包含了很多有用的方法,对它有个清晰的了解能够很好地帮你理清楚c# API的层次结构,本文就Object中6个方法进行简单的说明,重点讲述Equals方法。
综述
在.Net中,每一个类型都继承自一个公共的基类:System.Object。Object类定义了.NET世界中每一个类型都支持的一组公共的成员集合。当创建任何一个不显示指定其基类的类时,它隐含继承自System.Object,当然,你也可以显示地继承。下面来看一下,System.Object的接口:
public class Object
{
public Object();
public virtual Boolean Equals(Object obj);
public virtual Int32 GetHashCode();
public Type GetType();
public virtual String ToString();
protected virtual void Finalize();
protected Object MemberwiseClone();
public static bool Equals(object objA, object objB);
public static bool ReferenceEquals(object objA, object objB);
}
可以看出,Object类主要包含四个虚方法,子类可以重写它,应该说这几个是比较常用的(除了Finalize());两个静态方法,object对其有具体实现,可以直接使用;两个实例方法,GetType在反射中用的比较多,MemberwiseClone在实现ICloneable接口时比较常用。至此,相信大家对object应该有了一个整体的认识,下面分别对它的几个方法成员进行描述。
应用
1 ToString() : 自描述
用一句话来概括ToString()方法的作用,我觉得应该是“提供了一种获得对象当前状态的快照”。object类对它的默认实现是返回该对象的完全限定名,也就是说,如果你定义的类没有重写ToString()方法,那么直接调用当前对象的ToString()方法,返回的是一个字符串:命名空间+类名。如下面的代码,输出的结果是: ConsoleApplication.Program
namespace ConsoleApplication
{
public class Program
{
static void Main(string[] args)
{
Program p = new Program();
Console.WriteLine(p.ToString());
}
}
}
上面已经说了,ToString()方法主要用来返回对象当前状态的一个快照,故如果我们在项目中有这种需求,就应该重写该方法。下面写个简单的代码实例,后面几个方法的说明都会围绕这个实例说明。如果做网络协议方面的开发,经常会遇到各种各样的Error Code,下面我们设计一个简单的类,用来描述一个网络操作的返回结果,代码如下:
public class Status
{
//Indicate the type of error.
private int errorCode;
//Description for error.
private string errorString;
#region Constructors
public Status()
{
errorCode = 0;
errorString = string.Empty;
}
public Status(int errorCode)
{
this.errorCode = errorCode;
}
public Status(int errorCode, string errorString)
: this(errorCode)
{
this.errorString = errorString;
}
#endregion
#region Properties
public int ErrorCode
{
get
{
return errorCode;
}
set
{
errorCode = value;
}
}
public string ErrorString
{
get
{
return errorString;
}
set
{
errorString = value;
}
}
#endregion
}
Status类包括两个数据成员,errorCode 和 errorString,分别用来描述错误码和错误提示信息,这里是为了简单化,实际的开发中errorCode一般是设计成enum类型的,易于标识。接着,我们在Status类中重写ToString()方法,代码如下:
public override string ToString()
{
StringBuilder sb = new StringBuilder();
sb.AppendFormat("ErrorCode = {0};", this.errorCode);
sb.AppendFormat("ErrorString = {0}", this.errorString);
return sb.ToString();
}
代码很简单,就是将当前对象的数据以一定的形式输出来,下面我们写个客户端代码简单测试下:
static void Main(string[] args)
{
Status status1 = new Status(1, "ErrorString1");
Status status2 = new Status(2, "ErrorString2");
Console.WriteLine(status1.ToString());
Console.WriteLine(status2.ToString());
}
返回结果如下:
ErrorCode = 1;ErrorString = ErrorString1
ErrorCode = 2;ErrorString = ErrorString2
小结:ToString()方法主要用来描述对象的当前状态,在某些特定应用中非常有用,比如在一些网络协议中,我们需要以Http Get的方式发送数据给服务器,那么构建url就是个必须的工作,如果只是简单几个参数可能比较容易构建,但是一旦需要传递的参数很多,那么比较OO的做法就是构建一个类,将那些需要传递的数据作为数据成员封装好,然后重写ToString()方法,将对象的数据按照url要求的格式组织好返回。
2 Equals(): 判等
Equals相关的方法总共有三个,Equals(Object, Object) : Boolean; ReferenceEquals(Object, Object) : Boolean; Equals(Object) : Boolean。前两个方法object已经提供了具体的实现,对于ReferenceEquals,顾名思义,是用来判断两个对象的引用是否相同,需要注意两点:
1 如果传入两个null对象,返回true。
2 由于ReferenceEquals的两个参数都是Object类型的,如果传入值类型将会进行装箱,由此会导致引用不相同,比如代码:Object.ReferenceEquals(2, 2),返回的是false,因为对2进行装箱以后倒置两者指向了不同的引用。如果这样写呢:int n = 2; Object.ReferenceEquals(n, n); 返回的依然是false。
接着,简单说明一下Equals(Object, Object) : Boolean,.NET中对它的实现如下:
public static bool Equals(object objA, object objB)
{
return ((objA == objB) || (((objA != null) && (objB != null)) && objA.Equals(objB)));
}
流程如下:
判断两个对象是否指向同一个引用(包括两者为null的情况),如果是则返回true,否则继续进行;
如果两者都不为null的时候,返回的结果取决于实例方法Equals的返回值。
object对Equals(Object)的默认实现是只有当两个对象指向相同的引用才返回true,下面我们来对该方法进行重载,代码依旧在Status的基础上改,加一个方法,代码如下:
public override bool Equals(object obj)
{
if (obj != null && obj is Status)
{
Status temp = obj as Status;
if (temp.errorCode == this.errorCode &&
temp.errorString == this.errorString)
{
return true;
}
}
return false;
}
先判断obj对象是否为null以及它的类型,如果它是一个非空的Status类型则比较对象的值,相等则返回true。修改Main方法,对Equals方法进行测试:
static void Main(string[] args)
{
Status status1 = new Status(1, "ErrorString1");
Status status2 = new Status(1, "ErrorString1");
Console.WriteLine(status1.Equals(status2));
}
输出结果为True,倘若没有重写Equals方法,输出的将是False,因为两者的状态数据相同,但是指向不同的引用。
重写了Equals方法后,重载 == 和 != 运算符其实也是件很容易的时,直接调Equals方法即可,但是引用《Effective c#》中的建议:
1 定义==重载函数的时候,也要定义!=重载函数。
2 值类型最好不要重载定义Equals函数,而引用类型最好不要重载定义==操作符。
至此,有关Equals就介绍到这,相信大家对它应该有了一个清晰的认识,接着我们来看Object下一个与Equals密切相关的成员方法。
3 GetHashCode(): 对象地址
GetHashCode()方法返回一个能够标识内存中指定对象的整数,如果你打算将自定义的类型包含进System.Collections.HashTable类型中,强烈建议你重写这个方法的默认实现。当我们重写了Equals(Object)实例方法后,编译器会产生一个警告,建议你同时也重写GetHashCode方法。GetHashCode的作用是返回一个数值,又叫散列码,它根据对象的内部状态数据表识对象。因此,如果两个对象的状态数据相同,也应该获得相同的散列码。一般来说,重写GetHashCode只在打算将自定义的类型保存在一个基于散列值的集合中时有用。在底层,HashTable类型调用所含类型的GetHashCode()以及Equals()成员来确定要返回给调用者的正确对象。
创建散列码的算法有很多,这里不做说明,System.String类提供了一个可靠的GetHashCode()的实现,它基于字符串的字符数据。如果能确定某个字符串字段在对象之间是唯一的,例如id,那么就可以直接对这个字段的字符串调用GetHashCode();如果找不到这样一个唯一型字段,但已经重写了ToString(),可以直接从它GetHashCode(),对于Status类,由于我们已经实现了ToString()方法,就可以这样简单实现GetHashCode方法,代码如下:
public override int GetHashCode()
{
return ToString().GetHashCode();
}
有关HashCode是否有效的判断标准,引用《Effective C#》中描述如下:
1. 如果两个对象相等(由operator==定义),它们必须产生相同的散列码。否则,这样的散列码不能用来查找容器中的对象[22]。
2. 对于任何一个对象A,A.GetHashCode()必须是一个实例不变式(invariant)。即不管在A上调用什么方法,A.GetHashCode()都必须总是返回相同的值。这可以确保放在“散列桶”中的对象总是位于正确的“散列桶”中。
3. 对于所有的输入,散列函数应该在所有整数中产生一个随机的分布。这样,我们才能从一个散列容器上获得效率的提升。
那么如果两个对象不相等能否产生相同的散列码呢? 网上搜了一下,看到有网友发的一段代码,似乎对于不同的状态数据,是可以返回相同的散列码,不信运行下面代码试一下:
Console.WriteLine("0.89265452879139".GetHashCode());
Console.WriteLine("0.280527401380486".GetHashCode());
返回的结果都是:2060653827
想要了解更多有关GetHashCode()方法的描述,可以参考《Effective C#: 改善C#程序的50种方法》