**************************************************************************
该书在线阅读:编写高质量代码:改善C#程序的157个建议
源代码下载:点我下载
**************************************************************************
第1章 基本语言要素 / 2
-------------------------------
建议1:正确操作字符串 / 2
-------------------------------
- 确保尽量少的装箱
- 避免分配额外的内存空间
注:string是个特殊的引用类型,一旦赋值就不可改变。在运行时调用System.String类中的任何方法或进行任何运算,都会在内存中创建一个新的字符串对象,这也意味着要为该新对象分配新的内存空间。
而StringBuilder不会重新创建一个string对象。
所以一旦需要对string类型进行多次操作,就应该用StringBulider,减少性能损耗!
---------------------------------
建议2:使用默认转型方法 /6
---------------------------------
- 使用类型的转换运算符:
implicit (隐式)、explicit(显示) + operator,同时必须加上public与static
public static implicit operator Cat(Animal a) { return new Cat() { Name = "Cat:" + a.Name }; }
(一般不建议用户对自己的类型重载转换运算符。如需用户自定义的类型之间需要转换,建议从面向对象的角度考虑,因为它们一般都含有某种关系(如继承、实现等)。在这种情况下,就应该使用第四种方法:CLR支持的转型。)
- 使用类型内置的Parse、TryParse,或者如ToString、ToDouble、ToDateTime等方法。
- 使用帮助类提供的方法。
即System.Convert。该类还支持将任何自定义类型转换为任何基元类型,只需继承IConvertible接口即可。
System.BitConvert提供了基元类型与字节数组之间相互转换的方法。
注意 所谓“基元类型”:是指编译器直接支持的数据类型,即直接映射到FCL中的类型。基元类型包括:sbyte / byte / short / ushort / int / uint / long / ulong / char / float / double / bool / decimal /object / string。
- 使用CLR支持的转型
即上溯转型和下溯转型。也是我们平时说的向上转换和向下转换。实际上就是基类与子类之间的相互转换。
Animal animal; //Animal为基类 Dog dog = new Dog(); //Dog为Animal的子类 animal = dog; //隐式转换,通过。 //dog = animal; //编译不通过。基类到子类不支持隐式 dog = (Dog)animal; //通过,执行成功! //此处需注意,上面能显示转换animal = dog这句话,此次animal保存的对象来之dog Animal a = new Animal(); Dog d = (Dog)a; //编译通过,执行失败,不允许将Animal类型转换为Dog类型
------------------------------------------
建议3:区别对待强制转型与as和is /9
------------------------------------------
如果类型之间都上溯到某个共同的基类,那么根据此基类进行的转型(即基类转型为子类本身)应该使用as。子类与子类之间的转型,则应该提供转换操作符,以便进行强制转型。
当能使用as的情况下,都应该使用as,因为as更安全效率更高。而且结合is使用更加完美。
但as有个问题,即它不能操作基元类型。
---------------------------------------
建议4:TryParse比Parse好 / 12
---------------------------------------
TryParse无需处理异常,效率快于Parse,尤其是在解析失败的时候!因此也有了一种模式叫TryParse模式。
-----------------------------------------------------
建议5:使用int?来确保值类型也可以为null / 15
-----------------------------------------------------
从.net2.0开始,FCL中提供了一个额外的类型:可以为空的类型Nullable<T>,简写T?
它是个结构体,只有值类型才可以作为“可空类型”(引用类型本身就能为NULL)。
基元类型能隐式转换为可空类型:
int i = 0; int? j = i;
但可空类型不能直接转换为基元类型,需要使用null 合并运算符:??
?? 运算符定义当可以为 null 的类型分配给非可以为 null 的类型时返回的默认值。
int? i = null; int j = i??0; //j=0
博文链接:c#各种运算符
-----------------------------------------------------
建议6:区别readonly和const的使用方法 / 16
-----------------------------------------------------
- const是一个编译器变量,static readonly是一个运行时常量
- const只能修饰基元类型、枚举类型或字符串类型,readonly没有限制。
- readonly只能用于类成员,不能用于方法的局部变量。const无此限制。
- const 字段只能在该字段的声明中初始化。 readonly 字段可以在声明或构造函数中初始化。(对于实例字段,在包含字段声明的类的实例构造函数中;或者,对于静态字段,在包含字段声明的类的静态构造函数中)
注意:
- const本身是编译期常量,所以就是static的,加上static会编译错误。
- readonly灵活性大于const,但性能却略低于const(极小)。所以推荐尽量使用readonly。
- readonly变量是运行时变量,只能在声明或构造函数中初始化(能在构造函数中多次赋值),在其他地方“不可以更改”。
“不可以更改”有两层含义:
-
- 对于值类型变量,值本身不可以改变(readonly,只读)
- 对于引用类型变量,引用本身(相当于指针)不可改变,但是其成员可被改变。
Sample2 sample2 = new Sample2(new Student() { Age = 10 }); sample2.ReadOnlyValue.Age = 20; //成功
-----------------------------------------
建议7:将0值作为枚举的默认值 / 19
-----------------------------------------
看下面的例子:
1 enum Week 2 { 3 Monday = 1, 4 Tuesday = 2, 5 Wednesday = 3, 6 Thursday = 4, 7 Friday = 5, 8 Saturday = 6, 9 Sunday = 7 10 } 11 12 static Week week; 13 14 static void Main(string[] args) 15 { 16 Console.WriteLine(week); //0 17 }
输出结果为:0;因为枚举内容为int类型。所以默认值始终取0。
同时还能给枚举赋值其他整型数值。
Week week = (Week)9;
------------------------------------------------------
建议8:避免给枚举类型的元素提供显式的值 / 20
------------------------------------------------------
先看段例子:
1 enum Week 2 { 3 Monday = 1, 4 Tuesday = 2, 5 ValueTemp, 6 Wednesday = 3, 7 Thursday = 4, 8 Friday = 5, 9 Saturday = 6, 10 Sunday = 7 11 } 12 13 static void Main(string[] args) 14 { 15 Week week = Week.ValueTemp; 16 Console.WriteLine(week); 17 Console.WriteLine(week == Week.Wednesday); 18 19 }
输出结果为:
Wednesday
True
红色的ValueTemp就是新增加的枚举值,出现上面的问题是因为当枚举元素没有被显示赋值时,编译器会为那些未赋值元素逐个+1赋值。
因此ValueTemp被赋值为3。而枚举中允许出现重复值,也就是多次赋值效果。换句话说3被赋值给Wednesday。
---------------------------------
建议9:习惯重载运算符 / 22
---------------------------------
1 class Program 2 { 3 static void Main(string[] args) 4 { 5 Salary mikeIncome = new Salary() { RMB = 22 }; 6 Salary roseIncome = new Salary() { RMB = 33 }; 7 //Salary familyIncome = Salary.Add(mikeIncome, roseIncome); 8 Salary familyIncome = mikeIncome + roseIncome; 9 } 10 } 11 12 class Salary 13 { 14 public int RMB { get; set; } 15 16 public static Salary operator +(Salary s1, Salary s2) 17 { 18 s2.RMB += s1.RMB; 19 return s2; 20 } 21 }
----------------------------------------------------------------
建议10:创建对象时需要考虑是否实现比较器 / 23
----------------------------------------------------------------
一般对需要比较或排序的对象,继承IComparable<T>接口,实现默认比较器。如果需要其他比较可以如下例子中创建非默认的比较器。
View Code
-----------------------------------------
建议11:区别对待==和Equals / 27
-----------------------------------------
相等性比较主要有三种:运算符==、Equals、ReferenceEquals(引用比较)。
- 对于值类型,如果类型的值相等,就应该返回True。
- 对于引用类型,如果类型指向同一个对象,则返回True。
View Code
-----------------------------------------------------------
建议12:重写Equals时也要重写GetHashCode / 29
-----------------------------------------------------------
例子:
View Code
上面代码中当不重写GetHashCode时输出为:
基于键值的集合(如上面的DIctionary)会根据Key值来查找Value值。CLR内部会优化这种查找,实际上,最终是根据Key值的HashCode来查找Value值。
Object为所有的CLR类型都提供了GetHashCode的默认实现。每new一个对象,CLR都会为该对象生成一个固定的整型值,该整型值在对象的生存周期内不会改变,而该对象默认的GetHashCode实现就是对该整型值求HashCode。
简单重写GetHashCode,
public override int GetHashCode() { return this.IDCode.GetHashCode(); }
输出为:
尽管这里GetHashCode已经实现了,但是还存在另外一个问题,它永远只返回一个整型类型,而整型类型的容量显然无法满足字符串的容量,以下的例子就能产生两个同样的HashCode。
string str1 = "NB0903100006"; string str2 = "NB0904140001"; Console.WriteLine(str1.GetHashCode()); Console.WriteLine(str2.GetHashCode());
为了减少两个不同类型之间根据字符串产生相同的HashCode的几率,一个稍作改进版本的GetHashCode方法:
public override int GetHashCode() { return (System.Reflection.MethodBase.GetCurrentMethod().DeclaringType.FullName + "#" + this.IDCode).GetHashCode(); }
--------------------------------------------
建议13:为类型输出格式化字符串 / 32
--------------------------------------------
输出格式化字符串一般有两种
- 简单重写ToString()方法
- 继承IFormattable接口,实现其方法ToString。
代码如下:
View Code
上面这种方法是在意识到类型会存在格式化字符串输出方面的需求时没提起为类型继承了接口IFormattable。如果类型本身没有提供格式化输出的功能,这个时候,格式化器就派上了用场。格式化器的好处就是可以根据需求的变化,随时增加或者修改它。
View Code
结合以上两个版本,可以得出最终版。(至于选择哪个版本,看具体需求确定)
View Code
--------------------------------------------
建议14:正确实现浅拷贝和深拷贝 / 36
--------------------------------------------
为对象创建副本的技术称为拷贝(也叫克隆)。将拷贝分为浅拷贝和深拷贝。
- 共同点:将对象中的所有字段复制到新的对象(副本)中。而且,值类型字段的值被复制到副本中后,在副本中的修改不会影响到源对象对应的值。
- 不同点:引用类型浅拷贝复制的是引用类型的引用。即修改副本引用类型的值即是修改源对象的值。引用类型深拷贝而是拷贝整个引用对象,如同值类型。
- 注:string类型尽管是引用类型,但因性质特殊,所以按值类型处理。
拷贝建议通过继承IConeable接口的方式实现。但是该接口只有一个Clone的方法。所以浅拷贝和深拷贝可以自己定义。
实现深拷贝的方法有很多,比如手动拷贝每个字段的方法,但是不推荐,该方法不灵活也容易出错。
建议使用序列化的形式进行深拷贝。
View Code
--------------------------------------------------
建议15:使用dynamic来简化反射实现 / 40
--------------------------------------------------
dynamic是Framework4.0的新特性。dynamic的出现让C#具有了弱语言类型的特性,编译器在编译的时候不再对类型进行检查,编译器默认dynamic对象支持开发者想要的任何特性。
利用dynamic的这个特性,可以简化C#中的反射语法,大大提高效率。
通过的例子比较性能:
View Code
从结果来看,优化的反射实现,其效率和dynamic在一个数量级上。可是它带来了效率,却牺牲了代码的整洁度,这种实现在我看来是得不偿失的。所以,现在又了dynamic类型,建议大家:
始终使用dynamic来简化反射实现。
注:以上是原文的例子,但是个人感觉有些问题,作者只是测试了调用的效率。如果反射一次,多次调用的话,作者的例子可以参考。
如果是反射一次,调用一次的模式的话,第三个优化反射中委托的创建也应该放进循环中,同时反射(GetMethod)也应该放进循环中。
以下是本人测试性能的例子:
TestReflection
由上面的结果可见:
个人建议:
- 如果这些需反射的类都实现了同一个接口,即需反射的成员继承同一个接口,那么将对象转换为接口,调用接口的成员,无需使用反射。
- 这些类没有实现同一接口,那么优先采用dynamic方式。
- 如果Framework版本低于4.0,那么只能采用反射的方式,至于用Invoke还是delegate,如果反射和调用次数相差不大,建议用Invoke,如果调用次数远多于反射,那么建议用委托。