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


開篇

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

章節索引

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

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

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

建議4:TryParse比Parse好

建議5:使用int?確保值類型也可以為null

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

建議7:將0值作為枚舉的默認值

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

建議9:習慣重載運算符

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

 

 

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

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

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

1、使用類型的轉換運算符
其實就是使用內部的一個方法,轉換運算符分兩類:隱式轉換、顯式轉換(強制轉換)基元類型普遍都提供了轉換運算符。
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();
        }
    }

 

2、使用類型內置的Parse
在FCL中,類型自身會帶有一些轉換方法,比如int本身提供Parse、TryParse方法……
 
         
3、使用幫助類提供的方法
System.Convert提供將一個基元類型轉換到其他基元類型的方法,如ToChar、ToBoolean等,如果是自定義類型轉換為任何基元類型,只要自定義類型實現IConvertible接口並且實現相關的轉換方法即可;
ps:基元類型是指編譯器直接支持的數據類型,即直接映射到FCL中的類型,包括sbyte、byte、short、ushort、int、uint、long、ulong、char、float、double、bool、decimal、object、string。
4、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來舉例說明:
1、新建一個類庫,新建Person類,設置如下兩個常量:
namespace ClassLibrary
{
    public class Person
    {
        public const int height = 100;
        public readonly static int weight = 100;
    }
}
2、在主程序中添加ClassLibrary類庫的引用,輸出常量:
protected void Page_Load(object sender, EventArgs e)
{
      Response.Write("身高:" + ClassLibrary.Person.height);
      Response.Write("體重:" + ClassLibrary.Person.weight);
}
此時毫無疑問的,輸出結果為:身高:100體重:100,
 

3、修改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 
這種設計是利用了計算機基礎中的二進制數的“與”“或”運算,從而可以巧妙的將各個元素組合起來成為一個數據,並且能最后拆分出來,這種設計思想可以廣泛的應用在權限設計、收費方式……等需要多種數據組合的地方。
我再說說其中的原理吧,首先看我定義枚舉的值,對應出來的二進制數為:
0001、0010、0100、1000 ……
舉個例子:比如0x1和0x8組合,對應的二進制數是:0001、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;這是非常消耗性能的,泛型的出現,可以很好的避免類型轉換的問題:
1、ArrayList可以使用List<T>來代替
2、使用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);
        }
    }

 

 
       


免責聲明!

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



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