C# System.Object型別的主要方法

2020-07-16 10:04:46
所有型別都從 System.Object 派生,介面和指標是特例。下面介紹一些主要的 System.Object 提供的方法。

1) ReferenceEquals(object a, object b)

靜態方法。這個方法就是判斷兩個參照型別物件是否指向同一個地址。有此說明後,就確定了它的使用範圍,即只能對於參照型別操作。

對於任何兩個值型別資料比較,即使是與自身的比較,都會返回 false。

這是因為在呼叫此函數的時候,值型別資料要進行裝箱操作,轉換為 object 型別,而兩次裝箱,堆上會產生兩個物件:

Console.WriteLine(ReferenceEquals(1, 1));   // False

這個方法可以用來驗證字串駐留。當字串被駐留時,不會產生新的字串物件:

Console.WriteLine(ReferenceEquals("a", "a"));    // True

2) Equals(object a, object b)

靜態方法。如果兩者皆為 null,或者ReferenceEquals(a, b)返回真,則返回真。

否則,呼叫下面的方法(以a.Equals(b)的形式)。重寫 Equals 方法通常指重寫下面的方法(當然這個方法也無法重寫)。

3) Equals(object o)

虛方法,實體方法。它的呼叫結果和呼叫Equals(object a, object b)是一樣的(也不可能不一樣吧!),即如果兩個物件皆為 null,或者具有相同的參照就返回真,否則就返回假。

但是,任何型別都可以重寫該方法。重寫它之後,由於Equals(object a, object b)也呼叫它,所以跟著結果也會改變。

對於參照型別,儘管參照型別可能包含許多成員,使用 Equals 比較參照型別時,僅僅考慮兩個物件是否具有相同的參照,而不會逐個成員比較。

所以對於參照型別,除非你有其他的判等邏輯,否則不需要重寫該方法。

System.ValueType( 值型別 )重寫了該方法,使得方法僅僅比較值是否相等。

此時,如果值型別包含很多成員(例如結構體),會使用反射,取得型別所有的成員,然後逐個進行比較。

為了避開反射造成的效能損失,所以必須重寫該方法,只需要在其中遍歷所有結構的屬性,並一一進行比較即可。

如果你自定義的結構的相等邏輯不要求所有的屬性相等才意味著相等,而只是部分屬性相等就意味著相等時,你更應該重寫該方法。

例如,有這樣一個結構體:
struct Rectangle
{
    double width { get; set; }
    double height { get; set; }
}
需要重寫 Equals 方法。根據 MSDN,重寫的要求有:
  • x.Equals(null) 返回 false,因為 x 是值型別,值型別不會是 null。
  • x.Equals(x) 返回 true。
  • 交換性:x.Equals(y) 與 y.Equals(x) 返回相同的值。
  • 傳遞性:如果(x.Equals(y) && y.Equals(z))返回 true 則 x.Equals(2) 返回 true。
  • 只要不修改 x 和 y 所參照的物件,任何時候呼叫 x.Equals(y) 都返回相同的值。

通常推薦使用下面的方式來實現:
public override bool Equals(object obj)
{
    if(obj !=null&&obj is Rectangle)
    {
        //強行轉換為 Rectangle 型別
        var rect = (Rectangle)obj;
        //遍歷所有屬性
        return (rect.width == width) && (rect.height == height);
    }
    return false;
}
值得注意的是,雖然字串是參照型別,它也重寫了該方法,其行為和值型別一樣,也僅僅比較值是否相等。這是字串的行為看起來和值型別差不多的一個原因。

如果你重寫了 Equals 方法,那麼還應重寫 GetHashCode 方法。如果你沒有這麼做的話,編譯器會報告一條警告訊息:重寫了 Equals 但不重寫 GetHashCode。

這是因為,這兩個方法是有連帶關係的,如果兩個物件相等,那麼它們的雜湊碼必須也相等(不過反過來, 兩個物件不相等,它們的雜湊碼也可能相等)。

4) == 和上面判等方法的關係

== 是運算子過載,它並沒有什麼新意。普通的情況下,如果兩邊都是參照型別,則等同於 ReferenceEquals。

但 string 是特例,當兩邊都是字串或值型別時,== 比較值是否相同。但是,== 不能用於結構體,除非你過載它。

例如,還是使用上面的長方形結構體,這次我們規定只要面積(長乘以寬)相等,就算相等:
struct Rectangle
{
    double width { get; set; }
    double height { get; set; }
    public Rectangle(double w,double h)
    {
        width = w;
        height = h;
    }
    public override bool Equals(object obj)
    {
        if (obj != null && obj is Rectangle)
        {
            //強行轉換為 Rectangle 型別
            var rect = (Rectangle)obj;
            //面積相等嗎?
            return width*height == rect.width*rect.height;
        }
        return false;
    }
}
此時,下面的程式碼輸岀的結果一如預期:
var a = new Rectangle(1, 2);
var b = new Rectangle(2, 1);

Console.WriteLine(ReferenceEquals(a, b));  //False
Console.WriteLine(a.Equals(b));                  //true
如果我們沒有重寫 Equals 方法的話,則應該輸出兩個 false。但是,我們不能直接使用 == 符號判等:

Console.WriteLine(a==b);

我們必須過載它:
public static bool operator ==(Rectangle c1, Rectangle c2)
{
    return c1.Equals(c2);
}
public static bool operator !=(Rectangle c1, Rectangle c2)
{
    return !c1.Equals(c2);
}
此時,程式碼就會通過編譯並輸出正確的值。在過載 == 時,必須跟著一起過載 !=。

上面已經給出了過載 == 和 != 的標準方式。當然,如果故意的話,也可以用其他方式過載。

不過這樣一來,==和 Equals 將有可能會輸出不同的值,這是不符合邏輯的。

5) GetHashCode

在 CLR 中,任何物件的任何範例都對應一個雜湊碼。為此,System.Object 的虛方法 GetHashCode 能獲取任意物件的雜湊碼。

當然,相同的物件的雜湊碼必然相同(不過,如果不同物件的雜湊碼相同,則沒有關係,因為可能產生雜湊碰撞)。所以,重寫 Equals 方法必須重寫 GetHashCode 方法,保證兩者具有相同的語意。

重寫 GetHashCode 方法除了要保證相同物件的雜湊碼必然相同之外,還要保證該物件的雜湊碼是不可變的,所以,該物件的雜湊碼如果基於它的一些成員,這些成員也應當是不可變的。通常藉助型別中的唯一識別成員,例如 Id 等。

6) 重寫 Equals:完整版本

根據上面的討論,我們可以得出重寫 Equals 的步驟:
  • 重寫 Equals 覆蓋 Object.Equals。
  • 重寫 GetHashCode 方法。
  • 過載等號和不等號。
  • 實現 IEquatable<T> 介面,它會使得值型別比較大小時不會被隱式地裝箱。

因此,對 Equals 重寫的完整案例如下:
struct Rectangle:IEquatable<Rectangle>
{
    double width { get; set; }
    double height { get; set; }
    public override bool Equals(object obj)
    {
        if (obj != null && obj is Rectangle)
        {
            //強行轉換為 Rectangle 型別
            var rect = (Rectangle)obj;
            //面積相等嗎?
            return (rect.width==width)&&(rect.height==height);
        }
        return false;
    }
    public override int GetHashCode()
    {
        //保證語意一致性
        return width.GetHashCode() * height.GetHashCode();
    }
    //實現介面的方法,該方法會在傳入引數人Rectangle是優先於Object.Equals方法
    //從而避免裝箱
    public bool Equals(Rectangle other)
    {
        //遍歷所有屬性
        return (other.width == width) && (other.height == height);
    }
    //過載等於號和不等號
    public static bool operator == (Rectangle c1,Rectangle c2)
    {
        return c1.Equals(c2);
    }
    public static bool operator !=(Rectangle c1, Rectangle c2)
    {
        return !c1.Equals(c2);
    }
}

7) ToString

虛方法。返回型別的完整名稱(this.GetType().FullName)。重寫它的可能性很大,例如你希望 ToString 遍歷物件的所有屬性,列印出它所有屬性的值。

8) GetType

返回物件指向的型別物件,返回值的型別為 System.Type。得到型別物件之後,就可以通過反射方法獲得型別物件的成員,也就是物件本身的成員,例如欄位、屬性、方法等。

Typeof 關鍵字是這個方法的一個語法糖。

當沒有物件範例時,也可以使用 Type.GetType 方法。下面若干方法獲得的型別物件是相同的:
static void Main(string[] args)
{
    string a = "test";
    var typea = a.GetType();
    var typeb = Type.GetType("System.String");
    var typec = typeof(string);

    Console.WriteLine(ReferenceEquals(typea, typeb));       //true
    Console.WriteLine(ReferenceEquals(typea, typec));       //true
    Console.ReadKey();
}

9) Finalize

在 GC 決定回收這個物件之後,會呼叫這個方法。如果要做一些額外的事,例如回收物件的非託管屬性或物件,則應當重寫這個方法,但只有在存在非託管物件時才需要這麼做。