簡體   English   中英

什么是比較參考類型的兩個實例的“最佳實踐”?

[英]What is “Best Practice” For Comparing Two Instances of a Reference Type?

我最近遇到過這種情況,到目前為止,我一直在愉快地重寫等於運算符( == )和/或Equals方法,以查看兩個引用類型是否實際包含相同的數據 (即兩個看起來相同的不同實例)。

我一直在使用它,因為我已經進行了更多的自動化測試(比較參考/預期數據與返回的數據)。

在查看MSDN中的一些編碼標准指南時,我遇到了一篇建議反對它的文章 現在我理解為什么文章說這個(因為它們不是同一個實例 )但它沒有回答這個問題:

  1. 比較兩種參考類型的最佳方法是什么?
  2. 我們應該實施IComparable嗎? (我還看到提到這應該僅為值類型保留)。
  3. 有一些我不知道的界面嗎?
  4. 我們應該自己動手嗎?!

非常感謝^ _ ^

更新

看起來我錯誤地閱讀了一些文檔(這是漫長的一天)並且壓倒Equals可能是要走的路。

如果要實現引用類型,則應考慮在引用類型上覆蓋Equals方法(如果類型看起來像基本類型,如Point,String,BigNumber等)。 大多數引用類型不應重載等於運算符,即使它們重寫等於 但是,如果要實現旨在具有值語義的引用類型(例如復數類型),則應覆蓋相等運算符。

正確,高效地實現.NET中的相等性並且沒有代碼重復是很困難的。 具體來說,對於具有值語義的引用類型( 即將equatilence視為相等的不可變類型 ),您應該實現System.IEquatable<T>接口 ,並且應該實現所有不同的操作( EqualsGetHashCode== , != ) 。

舉個例子,這是一個實現值相等的類:

class Point : IEquatable<Point> {
    public int X { get; }
    public int Y { get; }

    public Point(int x = 0, int y = 0) { X = x; Y = y; }

    public bool Equals(Point other) {
        if (other is null) return false;
        return X.Equals(other.X) && Y.Equals(other.Y);
    }

    public override bool Equals(object obj) => Equals(obj as Point);

    public static bool operator ==(Point lhs, Point rhs) => object.Equals(lhs, rhs);

    public static bool operator !=(Point lhs, Point rhs) => ! (lhs == rhs);

    public override int GetHashCode() => X.GetHashCode() ^ Y.GetHashCode();
}

上面代碼中唯一可移動的部分是粗體部分: Equals(Point other)的第二行和GetHashCode()方法。 其他代碼應保持不變。

對於不表示不可變值的引用類,請不要實現運算符==!= 相反,使用它們的默認含義,即比較對象標識。

代碼故意等同於派生類類型的偶數對象。 通常,這可能不合適,因為基類和派生類之間的相等性沒有明確定義。 不幸的是,.NET和編碼指南在這里並不十分清楚。 Resharper 在另一個答案中發布的代碼在這種情況下容易受到不良行為的影響,因為Equals(object x)Equals(SecurableResourcePermission x) 將以不同的方式處理這種情況。

要更改此行為,必須在上面的強類型Equals方法中插入其他類型檢查:

public bool Equals(Point other) {
    if (other is null) return false;
    if (other.GetType() != GetType()) return false;
    return X.Equals(other.X) && Y.Equals(other.Y);
}

看起來你正在使用C#進行編碼,它有一個你的類應該實現的Equals方法,如果你想使用一些其他指標來比較兩個對象,而不是“這兩個指針(因為對象句柄就是指針)相同的內存地址?“

我從這里抓取了一些示例代碼:

class TwoDPoint : System.Object
{
    public readonly int x, y;

    public TwoDPoint(int x, int y)  //constructor
    {
        this.x = x;
        this.y = y;
    }

    public override bool Equals(System.Object obj)
    {
        // If parameter is null return false.
        if (obj == null)
        {
            return false;
        }

        // If parameter cannot be cast to Point return false.
        TwoDPoint p = obj as TwoDPoint;
        if ((System.Object)p == null)
        {
            return false;
        }

        // Return true if the fields match:
        return (x == p.x) && (y == p.y);
    }

    public bool Equals(TwoDPoint p)
    {
        // If parameter is null return false:
        if ((object)p == null)
        {
            return false;
        }

        // Return true if the fields match:
        return (x == p.x) && (y == p.y);
    }

    public override int GetHashCode()
    {
        return x ^ y;
    }
}

Java具有非常相似的機制。 equals()方法是Object類的一部分,如果您想要這種類型的功能,您的類會重載它。

重載'=='的原因對於對象來說可能是一個壞主意,通常,您仍然希望能夠執行“這些是相同的指針”比較。 這些通常依賴於,例如,將元素插入到不允許重復的列表中,並且如果此運算符以非標准方式過載,則某些框架內容可能不起作用。

下面我總結了實現IEquatable時需要做的事情,並提供了各種MSDN文檔頁面的理由。


摘要

  • 當需要測試值相等時(例如在集合中使用對象時),您應該為您的類實現IEquatable接口,覆蓋Object.Equals和GetHashCode。
  • 當需要測試參考相等性時,您應該使用operator ==,operator!=和Object.ReferenceEquals
  • 您應該只覆蓋operator ==和operator!=的值類型和不可變的引用類型。

理由

IEquatable

System.IEquatable接口用於比較對象的兩個實例是否相等。 根據類中實現的邏輯比較對象。 比較結果是一個布爾值,表示對象是否不同。 這與System.IComparable接口形成對比,后者返回一個整數,指示對象值的不同之處。

IEquatable接口聲明了必須重寫的兩個方法。 Equals方法包含執行實際比較的實現,如果對象值相等則返回true,否則返回false。 GetHashCode方法應返回唯一的哈希值,該哈希值可用於唯一標識包含不同值的相同對象。 使用的散列算法類型是特定於實現的。

IEquatable.Equals方法

  • 您應該為對象實現IEquatable,以處理它們存儲在數組或泛型集合中的可能性。
  • 如果實現IEquatable,還應該覆蓋Object.Equals(Object)和GetHashCode的基類實現,以便它們的行為與IEquatable.Equals方法的行為一致。

覆蓋等於()和運算符的指南==(C#編程指南)

  • x.Equals(x)返回true。
  • x.Equals(y)返回與y.Equals(x)相同的值
  • if(x.Equals(y)&& y.Equals(z))返回true,則x.Equals(z)返回true。
  • 連續調用x。 只要未修改x和y引用的對象,Equals(y)就會返回相同的值。
  • X。 Equals(null)返回false(僅適用於非可空值類型。有關更多信息,請參閱Nullable Types(C#編程指南) 。)
  • Equals的新實現不應該拋出異常。
  • 建議任何覆蓋Equals的類也會覆蓋Object.GetHashCode。
  • 建議除了實現Equals(object)之外,任何類還為自己的類型實現Equals(type),以增強性能。

默認情況下,operator ==通過確定兩個引用是否指示同一對象來測試引用相等性。 因此,引用類型不必實現operator ==以獲得此功能。 當一個類型是不可變的,也就是說,實例中包含的數據不能改變時,重載operator ==來比較值的相等而不是引用相等可能是有用的,因為作為不可變對象,它們可以被認為是相同的因為它們具有相同的價值。 在非不可變類型中覆蓋operator ==不是一個好主意。

  • 重載的operator ==實現不應該拋出異常。
  • 任何重載operator ==的類型也應該重載operator!=。

==運算符(C#參考)

  • 對於預定義的值類型,如果操作數的值相等,則相等運算符(==)返回true,否則返回false。
  • 對於除string之外的引用類型,如果其兩個操作數引用同一對象,則==返回true。
  • 對於字符串類型,==比較字符串的值。
  • 在運算符==覆蓋中使用==比較測試null時,請確保使用基礎對象類運算符。 如果不這樣做,將發生無限遞歸,從而導致堆棧溢出。

Object.Equals方法(對象)

如果您的編程語言支持運算符重載,並且您選擇重載給定類型的相等運算符,則該類型必須覆蓋Equals方法。 Equals方法的此類實現必須返回與相等運算符相同的結果

以下准則用於實現值類型

  • 考慮重寫Equals以獲得比ValueType上的Equals的默認實現所提供的性能更高的性能。
  • 如果重寫等於並且語言支持運算符重載,則必須為值類型重載等於運算符。

以下准則用於實現引用類型

  • 如果類型的語義基​​於類型表示某些值的事實,請考慮在引用類型上覆蓋Equals。
  • 大多數引用類型都不能重載等於運算符,即使它們重寫等於。 但是,如果要實現旨在具有值語義的引用類型(例如復數類型),則必須覆蓋相等運算符。

額外的陷阱

  • 重寫GetHashCode()時,請確保在哈希代碼中使用它們之前測試NULL的引用類型。
  • 我遇到了這里描述的基於接口的編程和運算符重載的問題: C#中基於接口的編程的運算符重載

該文章建議不要覆蓋等於運算符(對於引用類型),而不是覆蓋重寫等號。 如果相等檢查不僅僅意味着檢查,那么您應該在對象(引用或值)中覆蓋Equals。 如果需要接口,還可以實現IEquatable (由泛型集合使用)。 但是,如果確實實現了IEquatable,則還應該重寫equals,因為IEquatable備注部分指出:

如果實現IEquatable <T>,則還應覆蓋Object.Equals(Object)和GetHashCode的基類實現,以使它們的行為與IEquatable <T> .Equals方法的行為一致。 如果您重寫了Object.Equals(Object),則在調用類上的靜態Equals(System.Object,System.Object)方法時也會調用重寫的實現。 這確保了Equals方法的所有調用都返回一致的結果。

關於是否應該實現Equals和/或相等運算符:

實施等於方法

大多數引用類型不應重載等於運算符,即使它們重寫等於。

實施平等指南和平等操作員(==)

每當實現相等運算符(==)時重寫Equals方法,並使它們執行相同的操作。

這只表示每當實現相等運算符時都需要重寫Equals。 沒有說您在重寫Equals時需要覆蓋相等運算符。

對於將產生特定比較的復雜對象,然后實現IComparable並在Compare方法中定義比較是一個很好的實現。

例如,我們有“Vehicle”對象,其中唯一的區別可能是注冊號,我們使用它來進行比較,以確保測試中返回的預期值是我們想要的值。

我傾向於使用Resharper自動制作的東西。 例如,它為我的一個引用類型自動處理:

public override bool Equals(object obj)
{
    if (ReferenceEquals(null, obj)) return false;
    if (ReferenceEquals(this, obj)) return true;
    return obj.GetType() == typeof(SecurableResourcePermission) && Equals((SecurableResourcePermission)obj);
}

public bool Equals(SecurableResourcePermission obj)
{
    if (ReferenceEquals(null, obj)) return false;
    if (ReferenceEquals(this, obj)) return true;
    return obj.ResourceUid == ResourceUid && Equals(obj.ActionCode, ActionCode) && Equals(obj.AllowDeny, AllowDeny);
}

public override int GetHashCode()
{
    unchecked
    {
        int result = (int)ResourceUid;
        result = (result * 397) ^ (ActionCode != null ? ActionCode.GetHashCode() : 0);
        result = (result * 397) ^ AllowDeny.GetHashCode();
        return result;
    }
}

如果要覆蓋==並仍然進行ref檢查,仍然可以使用Object.ReferenceEquals

微軟似乎已經改變了他們的調整,或者至少存在關於不重載相等運算符的沖突信息。 根據這篇題為“如何:為類型定義價值平等”的微軟文章

“==和!=運算符可以與類一起使用,即使類沒有重載它們。但是,默認行為是執行引用相等性檢查。在類中,如果重載Equals方法,則應該重載==和!=運算符,但不是必需的。“

根據Eric Lippert對一個問題的回答 ,我問過關於C#中的平等最小代碼 - 他說:

“你遇到的危險就是你得到一個為你定義的==運算符,默認情況下引用相等。你可以很容易地在一個重載的Equals方法確實值相等並且==確實引用相等的情況下結束,然后你不小心在非參考相等的價值相等的東西上使用了引用相等。這是一種容易出錯的做法,人類代碼審查很難發現。

幾年前,我研究了一種靜態分析算法來統計檢測這種情況,我們在所研究的所有代碼庫中發現了每百萬行代碼大約兩個實例的缺陷率。 當只考慮具有某些被覆蓋的Equals的代碼庫時,缺陷率明顯更高!

此外,考慮成本與風險。 如果您已經擁有IComparable的實現,那么編寫所有運算符都是微不足道的單行程序,它們不會有錯誤並且永遠不會被更改。 這是你要編寫的最便宜的代碼。 如果在編寫和測試十幾種微小方法的固定成本與發現和修復難以看到的錯誤的無限成本之間做出選擇,其中使用引用相等而不是值相等,我知道我會選擇哪一個。“

.NET Framework永遠不會使用您編寫的任何類型的==或!=。 但是,如果其他人這樣做會發生危險。 因此,如果該類用於第三方,那么我將始終提供==和!=運算符。 如果該類僅用於組內部使用,我仍然可能實現==和!=運算符。

如果實現IComparable,我只會實現<,<=,>和> =運算符。 只有當類型需要支持排序時才應該實現IComparable - 比如在排序或在SortedSet等有序通用容器中使用時。

如果集團或公司制定了一項政策,以便不實施==和!=運營商 - 那么我當然會遵循該政策。 如果有這樣的策略,那么使用Q / A代碼分析工具強制執行它是明智的,該工具在與引用類型一起使用時標記==和!=運算符的任何出現。

我相信得到一些像檢查對象一樣簡單正確的東西對於.NET的設計來說有點棘手。

對於Struct

1)實現IEquatable<T> 它顯着提高了性能。

2)因為你現在擁有自己的Equals ,所以重寫GetHashCode ,並與各種相等性檢查覆蓋object.Equals一致。

3)重載==!=運算符不需要虔誠地完成,因為如果你無意中將一個結構與另一個結構等同於==!= ,編譯器會發出警告,但這樣做是為了與Equals方法保持一致。

public struct Entity : IEquatable<Entity>
{
    public bool Equals(Entity other)
    {
        throw new NotImplementedException("Your equality check here...");
    }

    public override bool Equals(object obj)
    {
        if (obj == null || !(obj is Entity))
            return false;

        return Equals((Entity)obj);
    }

    public static bool operator ==(Entity e1, Entity e2)
    {
        return e1.Equals(e2);
    }

    public static bool operator !=(Entity e1, Entity e2)
    {
        return !(e1 == e2);
    }

    public override int GetHashCode()
    {
        throw new NotImplementedException("Your lightweight hashing algorithm, consistent with Equals method, here...");
    }
}

上課

來自MS:

大多數引用類型不應重載等於運算符,即使它們重寫等於。

對我來說==感覺像價值平等,更像是Equals方法的語法糖。 a == b比寫a.Equals(b)更直觀。 我們很少需要檢查參考平等。 在處理物理對象的邏輯表示的抽象級別中,這不是我們需要檢查的。 我認為為==Equals設置不同的語義實際上可能令人困惑。 我認為它應該是==對於值相等和Equals引用(或更好的名稱,如IsSameAs )平等。 我不想在這里認真對待MS指南,不僅因為它對我來說不自然,而且因為超載==沒有造成任何重大傷害。 這與不重寫非泛型EqualsGetHashCode ,它可以咬回來,因為框架不會在任何地方使用==但只有在我們自己使用它的時候。 沒有超載==!=獲得的唯一真正的好處是與我無法控制的整個框架的設計的一致性。 這確實是一件大事, 很遺憾我會堅持下去

使用引用語義(可變對象)

1)覆蓋EqualsGetHashCode

2)實現IEquatable<T>不是必須的,但如果你有一個,那就太好了。

public class Entity : IEquatable<Entity>
{
    public bool Equals(Entity other)
    {
        if (ReferenceEquals(this, other))
            return true;

        if (ReferenceEquals(null, other))
            return false;

        //if your below implementation will involve objects of derived classes, then do a 
        //GetType == other.GetType comparison
        throw new NotImplementedException("Your equality check here...");
    }

    public override bool Equals(object obj)
    {
        return Equals(obj as Entity);
    }

    public override int GetHashCode()
    {
        throw new NotImplementedException("Your lightweight hashing algorithm, consistent with Equals method, here...");
    }
}

使用值語義(不可變對象)

這是棘手的部分。 如果不加以照顧,很容易搞砸..

1)覆蓋EqualsGetHashCode

2)重載==!=匹配Equals 確保它適用於空值

2)實現IEquatable<T>不是必須的,但如果你有一個,那就太好了。

public class Entity : IEquatable<Entity>
{
    public bool Equals(Entity other)
    {
        if (ReferenceEquals(this, other))
            return true;

        if (ReferenceEquals(null, other))
            return false;

        //if your below implementation will involve objects of derived classes, then do a 
        //GetType == other.GetType comparison
        throw new NotImplementedException("Your equality check here...");
    }

    public override bool Equals(object obj)
    {
        return Equals(obj as Entity);
    }

    public static bool operator ==(Entity e1, Entity e2)
    {
        if (ReferenceEquals(e1, null))
            return ReferenceEquals(e2, null);

        return e1.Equals(e2);
    }

    public static bool operator !=(Entity e1, Entity e2)
    {
        return !(e1 == e2);
    }

    public override int GetHashCode()
    {
        throw new NotImplementedException("Your lightweight hashing algorithm, consistent with Equals method, here...");
    }
}

如果您的類可以繼承,請特別注意它應該如何運行,在這種情況下,您必須確定基類對象是否可以等於派生類對象。 理想情況下,如果沒有派生類的對象用於相等性檢查,那么基類實例可以等於派生類實例,在這種情況下,不需要在基類的通用Equals中檢查Type相等性。

一般注意不要重復代碼。 我可以創建一個通用的抽象基類( IEqualizable<T>左右)作為模板,以便更容易重用,但遺憾的是在C#中阻止我從其他類派生。

上面的所有答案都不考慮多態性,即使通過基本引用進行比較,通常也希望派生引用使用派生的Equals。 請在此處查看問題/討論/答案 - 平等和多態

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM