編寫高質量代碼改善C#程序的157個建議讀書筆記【11-20】


章節索引

建議11:區別對待 == 和Equals

建議12:重寫Equals也要重寫GetHashCode

建議13:為類型輸出格式化字符串

建議14:正確實現淺拷貝和深拷貝

建議15:使用dynamic來簡化反射實現

建議16:元素數量可變的情況下不應使用數組

建議17:多數情況下使用foreach進行循環遍歷

建議18:foreach不能代替for

建議19:使用更有效的對象和集合初始化

建議20:使用泛型集合代替非泛型集合

 

建議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> ,關於裝箱拆箱的,不多說了,相信看過以上建議的朋友都比較熟悉了。

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM