編寫高質量程式碼改善C#程式的建議(Day1)
建議1:正確操作字串
儘量避免裝箱,避免分配額外的記憶體空間
“str”+9會發生一次裝箱行為,而“str”+9.ToString()則不會,因為ToString()是通過直接操作記憶體完成int到string的轉換,效率比裝箱高很多。
裝箱之所以會帶來效能損壞,是因為它需要完成三個步驟:
1.為值型別在託管堆中分配記憶體;2.將值型別的在值複製到新分配的堆記憶體中;3.返回已經成為引用型別的物件的地址。
string型別一旦賦值就不可被改變,呼叫string類的任何方法或進行任何運算都會在記憶體中建立一個新的字串物件,也就是分配新的記憶體空間。
StringBuilder不會建立新的string物件,它效率源於預先以非託管的方式分配記憶體,如果沒有初始沒有定義長度則預設分配16,當stringbuilder長度大於16則重新分配記憶體,使之成為16的倍數。
string.Format方法內部使用StringBuilder進行字串的格式化。
建議2:使用預設轉型方法
方法包括:1.使用型別轉換運算子(隱式轉換、顯示轉換);2.使用型別內建的Parse/TryParse、ToString、ToDouble等方法;3.使用幫助類提供的方法System.Convert(ToChar、ToBoolean、ToString)提供基元型別轉換為其他基元型別的方法,還支援將任何自定義型別轉換為任何基元型別,只要繼承IConvertible介面就可以
建議3:區別對待強制轉型與as和is
強制轉型意味著兩種情況:
1.A類和B類彼此依靠轉換操作符來完成兩個型別之間的轉型。
public static explicit operator B(A a){
B b = new B(){Name="轉型自:"+a.Name};
return b;
} // A類轉B類 B b = (B)a;
2.A類是B類的基類。 B b = (B)a;
as無法操作基元型別,只能是引用型別或者可為null的型別(如int?),如果涉及基元型別需要通過is轉型前的型別來判斷,避免轉型失敗。
static void DoWithSomeType(object obj) { if(obj is SecondType) { SecondType secondType = obj as SecondType; } }
建議4:TryParse比Parse效率高
在Parse轉換失敗時,try..catch造成的損耗是比較高的,所以失敗的時候Parse的執行效率比TryParse低很多。轉換成功的情況下,TryParse的效率也比Parse稍微高一些。
建議5:使用int?來確保值型別也可以為null
基元型別需要為null的兩個場景:
1)資料庫中的int欄位可為null,在C#中,值被取出來需要賦值給int型別則需要確保int型別可為null;
2)在分散式系統中,伺服器需要接收並解析來自客戶端的資料,如果一個int型別資料在傳輸過程中被篡改了,轉型失敗後應該儲存為null值,而不是提供一個初始值;
Nullable<T>是可為空型別的泛型結構體,Nullable<int>的簡寫是int?
建議6:區別readonly和const的使用方法
使用const的理由只有一個,就是效率,其與readonly的區別如下:
1)const是編譯期常量,readonly是執行時變數;
2)const只能修飾基元型別、列舉型別或字元型別,天然是static的,而readonly沒有限制;
const效率高是因為經過編譯器編譯後,所有用const變數的地方都會用const變數所對應的實際值來代替;
readonly是執行時變數,它在執行時第一次被賦值後將不可以改變,“不可以改變”有兩層意思:
1)對於值型別變數,值本身不可改變;
2)對於引用型別變數,引用本身不可改變,但是引用物件的欄位值是可以改變的;
以上兩種情況均可在建構函式中再次賦值;
建議8:避免給列舉型別的元素提供顯示的值
一般情況下建立列舉就是為了代替使用實際的數值,不正確的為列舉型別元素設定顯示的值,會帶來意想不到的錯誤。
enum Week { Monday = 1, Tuesday = 2, ValueTemp, Wednesday = 3, Thursday = 4, Friday = 5, Saturday = 6, Sunday = 7 } Week week = Week.ValueTemp; Console.WriteLine(week); Console.WriteLine(week==week.Wednesday); 結果輸出為: Wednes True
可以看到輸出結果與預期的不一樣,在修改列舉元素的時候可能會一不小心就增加了無效值,列舉本身所包括的列舉元素都是值型別,ValueTemp會自動在Tuesday=2的基礎上+1,與Wednesday的值都是3,所以等於同一個值。
在列舉型別上指定[Flags]屬性,表明列舉值可以執行AND、OR等按位運算,
如 week = Week.Monday | Week.Tuesday;
建議9:習慣過載運算子
應該使型別支援:
int x = 1; int y = 2;
int total = x + y;
而不是用 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; } }
建議10:建立物件時考慮是否實現比較器
假如需要對一個10個人的Salary列表按基本工資排序的時候,可以繼承IComparable介面,如果想按獎金排序,可以繼承IComparer實現自定義比較器。
class Program { static void Main(string[] args) { List<Salary> companySalary = new List<Salary>(); companySalary.Add(new Salary() { Name = "Mike", BaseSalary = 3000, Bonus = 2000 }); companySalary.Add(new Salary() { Name = "Lance", BaseSalary = 2822, Bonus = 5000 }); companySalary.Add(new Salary() { Name = "Rose", BaseSalary = 4000, Bonus = 3000 }); companySalary.Add(new Salary() { Name = "Yami", BaseSalary = 2500, Bonus = 1500 }); companySalary.Add(new Salary() { Name = "Lili", BaseSalary = 3200, Bonus = 2500 }); companySalary.Sort(); foreach (Salary item in companySalary) Console.WriteLine(item.Name + "\t BaseSalary:" + item.BaseSalary.ToString() + "\t Bonus:" + item.Bonus.ToString()); companySalary.Sort(new BonusComparer()); Console.WriteLine(); foreach (Salary item in companySalary) Console.WriteLine(item.Name + "\t BaseSalary:" + item.BaseSalary.ToString() + "\t Bonus:" + item.Bonus.ToString()); Console.ReadKey(); } } class Salary : IComparable<Salary> { public string Name { get; set; } public int BaseSalary { get; set; } public int Bonus { get; set; } public int CompareTo(Salary other) { if (BaseSalary > other.BaseSalary) return 1; else if (BaseSalary == other.BaseSalary) return 0; else return -1; } } class BonusComparer : IComparer<Salary> { public int Compare(Salary s1, Salary s2) { return s1.Bonus.CompareTo(s2.Bonus); } }
建議11:區別對待==和Equals
無論是操作符“==”還是方法“Equals”,都傾向於表達這樣一個原則:
對於值型別,如果型別的值相等,就應該返回True;
對於引用型別,如果型別指向同一個物件,則返回True;
操作符“==”和“Equals”方法都是可以被過載的,對於一些自定義型別,想要實現“值相等性”可以過載Equals方法,保留“==”操作符“引用相等性”。(FCL提供Object.ReferenceEquals方法比較兩個例項是否是同一個例項)
建議12:重寫Equals時也要重寫GetHashCode
如果重寫Equals方法的時候不重寫GetHashCode方法,在使用如Dictionary基於雜湊的集合時候,會有一些潛藏的BUG。
static Dictionary<Person,PersonMoreInfo> PersonValues = new Dictionary<Person,PersonMoreInfo>(); Person mike = new Person("NB123"); PersonMoreInfo mikeValue = new PersonMoreInfo(){SomeInfo = "Mike is man"}; PersonValues.Add(mike,mikeValue); Console.WriteLine(PersonValues.ContainsKey(mike)); // return True Person mike = new Person("NB123"); Console.WriteLine(PersonValues.ContainsKey(mike)); // return False
實際上Dictionary是根據Key值的HashCode來查詢Value值,Person沒有實現過載GetHashCode,CLR會呼叫Object的GetHashCode方法,每new一個物件,CLR都會為該物件生成一個固定的整型值,GetHashCode就是對該整型值求雜湊碼,所以雖然上面兩個mike的屬性值都一直,但是他們預設實現的HashCode不一致,故第二個判斷的結果是False,若要修正該問題就必須重寫GetHashCode方法。
public override int GetHashCode() {
// 整型型別的容量顯然無法滿足字串的容量,為了減少兩個不同型別之間根據字串產生相同的HashCode的機率,故增加字首 return (System.Reflection.MethodBase.GetCurrentMethod().DeclaringType.FullName + "#"this.IDCode).GetHashCode(); }
建議13:為型別輸出格式化字串
最簡單的字串格式化輸出是重寫ToString,但是ToString提供的格式化輸出比較單一。
為型別提供格式化的字串輸出有兩種方式。
第一種是意識到型別會產生格式化字串的輸出,於是讓型別繼承IFormattable介面,這要求開發者可以預見型別在格式化方面的需求。
class Person : IFormattable { public string IDCode { get; set; } public string FirstName { get; set; } public string LastName { get; set; } public string ToString(string format, IFormatProvider formatProvider) { switch (format) { case "CH": return this.ToString(); case "EG": return string.Format("{0} {1}", FirstName, LastName); default: return this.ToString(); } } public override string ToString() { return string.Format("{0} {1}", LastName, FirstName); } }
第二種方法也是常用的靈活多變的方法,就是根據需求的變化為型別提供多個格式化器。
PersonFormatter personFormatter = new PersonFormatter();
Console.WriteLine(person.ToString("CH", personFormatter));
Console.WriteLine(person.ToString("EG", personFormatter));
Console.WriteLine(person.ToString("TW", personFormatter));
class Person : IFormattable
{
public string IDCode { get; set; }
public string FirstName { get; set; }
public string LastName { get; set; }
public string ToString(string format, IFormatProvider formatProvider)
{
switch (format)
{
case "CH":
return this.ToString();
case "EG":
return string.Format("{0} {1}", FirstName, LastName);
default:
ICustomFormatter customFormatter = formatProvider as ICustomFormatter;
if(customFormatter ==null)
return this.ToString();
return customFormatter.Format(format, this, null);
}
}
public override string ToString()
{
return string.Format("{0} {1}", LastName, FirstName);
}
}
class PersonFormatter : IFormatProvider, ICustomFormatter
{
public string Format(string format, object arg, IFormatProvider formatProvider)
{
Person person = arg as Person;
if (person == null)
return string.Empty;
switch (format)
{
case "TW":
return string.Format("TW {0} {1}", person.LastName, person.FirstName);
default:
return string.Format("{0} {1}", person.LastName, person.FirstName);
}
}
public object GetFormat(Type formatType)
{
if (formatType == typeof(ICustomFormatter))
return this;
else
return null;
}
}
建議14:正確實現淺拷貝和深拷貝
淺拷貝:將物件中的所有欄位複製到新物件(副本)中,副本中值型別欄位的修改不會影響源物件的值,應用型別的複製是複製引用,所以副本引用型別的欄位值修改會影響源物件本身。
深拷貝:不管是值型別還是引用型別都會重新建立並賦值。
繼承ICloneable介面的方式實現,淺度拷貝直接用MemberwiseClone方法,深度拷貝直接用序列化的形式來實現。
public object Clone() { using(Stream sm = new MemoryStream()) { IFormatter formatter = new BinaryFormatter(); formatter.Serialize(sm,this); sm.Seek(0,SeekOrigin.Begin);//設定當前流中的位置 return formatter.Deserialize(sm) as Employee; } }
建議15:使用dynamic來簡化反射實現
編譯器在編譯的時候不再對型別進行檢查,預設dynamic物件支援開發者想要的任何特性,假如執行時dynamic物件不包含指定的特性,則會丟擲RuntimeBinderException異常。
var與dynamic是兩種不同的概念,var是編譯器的語法糖,一旦被編譯,編譯器會自動匹配var變數的實際型別,並用實際型別替換該變數的宣告。而dynamic被編譯後,實際是一個object型別,只不過編譯器會對dynamic型別進行特殊處理,讓它在編譯期間不進行任何的型別檢查,而是將型別檢查放到了執行期。
public class DynamicSample { public string Name { get; set; } public int Add(int a,int b) { return a + b; } } 這樣使用反射: DynamicSample reflectSample = new DynamicSample(); var addMethod = typeof(DynamicSample).GetMethod("Add"); int result = (int)addMethod.Invoke(reflectSample, new object[] { 1, 2 }); 如果使用dynamic則程式碼更簡潔並且執行效率更快: dynamic dynamicSample = new DynamicSample(); int result = dynamicSample.Add(1,2);
優化的反射實現,犧牲程式碼整潔度提高執行效率
// 優化的反射實現 var reflectSampleBetter = new DynamicSample(); var addMethod2 = typeof(DynamicSample).GetMethod("Add"); var addDelegate = (Func<DynamicSample, int, int, int>)Delegate.CreateDelegate(typeof(Func<DynamicSample, int, int, int>), addMethod2); Stopwatch stopwatch3 = Stopwatch.StartNew(); for (var i = 0; i < times; i++) { addDelegate(reflectSampleBetter, 1, 2); } Console.WriteLine($"優化的反射實現耗時: {stopwatch3.ElapsedMilliseconds}毫秒");
建議始終使用dynamic來簡化反射實現。