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

開篇

學生時代,老師常說,好記性不如爛筆頭,事實上確實如此,有些知識你在學習的時候確實倒背如流,可是時間一長又不經常使用了,可能就生疏了,甚至下次有機會使用到的時候,還須要上網查找資料,因此,還不如經常摘錄下來,即便下次忘記具體細節還能從我本身的博客中輕易的找出來呢,還能和各位園友分享知識,還有一點就是,讀書是一件鍥而不捨的事情,我大學期間試過從圖書館借回來的書,三個月限期已到了還沒讀完又還回去了,說到底就是沒有讀書的動力,因此開一個讀書筆記的文章系列也是頗有必要的,督促本身要把這本書啃完。web

章節索引

建議1:正確操做字符串拼接,避免Boxing小程序

建議2:使用默認轉型方法ide

建議3:區別對待強制轉型、as、is函數

建議4:TryParse比Parse好性能

建議5:使用int?確保值類型也能夠爲null學習

建議6:區別readonly和const的使用方法ui

建議7:將0值做爲枚舉的默認值spa

建議8:避免給枚舉類型的元素提供顯式的值設計

建議9:習慣重載運算符3d

建議10:建立對象時須要考慮是否實現比較器

 

 

建議1:正確操做字符串拼接,避免Boxing

1string str1 = "str1" + 9;
2string str2 = "str2" + 9.ToString();
    從IL代碼得知,第一行代碼會產生裝箱行爲,而第二行代碼9.ToString()並無發生裝箱行爲,它是經過直接操做內存來完成int到string的轉換,效率要比裝箱高,因此,在使用其餘值類型到字符串的轉換來完成拼接時,避免使用「+」來完成,而應該使用FCL提供的ToString()方法進行類型轉換再拼接;另外,因爲System.String類對象的不可變特性,進行字符串拼接時都要爲該新對象分配新的內存空間,因此在大量字符串拼接的場合建議使用StringBuilder。

建議2:使用默認轉型方法

一、使用類型的轉換運算符
其實就是使用內部的一個方法,轉換運算符分兩類:隱式轉換、顯式轉換(強制轉換)基元類型廣泛都提供了轉換運算符。
int i = 0;
float j = 0;
j = i; //int到float存在隱式轉換
i = (int)j; //float到int須要顯式轉換
自定義類型經過重載轉換運算符來實現這一類的轉換:
 class program
    {
        static void main(string[] args)
        {
            Ip ip = "127.0.0.1"; //經過Ip類的重載轉換運算符,實現字符串到Ip類型的隱式轉換
            Console.WriteLine(ip.ToString());
        }
    }
    public class Ip : Object
    {
        IPAddress value;
        //構造函數
        public Ip(string ip)
        {
            value = IPAddress.Parse(ip);
        }
        //重載轉換運算符,implicit 關鍵字用於聲明隱式的用戶定義類型轉換運算符。
        public static implicit operator Ip(string ip)
        {
            Ip iptemp = new Ip(ip);
            return iptemp;
        }
        //重寫基類ToString方法
        public override string ToString()
        {
            return value.ToString();
        }
    }

 

二、使用類型內置的Parse
在FCL中,類型自身會帶有一些轉換方法,好比int自己提供Parse、TryParse方法……
 
 
三、使用幫助類提供的方法
System.Convert提供將一個基元類型轉換到其餘基元類型的方法,如ToChar、ToBoolean等,若是是自定義類型轉換爲任何基元類型,只要自定義類型實現IConvertible接口而且實現相關的轉換方法便可;
ps:基元類型是指編譯器直接支持的數據類型,即直接映射到FCL中的類型,包括sbyte、byte、short、ushort、int、uint、long、ulong、char、float、double、bool、decimal、object、string。
四、CLR支持的轉換
即子類與父類的上溯轉換和下溯轉換;子類向父類轉換的時候,支持隱式轉換,而當父類向子類轉換的時候,必須是顯式轉換,就比如,狗(子類)是動物(父類),但,動物不必定是狗,也多是貓。
 
 
 

建議3:區別對待強制轉型、as、is

 secondType = (SecondType)firstType;
以上代碼發生強轉換類型,意味着下面兩種事情的其中一件;
1)FirstType和SecondType彼此依靠轉換操做符來完成兩個類型的轉換;
2)FirstType是SecondType的基類;
 
第一種狀況:FirstType和SecondType存在轉換操做符
    public class FirstType
    {
        public string Name { get; set; }
    }
    public class SecondType
    {
        public string Name { get; set; }
        //explicit 和 implicit 屬於轉換運算符,explicti:顯式轉換,implicit能夠隱式轉換
        public static explicit operator SecondType(FirstType firstType)
        {
            SecondType secondType = new SecondType()
            {
                Name = firstType.Name
            };
            return secondType;
        }
    }
這種狀況,必須使用強轉換,而不能使用as操做符

咱們再看看這種狀況,這段代碼編譯成功,可是運行時報錯,其緣由是萬類都繼承自object,可是編譯器會檢查o在運行時是否是SecondType類型,從而繞過了轉換運算符,因此建議,若是類型之間存在繼承關係,首選使用as,子類之間的轉換應該提供轉換運算符以便進行強制轉換。

第二種狀況:FirstType是SecondType的基類
這種狀況,既可使用as也可使用強制轉換,從效率和代碼健壯性來看,建議使用as,由於as操做符不會拋出異常,類型不匹配的時候,返回值爲null。
 
is和as:
object o = new object();
if (o is SecondType)
{
   secondType = (SecondType)o;
}
這段代碼實際效率不高,由於執行了2次類型檢測,is操做符返回boolean返回值,只是檢測並無轉換,而as操做符會進行轉換,若是轉換失敗則返回null;

建議4:TryParse比Parse好

//Parse
int a = int.Parse("123");
 
 //TryParse
int x = 0;
if (int.TryParse("123", out x))
{
  //轉換成功,x=123
}
else
{
  //轉換失敗,x=0
}
這個應該沒必要多說了,相信不少人都常用的,從.NET2.0開始,FCL開始爲基元類型提供TryParse方法以解決在Parse轉換失敗的時候觸發的異常所帶來的性能消耗;
在效率方面,若是Parse和TryParse都執行成功的話,它們的效率是在同一個數量級的,甚至在書中的實驗中,TryParse還比Parse高,若是Parse和TryParse都執行失敗的話,Parse的執行效率就大大低於TryParse了。

建議5:使用int?確保值類型也能夠爲null

在開發的過程當中,可能你也遇到過值類型不夠用的場景,好比,數據表字段設置爲int類型,而且容許爲null,這時反映在C#中,若是將null賦值給int類型的變量也不對,會報錯;
因此,從.NET2.0開始,FCL提供一種能夠爲Null的類型Nullable<T> 它是一個結構體:
public struct Nullable<T> where T: struct
可是結構體Struct是值類型,應該也不能爲空纔對啊,書中也沒有解釋得很深刻,很模糊的一兩句就帶過了,因而我繼續深刻探討,首先使用Reflector對mscorlib.dll反編譯;
public struct Nullable<T> where T: struct
{
    private bool hasValue;
    internal T value;
    public Nullable(T value);
    public bool HasValue { get; }
    public T Value { get; }
    public T GetValueOrDefault();
    public T GetValueOrDefault(T defaultValue);
    public override bool Equals(object other);
    public override int GetHashCode();
    public override string ToString();
    public static implicit operator T?(T value);
    public static explicit operator T(T? value);
}
不知道什麼緣由,當我展開這些方法的時候,都是空空的,可是,我發現它有重載轉換運算符,implicit 是隱式轉換,explicit 是顯式轉換
而後在寫一個小程序,代碼以下:
protected void Page_Load(object sender, EventArgs e)
{
    Nullable<int> a = null;
}
而後對這個web應用程序進行反編譯查看:
protected void Page_Load(object sender, EventArgs e)
{
    int? a = new int?();
}
能夠看出,Nullable<int> a = null; 最終是進行了初始化,而此時,hasValue屬性的值也應該爲False;
因此,我猜測,Nullable<int> 或者 int? ……等可空的基元類型設置爲null的時候,實際上並非像引用類型那樣爲null了,而是進行了初始化,而且hasValue屬性的值爲False。
猜測完以後,我去MSDN搜了一下,獲得驗證:http://msdn.microsoft.com/zh-cn/library/ms131346(v=vs.100).aspx

建議6:區別readonly和const的使用方法

    這個建議我打算本身寫一個比較簡明的例子來講明,而不使用書本的例子,即便有些工做幾年的朋友,也可能一會兒說不清楚const與readonly的區別,感受它們實現的效果也是同樣的,都表示一個不可變的值,其實它們的區別在於:
·const是編譯時常量(編譯時肯定下來的值)
·readonly是運行時常量(運行時才肯定)
 
下面創建一個DEMO來舉例說明:
一、新建一個類庫,新建Person類,設置以下兩個常量:
namespace ClassLibrary
{
    public class Person
    {
        public const int height = 100;
        public readonly static int weight = 100;
    }
}
二、在主程序中添加ClassLibrary類庫的引用,輸出常量:
protected void Page_Load(object sender, EventArgs e)
{
      Response.Write("身高:" + ClassLibrary.Person.height);
      Response.Write("體重:" + ClassLibrary.Person.weight);
}
此時毫無疑問的,輸出結果爲:身高:100體重:100,
 

三、修改Person類中的height、weight常量爲:170,,而且編譯該類庫(注意:只生成該類庫,而不生成主程序)
此時再運行主程序頁面,輸出結果爲:身高:100體重:170 ;
究其緣由,height爲const常量,在第一次編譯期間就已經將值100HardCode在主程序中了,而第二次修改值以後,並無生成主程序,因此,再次運行的時候,仍是第一次的值,咱們使用ILDASM來看看編譯後的IL代碼吧。

建議7:將0值做爲枚舉的默認值

    容許使用的枚舉類型有byte、sbyte、short、ushort、int、uint、long、ulong、應該始終將0值做爲枚舉的默認值; 書中這個建議舉的例子我不太明白,個人理解大概是這樣子的,假若有以下的枚舉
   enum Week
    {
        Money = 1,
        Tuesday = 2,
        Wednesday = 3,
        Thursday = 4,
        Friday = 5,
        Saturday = 6,
        Sunday = 7
    }
萬一你一不當心代碼寫成這樣
static Week week;
protected void Page_Load(object sender, EventArgs e)
{
    Response.Write(week);
}
輸出的結果爲0,就會讓人以爲是多了第八個值出來了,因此,建議使用0值做爲枚舉的默認值。

建議8:避免給枚舉類型的元素提供顯式的值

「通常狀況下,沒有必要爲枚舉元素提供顯示的值」
我以爲這個建議是無關緊要了,這個看我的習慣,做者的建議是假如咱們在上面的枚舉中,增長一個元素,代碼以下:
 enum Week
    {
        Money = 1,
        Tuesday = 2,
        TempValue,
        Wednesday = 3,
        Thursday = 4,
        Friday = 5,
        Saturday = 6,
        Sunday = 7
    }
此時,TempValue的值是什麼呢?
Week week = Week.TempValue;
Response.Write(week);
Response.Write(week==Week.Wednesday);
ValueTemp的結果倒是:Wednesday True;
若是沒有爲元素顯式賦值,編譯器會逐個爲元素的值+1,也就是自動在Tuesday=2的基礎上+1,最終TempValue和Wednesday的值都是3,而後做者的意願是但願乾脆就不要指定值了,由於編譯器會自動幫咱們+1,可是,個人想法是,若是不指定值的話,當咱們下次來看看這個枚舉的話,難道要數一數該元素排行第幾才能知道表明的Value嗎?並且,萬一枚舉有修改的話就有可能不當心修改而致使Value亂掉的狀況了。

System.FlagsAttribute屬性
當一個枚舉指定了System.FlagsAttribute屬性以後,就意味着能夠對這些值進行AND、OR、NOT、XOR按位運算,這就要求枚舉中的每一個元素的值都是2的n次冪指數了,其目的是任意個元素想加以後的值都不會和目前枚舉中的任一元素的值相同,書中關於這方面說得不多,只是提了個大概,因而我參考了些資料,作了個DEMO更加深刻的研究。
 [Flags]
    enum Week
    {
        None = 0x0,
        Money = 0x1,
        Tuesday = 0x2,
        Wednesday = 0x4,
        Thursday = 0x8,
        Friday = 0x10,
        Saturday = 0x20,
        Sunday = 0x40
    }
        protected void Page_Load(object sender, EventArgs e)
        {
            //利用「|」運算,將各個元素組合起來
            Week week = Week.Sunday | Week.Tuesday | Week.Thursday;
            Response.Write(GetDayOfWeek(week));
        }
        private string GetDayOfWeek(Week week)
        {
            string temp = string.Empty;
            foreach (Week w in Enum.GetValues(typeof(Week)))
            {
                //利用「&」運算拆分
                if ((week & w) > 0)
                    temp += string.Format("{0} <br>", w.ToString());
            }
            return temp;
        }
輸出結果爲:
Tuesday 
Thursday 
Sunday 
這種設計是利用了計算機基礎中的二進制數的「與」「或」運算,從而能夠巧妙的將各個元素組合起來成爲一個數據,而且能最後拆分出來,這種設計思想能夠普遍的應用在權限設計、收費方式……等須要多種數據組合的地方。
我再說說其中的原理吧,首先看我定義枚舉的值,對應出來的二進制數爲:
000一、00十、0100、1000 ……
舉個例子:好比0x1和0x8組合,對應的二進制數是:000一、1000,那麼他們經過「|」運算組合起來以後的值是:1001,
也就是調用GetDayOfWeek方法的時候,參數值爲1001了,而後遍歷枚舉的時候進行&運算拆分
   Monday:1001 & 0001 = 0001 結果大於0,符合條件
  Tuesday:1001 & 0010 = 0000 結果等於0,不符合條件
Wednesday: 1001 & 0100 = 0000 結果等於0,不符合條件
 Thursday: 1001 & 1000 = 1000 結果大於0,符合條件
因而,經過這種方法,就能找出當初組合起來的2個元素了。

建議9:習慣重載運算符

上幾個建議當中,咱們接觸太重載轉換符,使得能夠實現相似IPAddress ip="127.0.0.1";之類的不一樣類型的對象之間的轉換,使得代碼更加直觀簡潔,一樣的對於下面2段代碼:
(1)int total=x+y;
(2)int total=int.Add(x,y);
咱們固然但願看到的是第一種而不是第二種,由於第一種語法特性咱們大多數人看得習慣明解,因此,構建本身的類型的時候,咱們應該考慮是否能夠進行運算符重載。
class Salary
{
        public int RMB { get; set; }
        public static Salary operator +(Salary s1, Salary s2)
        {
            s2.RMB += s1.RMB;
            return s2;
        }
}
進行重載以後,就能夠這樣使用了,方便多了。
  Salary s1 = new Salary() { RMB = 10 };
  Salary s2 = new Salary() { RMB = 20 };
  Salary s3 = s1 + s2;

 

 

建議10:建立對象時須要考慮是否實現比較器

有對象的地方就會存在比較,過年回家,你媽也會把你跟人家的孩子來比,實現IComparable 接口便可實現比較排序功能;
咱們先來新建一個基礎的類來一步步看看是如何實現比較器的;
  class Salary  
    {
        public string Name { get; set; }
        public int BaseSalary { get; set; }
        public int Bonus { get; set; }
    }
由於ArrayList有sort()這個排序方法,那豈不是不用實現也能進行對比排序了嗎?事實果然如此的美好嗎?
ArrayList companySalary = new ArrayList();
companySalary.Add(new Salary() { Name = "A", BaseSalary = 2000 });
companySalary.Add(new Salary() { Name = "B", BaseSalary = 1000 });
companySalary.Add(
new Salary() { Name = "C", BaseSalary = 3000 }); companySalary.Sort(); //排序 foreach (Salary item in companySalary) { Response.Write(item.Name + ":" + item.BaseSalary); }
現實卻如此悲慘,由於對象類裏面有不少字段,編譯器不會智能到知道你要使用哪一個字段來做爲排序對比的字段的。

so,咱們必須對Salary類實現IComparable接口,而且實現接口成員CompareTo(object obj)
    class Salary : IComparable
    {
        public string Name { get; set; }
        public int BaseSalary { get; set; }
        public int Bonus { get; set; }
        //實現IComparable接口的CompareTo方法,比較器的原理
        public int CompareTo(object obj)
        {
            Salary staff = obj as Salary;
            if (BaseSalary > staff.BaseSalary)
            {
                return 1; //若是自身比較大,返回1
            }
            else if (BaseSalary == staff.BaseSalary)
            {
                return 0;
            }
            else
            {
                return -1;//若是自身比較小,返回1
            }
        }
    }
調用地方的代碼不用修改,程序再次跑起來,運行結果爲:
B:1000 A:2000 C:3000
OK,咱們再次深刻一點,假設這個月結算不以BaseSalary來排序,而是以Bonus獎金來排序,那該怎麼辦?固然,從新修改Salary類內部的CompareTo接口成員確定是能夠的,可是,比較聰明的方法就是自定義比較器接口IComparer(注意,剛纔實現接口名字叫IComparable,而自定義的比較器接口是IComparer)
 class BonusComparer : IComparer
    {
        public int Compare(object x, object y)
        {
            Salary s1 = x as Salary;
            Salary s2 = x as Salary;
            return s1.Bonus.CompareTo(s2.Bonus);
            //實際上,上例也可使用內部字段的CompareTo方法
            //可是因爲演示比較器內部原理,則寫了幾個if了。
        }
    }

Sort方法接受一個實現了IComparer接口的類對象做爲參數,因此,咱們能夠這樣子進行傳參
//提供非默認的比較器BonusComparer
companySalary.Sort(new BonusComparer());
關於比較器的內容,書中說到這裏就應該結束了,接下來是考慮比較的時候性能的問題,能夠想象,若是一個集合成千上萬的數據甚至更多須要比較的話,而上面的例子中,使用了類型轉換Salary s1 = x as Salary;這是很是消耗性能的,泛型的出現,能夠很好的避免類型轉換的問題:
一、ArrayList可使用List<T>來代替
二、使用IComparable<T> 、 IComparer<T> 來代替
Just Look Like That
    class Salary : IComparable<Salary>
    {
        public string Name { get; set; }
        public int BaseSalary { get; set; }
        public int Bonus { get; set; }
        public int CompareTo(Salary staff)
        {
            return BaseSalary.CompareTo(staff.BaseSalary);
        }
    }
    class BonusComparer : IComparer<Salary>
    {
        public int Compare(Salary x, Salary y)
        {
            return x.Bonus.CompareTo(y.Bonus);
        }
    }
相關文章
相關標籤/搜索