章節索引
建議11:區別對待 == 和Equals
CLR中將“相等性”分為兩類:
1、值相等性:兩個變量包含的數值相等。
2、引用相等性:兩個變量引用的是內存中的同一個對象。
但並不是所有的類型的比較都是按照其本身,比如string是一個特殊的引用類型,但是在FCL中,string的比較就被重載為針對“類型的值”的比較,而不是“引用本身”的比較。對於自定義類型來說,如果想要實現這樣的值比較而不是引用比較的話,則需要重載Equals方法,比如對於Person類,如果IDCode相同,我們可以認為他們是同一個人。
class Person { public string IDCode { get; private set; } public Person(string idCode) { this.IDCode = idCode; } public override bool Equals(object obj) { return IDCode == (obj as Person).IDCode; } }
此時通過Equals去比較的話,則就會通過重載后的方法來進行了。
object a = new Person("ABC"); object b = new Person("ABC"); Console.WriteLine(a == b); //False Console.WriteLine(a.Equals(b)); //True
說到這里,作者依然沒說白“==”和“Equals”的區別,只是說了一句建議的話:“對於引用類型,我們要定義值相等性,應該僅僅去重載Equals方法,同時讓==表示引用相等性”。
同時,為了明確有一種方法來肯定比較的是“引用相等性”,FCL提供了Object.ReferenceEquals方法。
bool equal= object.ReferenceEquals(object a,object b);
外事不決問Google,內事不決靠反編譯、MSDN了。為了弄懂==和Equals的區別,我作如下搜集整理:
1、==是運算符,而Equals是方法;
2、對於值類型、string類型,==和Equals都是比較值內容相等,使用ILSpy對Int類型進行反編譯觀察;int類型中的Equals方法內部邏輯就是“==”;
// int [__DynamicallyInvokable, TargetedPatchingOptOut("Performance critical to inline across NGen image boundaries")] public bool Equals(int obj) { return this == obj; }
string類型則是判斷引用地址是否相同或者值內容是否相同,兩者有一個符合條件則視為“相等”,請看string類的反編譯代碼。
// string [__DynamicallyInvokable, ReliabilityContract(Consistency.WillNotCorruptState, Cer.MayFail), TargetedPatchingOptOut("Performance critical to inline across NGen image boundaries")] public bool Equals(string value) { if (this == null) { throw new NullReferenceException(); } return value != null && (object.ReferenceEquals(this, value) || (this.Length == value.Length && string.EqualsHelper(this, value))); }
3、對於引用類型,==和Equals都是比較棧內存中的地址是否相等,並且自定義類型中可以進行運算符重載== 或者Override Equals 來改寫認為兩對象相等的條件,比如Person類中,我認為只要IDCard相同即對象相同等,此時可以進行重寫或者重載。
看到這里,是不是覺得有點迷茫?==好像跟Equals差不多啊,為了想弄清這個問題,我加了作者陸敏技的QQ,以下是聊天記錄:

建議12:重寫Equals也要重寫GetHashCode
坑爹啊!上一個建議的代碼原來編譯成功,但編譯器會友情提示的,這里作者又引出了另外一個建議,何時了啊!

這是因為如果重寫Equals方法而不重寫GetHashCode方法,在使用Dictionary類的時候,可能會有一個潛在的Bug。
static Dictionary<Person, string> personValues = new Dictionary<Person, string>(); protected void Page_Load(object sender, EventArgs e) { AddPerson(); Person mike = new Person("Mike"); Response.Write(personValues.ContainsKey(mike)); //False } void AddPerson() { Person mike = new Person("Mike"); personValues.Add(mike, "mike"); Response.Write(personValues.ContainsKey(mike)); //True } 本段代碼輸出結果:True False
這段代碼的意思是,執行AddPerson()的時候,將idCode=Mike的Person對象存進Dictionary中,然后在Page_Load方法內,也同樣new一個idCode=Mike的Person對象,使用ContainsKey方法搜索是否存在此對象Key,結果是不存在此對象。
你可以會問,上一個建議中,我們已經重寫了Person類的Equals方法了,只要idCode相等,我們就可以認為他們是相等的了,為什么此處會找不到Mike呢?
答:這是由於CLR已經優化了Dictionary這種查找,實際上是根據Key值的HashCode來查找Value值的。CLR首先調用Person類型的GetHashCode方法,發現這貨根本就沒有重寫,於是就向上找Object的GetHashCode方法,Object為所有的CLR類型都提供GetHashCode默認實現,每new一個對象,CLR都會為該對象生成一個固定整形值,在對象生命周期內不會改變,對象默認的GetHashCode實現就是該整型值的HashCode,所以,雖然Mike值相等,但是HashCode是不相等的。
若要修正此問題,就必須重寫GetHashCode方法
public override int GetHashCode() { return this.IDCode.GetHashCode(); }
進一步改進:GetHashCode方法存在一個問題,它返回的是一個整形類型,而整形類型的容量長度遠遠無法滿足字符串的長度,也就是說,值不相同的情況下,HashCode可能存在相同的情況,為了減少產生相同HashCode的情況,做改進版本:
public override int GetHashCode() { return (System.Reflection.MethodBase.GetCurrentMethod().DeclaringType.FullName + "#" + this.IDCode).GetHashCode(); }
小結:這個建議至少讓我了解了HashCode,以前重寫ToString方法的時候,就經常看到GetHashCode這個東東。
建議13:為類型輸出格式化字符串
這個建議我讀了兩次才明白啊。
1、實現IFormattable接口實現ToString()輸出格式化字符串
一般我們為類型提供格式化字符串的輸出的做法是重寫ToString(),但是這種方法提供的字符串輸出是非常單一的,所以我們可以實現IFormattable接口的ToString方法,可以讓類型根據用戶的輸入而格式化輸出,因為重寫的ToString方法沒有參數,而實現 IFormattable接口的的ToString方法有參數,還是看代碼最清晰。
public class Person : IFormattable { public string FirstName { get; set; } public string LastName { get; set; } //重寫的ToString方法輸出字符串比較單一 public override string ToString() { return string.Format("{0},{1}", FirstName, LastName); } //實現IFormattable接口的ToString方法因為有參數,所以可以實現復雜的邏輯 public string ToString(string format, IFormatProvider formatProvider) { if (format == "ch") return string.Format("中文名字:{0},{1}", FirstName, LastName); else return string.Format("EnglishName:{0},{1}", FirstName, LastName); } }
這樣子調用:
Person p = new Person() { FirstName="wayne", LastName="chan" }; Response.Write(p.ToString()); Response.Write(p.ToString("ch",null)); Response.Write(p.ToString("english", null));
2、格式化器
上面的方法是在預見類型會存在格式化字符串輸出的需求的時候,提前為類型實現了接口IFormattable,如果類型本身沒有提供格式化字符串輸出的功能,這時“格式化器”就派上用場了。
//針對Person的格式化器 class PersonFormatter : IFormatProvider, ICustomFormatter { //IFormatProvider成員 public object GetFormat(Type formatType) { if (formatType == typeof(ICustomFormatter)) return this; else return null; } //ICustomFormatter成員 public string Format(string format, object arg, IFormatProvider formatProvider) { Person person = arg as Person; if (person == null) return string.Empty; switch (format) { case "Ch": return string.Format("{0}{1}", person.LastName, person.FirstName); case "Eg": return string.Format("{0}{1}", person.FirstName, person.LastName); default: return string.Format("{0}{1}", person.FirstName, person.LastName); } } }
一個典型的格式化器應該要實現IFormatProvider, ICustomFormatter 接口,如果使用的話,就先初始化一個格式化器,如下:
Person person = new Person() { FirstName = "wayne", LastName = "chan", IDCode = "aaaa" }; //初始化格式化器
PersonFormatter pFormatter = new PersonFormatter(); Response.Write(pFormatter.Format("Ch", person, null));
其實看到這里,我覺得這個建議已經是非常細致的.NET知識了,一般人遇到這種情況,直接就會使用上一種方法了,在看書的時候,我也想直接跳過算了,但最后想,還是把他也記錄下吧,畢竟這也是對自己的提高啊,即使以后還是會把這個知識點遺忘掉,還是可以在本博客找回來啊。
建議14:正確實現淺拷貝和深拷貝
淺拷貝和深拷貝的區別:
淺拷貝:
修改副本的值類型字段不會影響源對象對應的字段,修改副本的引用類型字段會影響源對象,因為源對象復制給副本對象的時候,是引用類型的引用地址,也就是兩者引用的是同一個對象。
深拷貝:
無論值類型還是引用類型的字段,修改副本對象不會影響源對象,即使是引用類型,也是重新創建了一個新的對象引用。
要想自定義類型具有Clone拷貝的能力,就得繼承ICloneable接口,然后根據需求,實現Clone方法以便實現淺拷貝或者深拷貝。
淺拷貝示例:
namespace WebApplication { public class Employee : ICloneable { public string IDCode { get; set; } public int Age { get; set; } public Department Department { get; set; } //實現ICloneable接口成員 public object Clone() { return this.MemberwiseClone(); } } public class Department { public string Name { get; set; } public override string ToString() { return this.Name; } } public partial class WebForm1 : System.Web.UI.Page { protected void Page_Load(object sender, EventArgs e) { //初始化Employee對象employeeA Employee employeeA = new Employee() { IDCode = "A", Age = 10, Department = new Department() { Name = "DepartmentA" } }; //從employeeA 淺拷貝出 employeeB Employee employeeB = employeeA.Clone() as Employee; //修改employeeB對象的屬性 employeeA.IDCode = "B"; employeeA.Age = 15; employeeA.Department.Name = "DepartmentB"; //輸出以便驗證 Response.Write(employeeB.IDCode); // A Response.Write(employeeB.Age); //10 Response.Write(employeeB.Department.ToString()); //DepartmentB } } }
從輸出結果可以驗證得到結果:
1、IDCode即使是string引用類型,Object.MemberwiseClone 依然為其創造了副本,在淺拷貝中,我們可以將string當做值類型來看待。
2、Employee的Department屬性是引用類型,改變源對象employeeA中的值,會影響到副本對象employeeB
深拷貝示例
建議使用序列化的形式進行深拷貝:
//實現ICloneable接口成員 public object Clone() { //淺拷貝 //return this.MemberwiseClone(); //使用序列化進行深拷貝 using (Stream objectStream = new MemoryStream()) { IFormatter formatter = new BinaryFormatter(); formatter.Serialize(objectStream, this); objectStream.Seek(0, SeekOrigin.Begin); return formatter.Deserialize(objectStream) as Employee; } }
這里我按照書中的代碼來運行程序,結果爆黃頁錯誤了,提示信息是:
中的類型“WebApplication.Employee”未標記為可序列化。
因為之前有相關的開發經驗,知道那是因為實體類沒有被標記為序列化屬性,難道作者編寫示例的時候沒有檢查出這個錯誤?或者是其他原因?
我們在實體類上標記一下即可運行成功,這是修改源對象employeeA中的值也不會影響到副本對象employeeB了。
[Serializable] public class Employee : ICloneable [Serializable] public class Department
建議15:使用dynamic來簡化反射實現
dynamic是Framework4.0的新特性,dynamic的出現讓C#具有了弱語言類型的特性,編譯器在編譯的時候,不再對類型進行檢查,不會報錯,但是運行時如果執行的是不存在的屬性或者方法,運行程序還是會拋出RuntimeBinderException異常。
var 與 dynamic 的區別
var是編譯器給我們的語法糖,編譯期會匹配出實際類型並且替換該變量的聲明。
dynamic 被編譯后,實際是一個object類型,只不過編譯器對dynamic做特殊處理,將類型檢查放到了運行期。
這從VS的編譯器窗口可以看出來,var 聲明的變量在VS中有智能提示,因為VS能推斷出來實際類型;dynamic聲明的變量沒有智能提示。
利用dynamic 簡化反射
public class DynamicSample { public string Name { get; set; } public int Add(int a, int b) { return a + b; } } public partial class DynamicPage : System.Web.UI.Page { protected void Page_Load(object sender, EventArgs e) { //普通的反射做法 DynamicSample dynamicSample = new DynamicSample(); var addMethod = typeof(DynamicSample).GetMethod("Add"); int res = (int)addMethod.Invoke(dynamicSample, new object[] { 1, 2 }); //dynamic的做法,簡潔,推薦 dynamic dynamicSample2 = new DynamicSample(); int res2 = dynamicSample2.Add(1, 2); //Add不會智能提示出來 } }
使用dynamic還有一個優點就是,比沒有優化過的反射性能好,跟優化過的反射性能相當,但代碼整潔度高,作者也是貼了代碼並貼出運行結果而已,沒有作過多的介紹,所以此處作罷了。
建議16:元素數量可變的情況下不應使用數組
1、從內存使用角度看,數組在創建時被分配一段固定長度的內存,數據的存儲結構一旦被分配,就不能再變化;
2、ArrayList是鏈表結構,可以動態增減內存空間;
3、List<T>是ArrayList的泛型實現,省去了拆箱和裝箱帶來的開銷。
基於數組本身在內存的特點,因此,在使用數組的時候需要注意大對象(占用內存找過85000字節的對象)的問題,因為他們會被分配在大對象堆里,在回收過程中效率極低,所以,數組的長度不宜過份大。
再來回應本建議主旨,現在我們知道數組是不可變的,如果非得讓數組變成“可變”的,那就只有像String那樣,重新構造一個新的數組,再Copy過去了,這樣可想性能是如此的差啊。
public static class ClassForExtensions { public static Array ReSize(this Array array, int newSize) { //返回當前數組、指針或引用類型包含的或引用的對象的 System.Type Type t = array.GetType().GetElementType(); //構造一個滿足需要的新數組 Array newArray = Array.CreateInstance(t, newSize); //將舊數組的內容Copy到新數組 Array.Copy(array, 0, newArray, 0, Math.Min(array.Length, newSize)); return newArray; } }
總結:
此建議跟“如果大規模string字符串拼接就用StringBuilder”異曲同工。
建議17:多數情況下使用foreach進行循環遍歷
為什么會有這個建議,我就有些不解了,作者先是參照IEnumerator、IEnumerable自己實現了一個類似的迭代器,然后說它的內部實現用了for循環或者是while循環,寫法都有點啰嗦,然后就說foreach出現了,還說foreach最大限度簡化了代碼,然后開始分析IL了,關於這個建議點,我覺得說得挺含糊的,不過根據作者的觀點,foreach循環除了提供簡化的語法外,還有兩個優勢。
1、自動將代碼置入try-finally塊
2、若類型實現IDispose接口,foreach會在循環結束后自動調用Dispose方法。
建議18:foreach不能代替for
foreach不支持循環時對集合進行增刪操作,而for循環可以,其原因是foreach循環使用了迭代器進行集合的遍歷,在迭代器里維護了一個集合版本的控制,我們對集合進行增刪操作的時候,都會產生一個新的版本號,當foreach循環調用MoveNext 方法遍歷元素時會對版本號進行檢測,一旦檢測版本號變動,則拋出異常,以下是我使用ILSpy反編譯得出的代碼, IEnumerator接口只定義了MoveNext成員,具體實現需要反編譯其實現類,我是對List<T>進行反編譯的。
// System.Collections.Generic.List<T>.Enumerator [__DynamicallyInvokable] public bool MoveNext() { List<T> list = this.list; if (this.version == list._version && this.index < list._size) { this.current = list._items[this.index]; this.index++; return true; } return this.MoveNextRare(); }
List<T>中對版本號的檢測沒有拋出異常,而某些實現類則會,比如:ArrayList類
// System.Collections.ArrayList.ArrayListEnumeratorSimple public bool MoveNext() { if (this.version != this.list._version) { throw new InvalidOperationException(Environment.GetResourceString("InvalidOperation_EnumFailedVersion")); } // other code }
而for循環則不會出現這個問題,我們通常在for循環的內部使用索引器來對集合成員的訪問,不對版本號進行判斷檢測。以下是對List<T>的索引器的反編譯代碼。
// System.Collections.Generic.List<T> [__DynamicallyInvokable] public T this[int index] { [__DynamicallyInvokable, TargetedPatchingOptOut("Performance critical to inline across NGen image boundaries")] get { if (index >= this._size) { ThrowHelper.ThrowArgumentOutOfRangeException(); } return this._items[index]; } [__DynamicallyInvokable, TargetedPatchingOptOut("Performance critical to inline across NGen image boundaries")] set { if (index >= this._size) { ThrowHelper.ThrowArgumentOutOfRangeException(); } this._items[index] = value; this._version++; } }
可以看出,get屬性沒有對_version版本號進行檢測,只要索引不超過size即可,而set屬性,會對_version版本號+1。
建議19:使用更有效的對象和集合初始化
這個建議應該很多人都知道或者都已經在用了,如果你還不知道,那你就out了。
List<Person> list = new List<Person>(); Person p = new Person(); p.ID = 1; p.Name = "Tommy"; list.Add(p);
騷年,你還在這樣進行對象、集合初始化嗎?奧特了,借助了.NET的高級語法,我們可以使用對象和集合的初始化器來寫出更加優雅的代碼。設定項在大括號中對屬性進行賦值
List<Person> lst = new List<Person>() { new Person(){ ID=1,Name="Tommy"}, new Person(){ ID=2,Name="Sammy"} };
初始化設定項除了為對象、集合初始化方便外,還為Linq查詢時的匿名類型進行屬性的初始化的方便。
List<Person> lst = new List<Person>() { new Person(){ Age = 10,Name="Tommy"}, new Person(){ Age = 20,Name="Sammy"} }; var entity = from p in lst select new { p.Name, AgeScope = p.Age > 10 ? "Old" : "Young" }; foreach (var item in entity) { Response.Write(string.Format("name is {0},{1}", item.Name, item.AgeScope)); }
AgeScope 屬性是經過計算得出的,有了如此方便的初始化方式,使得代碼更加優雅靈活。
建議20:使用泛型集合代替非泛型集合
這個建議老生長談了,盡量不要使用ArrayList,而是應該使用List<T> ,關於裝箱拆箱的,不多說了,相信看過以上建議的朋友都比較熟悉了。
