避免陷阱,重現Equals方法您需要注意的其中2個原則
以下程式碼源自於真實專案,本人只是做了一點簡化,大家來找碴,看看哪些地方不妥:
<!--
Code highlighting produced by Actipro CodeHighlighter (freeware)
http://www.CodeHighlighter.com/
--> public class PathedObject
{
public PathedObject(object obj)
{
Object = obj;
ObjectPath = new object[0];
}
public PathedObject(object obj, object[] path)
{
System.Diagnostics.Debug.Assert(path != null);
Object = obj;
ObjectPath = path;
}
public object Object
{
get;
set;
}
public object[] ObjectPath
{
get;
internal set;
}
/**////
/// Rules:
/// Suppose Path 1 = A->B->D
/// Path 2 = A->C->D
/// Path 3 = D
///There are :
/// Path 1 == Path 3
/// Path 1 != Path 2
///
///
///
public override bool Equals(object obj)
{
PathedObject other = obj as PathedObject;
if (other == null)
return false;
if (this.ObjectPath.Length > 0 && other.ObjectPath.Length > 0)
{
if (!this.Object.Equals(other.Object))
return false;
for (int i = 0; i < ObjectPath.Length; i++)
{
if (!ObjectPath[i].Equals(other.ObjectPath[i]))
return false;
}
return true;
}
else
return this.Object.Equals(other.Object);
}
public override int GetHashCode()
{
int hashCode = Object.GetHashCode();
if (ObjectPath != null && ObjectPath.Length > 0)
{
foreach (object path in ObjectPath)
hashCode = HashCodeUtil.MixCodes(hashCode, path.GetHashCode());
}
return hashCode;
}
}
public class HashCodeUtil
{
public static int MixCodes(int code1, int code2)
{
int h = (code1 << 4) + code2;
int g = h & unchecked((int)0xF0000000);
if (g != 0)
h ^= g >> 24;
h &= ~g;
return h;
}
}
此前一文沒有釋出在首頁瀏覽量果然少的可憐,而且沒有得到反饋也不知是否大家都明白問題出在哪裡,或者我貼的程式碼過於羞澀,難以理解。
但我相信大部分熟讀過《CLR Via C#》一書的人應該明白問題出在哪裡,因為道理都在那本書裡擺著。
至於我為什麼寫此文重談一遍,一個是因為讀書歸讀書,碰到實際情況時就不見得也能保持冷靜明白個所以然,能夠避免踩此陷阱; 二則我也很難理解我們的架構師為什麼會犯此錯誤,是故意的呢還是不夠仔細踩了地雷。於是寫此文記錄一下,以免今後自己犯此錯誤。
轉入正題,如果不夠仔細的話,如果不是提交了一個ChangeList 將存放PathedObject集合用Dictionary替換了先前的List造成了功能回退的話,也許我不會留意上面的程式碼。然當我仔細瀏覽上面程式碼的時候,造成功能回退的原因也就很好理解了。
原則一:GetHashCode()方法跟Equals()方法應該保持一致
我們都知道重寫了Equals方法,必須要重寫GetHashCode,要不然就會有個編譯警告。如果兩個物件視為相同, 它們就必須要返回相同的HashCode, 這個實際上是由HashTable原理決定的。
在Just Reflect 一問中我藉助反射器簡單闡述了Hashtable的Contains方法實現的方式:在呼叫Contains方法時,其首先會用引數物件的HashCode去判斷對應的Hashtable槽有沒有被佔用,如果沒有就會直接返回false,有則會呼叫Equals方法判斷槽裡的物件跟引數是不是一致,然後。。。
瞭解這一點,我們也就不會對於下面的測試程式碼的結果感到詫異:
Code highlighting produced by Actipro CodeHighlighter (freeware)
http://www.CodeHighlighter.com/
--> static void Main(string[] args)
{
object a = "A";
object b = "B";
object c = "C";
object d = "D";
PathedObject pathOne = new PathedObject(d, new object[] { a, b, d });
PathedObject pathTwo = new PathedObject(d, new object[] { a, c, d });
PathedObject pathThree = new PathedObject(pathOne.Object);
Dictionary<PathedObject, object> dic = new Dictionary<PathedObject, object>();
dic.Add(pathOne, pathOne.Object);
List<PathedObject> list = new List<PathedObject>();
list.Add(pathOne);
Console.WriteLine(dic.ContainsKey(pathThree)); // 'False'
Console.WriteLine(list.Contains(pathThree)); // 'True'
Console.ReadLine();
}
用路徑D 去Dictionary中查詢時無一被命中,然而在List裡卻能到找。這是因為在於Dictionary和List 各自的Contains方法實現的方式不同, 在List裡它只會逐一遍歷呼叫Equals方法判等,找到就返回。
這就解釋了為什麼在我們專案中將存放PathedObject的集合用Dictionary替換了先前的List造成了Regression。因為根據目前GetHashCode方法,路徑A->B->D, 跟路徑D的產生的HashCode是不同的,這將導致呼叫系統封裝後的Contains方法返回值由原先的True變成了False,導致了Client端走了不同的流程從而造成了功能回退。
既然我們不能違背原則一,是不是我們只要簡單修改下GetHashCode的實現讓路徑A->B->D跟路徑D的PathedObject返回相同的HashCode就能解決此問題,例如如下的方法:
public override int GetHashCode()
{
return Object.GetHashCode();
}
問題是不是能解決了呢?如果重新Run我們的應用程式,你會驚喜的發現問題得到解決了,結果和我們期待的一樣。於是提交程式碼,皆大歡喜?
如果你多個心眼你會發現原來的GetHashCode實現視乎更合理些,那麼是不是有其他地方不妥呢?仔細再審視一遍PathedObject實現,我們會發現其實真正的問題在於Equals實現的邏輯。
原則二:Equals方法須滿足傳遞性。假設呼叫A.Equals(B),B.Equals(C)均返回true, 那麼呼叫A.Equals(C)也必須返回true
顯然根據目前的程式碼違背了這條原則:
假設有: Path 1 = A->B->D
Path 2 =
A->C->D
Path 3 = D
根據上面Equals實現的方式可以得到 Path 1 == Path 3; Path 3 == Path 2, 而Path 1 != Path 2,這就是問題的所在。
下面略帶解釋下PathedObject實現方式,和猜測下為什麼Equals方法按目前的方式實現,希望對大家理解有幫助。
我們的產品為一3D應用軟體,擁有很多的Instance,一個Instance包含了大量的幾何資訊,Instance需要能被重用。這個關係好比一個汽車有4個輪子,而每個輪子的幾何資訊是相同的,我們沒理由去保持四份這樣的幾何資訊。4個輪子雖然本身構造沒有區別,但他們出現位置是有區別的,前面,後面,左側,右側,於是我們引入了一個PathedObject標識他們,PathedObject物件分別包含了這個Instance和其的路徑資訊,這樣既能確保了只需儲存一份幾何資訊,又能標識不同的例項物件。我想這可能是很多三維軟體的通用做法。
而至於認為路徑A->B->D, 跟路徑D相同,我想這裡可能是為了貪圖方便。因為在系統底層我們保留的都是PathedObject物件,比方說我們用滑鼠去Pick,那麼是很容易得到當前位置對應的Instance的詳細路徑並加以保持。而對於上層可能希望更加關注於Instance物件本身而淡化其Path, 因此才會用如下的呼叫,並且希望能返回True。
PathedObject pathThree = new PathedObject(pathOne.Object);
PathedObjectList.Contains(pathThree)
最後, 原則三,應避免將HashCode儲存到持久層。
不幸的是我們的框架也違反了這一條,至於其中的道理還是請各位看官參閱《CLR Via C#》第五章吧,裡面還有很多g更多重寫Equals方法需要注意的地方,而且通俗易懂。
原文地址:http://www.cnblogs.com/anders06/archive/2009/10/19/1586093.html
來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/12639172/viewspace-617202/,如需轉載,請註明出處,否則將追究法律責任。
相關文章
- 七個Swift中的陷阱以及避免方法Swift
- jeesite的陷阱需要注意
- 重寫equals()方法時,需要同時重寫hashCode()方法
- java equals()方法的注意事項Java
- 2個軟體開發原則如何挽救您的專案 -Jordy Baylac
- Swift 中的 7 個陷阱以及如何避免Swift
- 網站建設需要注意哪些設計原則網站
- 重構的原則
- 建立堡壘機的原則有哪些?需要注意哪些方面?
- Java中父類方法重寫有哪些需要注意的?Java
- 重寫遵循的原則
- 雲端計算服務,多雲管理需要注意的陷阱!
- ios程式設計師職業中需要避免的八大「陷阱」iOS程式設計師
- java 中為什麼重寫 equals 後需要重寫 hashCodeJava
- 成功安裝MES要避免這3個陷阱
- 重構原則(Java)Java
- Golang 需要避免踩的 50 個坑Golang
- 5個需要避免的CSS錯誤CSS
- 建議重寫equals方法時也一併重寫hashCode方法
- Java重寫equals方法時為什麼要重寫hashcode方法Java
- 重寫Object.equals()方法和Object.hashCode()方法Object
- 網站設計過程中需要遵循的幾個原則網站
- 科普展館需要遵循的設計原則體現在哪裡?
- 秒殺系統的原則和注意項
- 重寫equals()時為什麼也得重寫hashCode()之深度解讀equals方法與hashCode方法淵源
- 雲中的資料管理,這七個常見陷阱要避免
- 專案管理應遵循的幾個原則(2)(轉)專案管理
- 真正需要學習的12個微服務設計原則微服務
- MySQL 中處理 Null 時要注意兩個陷阱MySqlNull
- static方法應用的原則
- MySql避免重複插入記錄的幾種方法MySql
- java為什麼要重寫hashCode和equals方法Java
- Javascript需要注意的幾個運算子JavaScript
- golang split需要注意的一個點Golang
- 10個需要注意的SQL問題SQL
- 利用副專案找 IT 工作,需要滿足這 3 個原則
- 十個你需要在 PHP 7 中避免的坑PHP
- 需要避免的6個雲原生開發問題