2011-11-02 55 views
4

我在尋找在域模型中實現相等性時有關最佳實踐的建議。在我看來,有三(3)類型的平等:.NET域模型中的對象平等

  1. 參照平等 - 這意味着這兩個對象都存儲在同一 物理內存空間。

  2. 身份平等 - 意味着兩個對象都具有相同的身份值。 例如,具有相同訂貨號 的兩個訂單對象表示相同的實體。當將值存儲在列表,哈希表等中時,這是特別重要的,並且 對象需要唯一標識用於查找。

  3. 價值平等 - 兩個對象都具有相同的所有屬性。

按照慣例,.NET提供了兩種方法來測試相等性:Equals和==。那麼我們如何將三(3)種類映射到兩(2)種方法?

當然,我忽略了MS添加的Object.ReferenceEquals,因爲認識到大多數人都在壓倒Equals,因爲參照平等不是他們想要的行爲。所以也許我們可以將第一種類型(?)分開。

鑑於GetHashCode和Equals在哈希表上下文中的行爲,可以肯定地說Equals總是應該提供Identity Equality嗎?如果是這樣,我們如何爲呼叫者提供測試價值平等的方式?

而且,不要大多數開發者認爲Equals和==會產生相同的結果嗎?由於==測試引用相等,這是否意味着當我們覆蓋Equals時,我們也應該重載==?

您的想法?

UPDATE

我不知道所有的細節,但我被告知(在與同事的面對面的談話)是WPF有數據綁定對象使用的Equals參考平等的嚴格要求或數據綁定無法正常工作。

另外,看着典型的Assert類,還有更令人困惑的語義。 AreEqual(a,b)通常會使用暗示着Identity或Value Equality的Equals方法,而AreSame(a,b)將ReferenceEquals用於引用相等。

+0

到目前爲止,我發現的一切似乎都指向WPF,使用'Equals()'而不是'ReferenceEquals()'或'=='來執行相等操作。 [鏈接1](http://www.lhotka.net/weblog/DataRefreshInWPF.aspx)[鏈接2](http://kentb.blogspot.com/2007/03/beware-datacontext-and-equals.html) 。可能是因爲你的同事改變了類的'Equals()'方法以單向工作,改變了數據,期望數據綁定更新,並且它不是因爲Equals()仍然返回true? –

+0

謝謝,我認爲第二個鏈接的開頭段落完美地解釋了它(並且比我的同事更好!)。 – SonOfPirate

回答

0

我想我會建議從上面的帖子我的總結以及外部對話作爲一個答案,而不是通過更新原來的職位迷惑的話題。我會把這個話題放開,讓讀者在選擇之前投票選出他們認爲最好的答案。

下面是我從這些討論gleened的關鍵點:

  1. 實體的定義本身就在域模型具有同一性。

  2. 總根是(根據我讀過的定義),包含其他實體的實體;因此,一個總體也具有身份。

  3. 雖然實體是可變的,他們的身份不應該。

  4. 微軟指南指出,當GetHashCode()方法的兩個對象是相等的,equals需要對這些對象返回true。

  5. 當存儲在哈希表中的實體,GetHashCode的應該返回表示實體的身份的值。

  6. 身份平等並不意味着參照性平等或價值平等。價值平等並不意味着參照平等。但是,參照平等確實意味着同一性和價值平等。

真相被告知,我已經意識到的是,這可能只是一個語法/語義問題。我們需要第三種定義平等的方式。我們有兩個:

的Equals。在領域模型中,當兩個實體共享相同的身份時,它們是等於。我覺得這一定是爲了滿足以上#4 &#5。我們使用實體的身份來生成從GetHashCode返回的哈希碼,因此,必須使用相同的值來確定等於

。基於現有的用法(在調試和測試框架中),當兩個對象/實體相同時,它們引用相同的實例(參考平等)。

???。那麼我們如何在代碼中指示價值平等?

在我所有的談話中,我發現我們正在應用限定詞來形式化這些術語;使用「IdentityEquals」和「IsSameXYZ」等名稱,因此「Equals」意味着價值平等或「IsEquivalentTo」和「ExactlyEquals」意味着價值平等,因此「Equals」意味着身份平等。

雖然我很欣賞的靈活性,更多的我走這條道路,我就越意識到沒有任何開發此看到相同的方式。這會導致問題。

而且我可以告訴你,每一個開發者,我談過,一個一個,表明他們希望「==」表現得正好等於相同。但是,即使我們覆蓋Equals,微軟建議不要重載「==」。如果核心==運算符只是簡單地委託給Equals,那本來就不錯。

因此,底線,我會忽略equals提供身份平等,提供我們的基類,用於參照平等sameAs的方法(只是一個的ReferenceEquals方便包裝)和重載==等於什麼,他們是一致的。然後,我將使用比較器來「比較」兩個「相等」實體的值。

更多想法?

1

對於引用平等,我使用object.ReferenceEquals如您所說,儘管您也可以將引用轉換爲對象並進行比較(只要它們是引用類型)。

對於2和3,它真的取決於開發人員想要什麼,如果他們想要將相等定義爲身份或值相等。通常,我喜歡將Equals()保留爲值相等,然後爲外部比較器提供身份相等。

大多數比較項目的方法都讓您能夠傳入自定義比較器,這就是我通常在任何自定義相等比較器(如身份)中傳遞的位置,但這就是我。

正如我所說,這是我的典型用法,我也構造的對象模型在那裏我只考慮屬性的子集來表示身份和其他人不比較。

您可以隨時創建一個非常簡單的ProjectionComparer,它可以接受任何類型的數據並根據投影創建一個比較器,使得在需要的地方傳遞自定義比較器以識別標識等非常容易,並且只需等待Equals()方法值。

而且,通常情況下,我個人不超載==除非我寫的是需要典型的比較操作的值類型,因爲運算符重載和超載怎麼都沒有覆蓋這麼大的混亂。

但同樣,這只是我的:-)

UPDATE意見這裏是我的投影比較器,你可以找到許多其他的實現,當然,不過這一次很適合我,它實現了兩個EqualityComparer<TCompare>(支持bool Equals(T, T)int GetHashCode(T)IComparer<T>支持Compare(T, T)):

public sealed class ProjectionComparer<TCompare, TProjected> : EqualityComparer<TCompare>, IComparer<TCompare> 
{ 
    private readonly Func<TCompare, TProjected> _projection; 

      // construct with the projection 
    public ProjectionComparer(Func<TCompare, TProjected> projection) 
    { 
     if (projection == null) 
     { 
      throw new ArgumentNullException("projection"); 
     } 

     _projection = projection; 
    } 

    // Compares objects, if either object is null, use standard null rules 
      // for compare, then compare projection of each if both not null. 
    public int Compare(TCompare left, TCompare right) 
    { 
     // if both same object or both null, return zero automatically 
     if (ReferenceEquals(left, right)) 
     { 
      return 0; 
     } 

     // can only happen if left null and right not null 
     if (left == null) 
     { 
      return -1; 
     } 

     // can only happen if right null and left non-null 
     if (right == null) 
     { 
      return 1; 
     } 

     // otherwise compare the projections 
     return Comparer<TProjected>.Default.Compare(_projection(left), _projection(right)); 
    } 

    // Equals method that checks for null objects and then checks projection 
    public override bool Equals(TCompare left, TCompare right) 
    { 
     // why bother to extract if they refer to same object... 
     if (ReferenceEquals(left, right)) 
     { 
      return true; 
     } 

     // if either is null, no sense checking either (both are null is handled by ReferenceEquals()) 
     if (left == null || right == null) 
     { 
      return false; 
     } 

     return Equals(_projection(left), _projection(right)); 
    } 

    // GetHashCode method that gets hash code of the projection result 
    public override int GetHashCode(TCompare obj) 
    { 
     // unlike Equals, GetHashCode() should never be called on a null object 
     if (obj == null) 
     { 
      throw new ArgumentNullException("obj"); 
     } 

     var key = _projection(obj); 

     // I decided since obj is non-null, i'd return zero if key was null. 
     return key == null ? 0 : key.GetHashCode(); 
    } 

    // Factory method to generate the comparer for the projection using type 
    public static ProjectionComparer<TCompare, TProjected> Create<TCompare, 
        TProjected>(Func<TCompare, TProjected> projection) 
    { 
     return new ProjectionComparer<TCompare, TProjected>(projection); 
    } 
} 

這使你可以做這樣的事情:

List<Employee> emp = ...; 

// sort by ID 
emp.Sort(ProjectionComparer.Create((Employee e) => e.ID)); 

// sort by name 
emp.Sort(ProjectionComparer.Create((Employee e) => e.Name)); 
+0

所以,如果我遵循你,你的Equals覆蓋將執行完整的Value Equality,並且如果你使用散列表或字典與對象一起提供一個比較器來散列表來執行Identity Equality。正確? – SonOfPirate

+0

@SonOfPirate:通常。這只是如果我需要一個,如果我的類主要是一個功能類(如DAO等),我通常不會打擾。只有在POCO中它真的成爲一個問題。這隻取決於我的需求,我是否需要價值或身份平等爲目前的商業案例。 –

+0

@SonOfPirate:有很多次我不會覆蓋Equals(),只會使用投影比較器,因爲我不需要定義相等的類。 –

1

我通常發展我的域模型的方式是圍繞==ReferenceEquals()進行參考平等。和Equals()執行價值平等。我用這些都不對身份平等的原因有三方面:

並非事事有身份,所以會引起有關如何混淆的Equals()和==其實當無身份對象參與工作。想一想有關包含多個實體的緩存或臨時/幫助對象的例子。關於可能基於幾個不同的域對象的聚合對象呢?它會比較哪個身份?

身份平等是價值平等的一個子集,根據我的經驗,每當涉及身份平等時,價值平等並不遙遠,通常價值身份包括身份平等。畢竟,如果身份不一樣,這些值是否真的是一樣的?

什麼是它自己的身份平等真的說,問自己這個問題:「沒有上下文,身份平等意味着什麼?」 Id 1的用戶是否等於Id 1的評論?我當然希望不是因爲兩個實體都是非常不同的東西。

那麼爲什麼使用任何內建的等式方法(==Equals())來處理例外情況,而不是規則?相反,我傾向於實現一個基類,該基類提供我的身份信息並實現身份平等,具體取決於身份平等在我當前的域中的普遍程度。

例如;在身份平等非常罕見的領域,我會創建一個自定義EqualityComparer<T>,以在上下文敏感的方式中在需要的時間和地點進行身份平等,如果身份平等不是我當前域中的常見問題。

但是,在身份平等很常見的域中,我會選擇一個名爲IdentityEquals()的身份基類中的方法,該方法負責基本級別上的身份相等性。

這樣我只在相關性和邏輯性的地方暴露身份平等。沒有任何潛在的混淆關於我的任何平等檢查可能如何工作。無論是Equals(),==IdentityEquals/EqualityComparer<T>(取決於身份平等在我的域中的普遍程度)。

另外,作爲一個方面說明我會建議閱讀微軟的guidelines for overloading equality

具體做法是:

默認情況下,運營商==參考相等測試通過 確定兩個引用是否表示相同的對象,所以引用 類型並不需要實現==操作符,以獲得這 功能。當類型是不可變的,這意味着包含在 的實例中的數據不能被改變,重載運算符==比較 值相等,而不是參考平等可以是有用的,因爲如 不可變的對象,它們可以,只要認爲是相同的他們 具有相同的值。 不推薦使用非不可變類型覆蓋運算符== 。

編輯:

關於Assert.AreEqualAssert.AreSame,您的域名定義了相等的意思;無論是參考,身份還是價值。因此,通過擴展,您的域中的Equals的定義也延伸到Assert.AreEqual的定義。如果你說Equals檢查身份相等,那麼通過邏輯擴展Assert.AreEqual將驗證身份相等。

Assert.AreSame同時檢查對象是否是同一物體。相同和相等是兩個不同的概念。檢查A引用的對象是否與B引用的對象相同的唯一方法是引用相等。語義和語法上這兩個名稱都有意義。

+0

我同意一致性是需要的,閱讀指南後,這就是我發佈該主題的原因。我不確定EqualityComparer是否是答案,因爲一個Equals方法產生與另一個Equals方法不同的結果是我試圖避免的。我將不得不更多考慮。 – SonOfPirate

+0

FWIW - 在面向服務的應用程序中,身份相等的用例很常見,其中一個對象是從服務請求實現的,需要與現有的域對象進行比較。這兩個對象很可能對一個或多個屬性具有不同的值,但仍然代表相同的實體。 – SonOfPirate

+0

我會澄清我的第一條評論。我開發了其他開發人員使用的框架和工具。瞭解我的目標受衆,使用需要清晰,一致或快速接受。 – SonOfPirate