改善C#程序的50种方法(第二版)

合集下载
  1. 1、下载文档前请自行甄别文档内容的完整性,平台不提供额外的编辑、内容补充、找答案等附加服务。
  2. 2、"仅部分预览"的文档,不可在线预览部分如存在完整性等问题,可反馈申请退款(可完整预览的文档不适用该条件!)。
  3. 3、如文档侵犯您的权益,请联系客服反馈,我们会尽快为您处理(人工客服工作时间:9:00-18:30)。

C#高效编程改善C#程序的50种方法(第2版)读书笔记
第一部分:C#的语言元素
一、用属性代替可访问的字段
1、.NET数据绑定只支持对属性的数据绑定,而不支持公有数据成员;
2、在属性的get和set访问器中可使用lock添加多线程的支持。

二、用readonly(运行时常量)而不是const(编译时常量)
1、const只可用于基元类型、枚举、字符串,而readonly则可以是任何的类型;
2、const在编译时将替换成具体的常量,这样如果在引用中同时使用了const和readonly两种值,则对readonly的再次改变将会改变设计的初衷,这是需要重新编译所更改的程序集,以重新引用新的常量值。

3、const(属于编译时常量)比readonly(运行时)效率高,但失去了应用的灵活性。

三、用is与as操作符而不是强制类型转换
1、两者都是在运行时进行类型的转换,as操作符只能使用在引用类型,而is可以使用值和引用类型;
2、通常的做法是用is判断类型,然后选择使用as或强类型转换操作符(用operater 定义的转换)有选择地进行。

四、ConditionalAttribute代替#if #endif条件编译
1、ConditionalAttribute只用于方法级,对其他的如类型、属性等的添加都是无效的;而#if #endif则不受此限制;Conditional 特性强制我们将条件代码拆分为若干独立的方法,有助于我们代码的高效性。

语法:[Conditional(“DEBUG”)]。

2、ConditionalAttribute可以添加多个编译条件的或(OR)操作,例如:[Conditional(“DEBUG”),Conditional(“TRACE”)],而#if #endif则可以添加与(AND),若想创建一个依赖多个环境变量的条件例程,我们必须使用#if时,应该避免将可执行代码放入其中,即#if只不是创建新的符号而已;
3、ConditioanlAttribute定义可以放在一个单独的方法中,使得程序更为灵活。

#if 和#endif会在Release和Debug版本中都留下一个方法,虽然在Release中什么也不做,但是方法加载、JIT编译还是会有一些开销。

且易引发一些意想不到的Bug。

五、为类型提供ToString()方法
1、可以通过重写以更友好的方式提供用户需要的信息,让类型的ToString()方法输出有用的信息;【C#3.0中编译器会为所有匿名类型创建一个默认的ToString()方法,将显示对象中的每个属性值。


2、使用IFormatter.ToString()方法提供更灵活的定制,如果添加IFormatProvider 和ICustomFormatter接口则更有意义的定制消息输出。

六、理解几个等同性判断间的关系
1、当我们创建类型时,应该为类型定义“等同性”含义。

C#提供4种不同的函数来判断两个对象的是否“相等”,ReferenceEquals(Object left , object right)、Equals(object left , object right)、virtual Equals(object right)、
Operator==(MyClass left , MyClass right)。

2、对于前两个我们永远不需要重新定义,ReferenceEquals()方法判断依据是对象的标识,无论对象是引用还是值类型(意味着两个相同值的值类类比较永远是False);静态Equals()方法实质是调用了Left参数的Equals()方法来判断两个对象是否相等。

3、可重写的Equals()方法默认使用对象标识判断,即比较对象是否引用相等,但对于值类型准备准备效率不高,所以建议在创建值类型时,都覆写其ValueType.Equals()方法,而对于引用类型只当需要改变其预定义语义时才重写。

例如字符串需要使用值语义而不是引用语义来判断是否相等。

4、当覆写Equals()方法时,也要同时实现IEquatable<T>,也应当重写GetHashCode()方法,同时提供operater==()操作。

七、理解GetHashCode()的陷阱
1、GetHashCode()仅在一种地方用到,即基于散列的集合定义键的散列值时,包括HashSet<T>和Dictionary<K,V>容器等。

Object提供的GetHashCode()效率低下,创建大多数类型,最好的的办法是完全避免实现GetHashCode();
2、如果创建的类型将被当做散列表中的键来使用,则需要自己实现GetHashCode()。

3、重写GetHashCode()必须遵循以下3个原则:
(1)如果两个对象相等(由Operator==定义),那么它们必须生成相同的散列码。

(2)对于任何一个对象A,应当是一个实例不变式,即A.GetHashCode()必须保持不变。

(3)对于所有输入,散列函数都应该在所有整数中按照随机分布生成散列码。

八、推荐使用查询语法而不是循环
1、查询语法比循环具有更强的可读性。

可以将过滤(Where)、排序(Orderby)、和一个投影(Select)组合在一次遍历中实现。

2、Linq并行计算扩展,只需要简单的在查询后加.AsParallel()方法即可。

2、foreach可以消除编译器对for循环对数组边界的检查;
foreach的循环变量是只读的,且存在一个显式的转换,在集合对象的对象类型不正确时抛出异常;
3、foreach使用的集合需要有:具备公有的GetEnumberator()方法;显式实现了IEnumberable接口;实现了IEnumerator接口;
4、foreach可以带来资源管理的好处,因为如果编译器可以确定IDisposable接口时,可以使用优化的try…finally块;
九、避免API中使用转换操作符
1、通过构造函数而不是转换操作符来创建对象,C#只有使用New操作符时才会创建新对象。

2、在访问被替换的对象时,和使用者打交道的实际上是一些临时对象或者内部的字段,这些临时对象被修改后就会被丢弃。

而且进行类型转换的代码是由编译器完成的,所以Bug 很难被发现。

十、使用可选参数减少方法重载数量
1、C#可选参数和命名实参可以大大简化重载代码数量并具有相当的灵活性
2、在C#4.0中Ref在COM场景中也变成了可选,几乎所有的参数都会以引用形式传递,即使这些参数不会被调用方法修改。

3、具名参数可以避免参数顺序所带来的混乱。

例SetName(lastName:”aaa” , firstName:”bbb”);
4、对于程序集第一次发布,可以随意使用可选参数和具名参数的,并任意给出你想提供的重载,而在进行后续发布时,必须为额外的参数创建重载,这样才能保证现有程序的正
常运行;在任何的后续发布中,都要避免修改参数的名称,因为参数名称已经成为了公有接口的一部分。

十一、理解短小方法的优势
1、在C#中,手工进行的额外优化反而会拖慢JIT的运行速度,要尽量写出最清晰的代码,将优化工作交给JIT编译器完成。

如:将函数逻辑直接写进循环以期降低方法调用。

2、JIT编译器以方法为单位进行编译,CLR将按照函数粒度逐一进行JIT 编译,没有被调用的方法不会被JIT编译,如果要处理太多IL,将大大减慢程序的启动速度。

3、如果将较长的Switch中的Case语句的代码替换成一个一个的方法,则JIT编译器所节省的时间将成倍增加;
4、提高程序的启动和运行速度,就必须注意分支语句的优化,因为分支诗句在第一次调用时,其所有分支都会被JIT编译,而我们却只会用到某个分支中的内容。

5、短小精悍的方法并选择较少的局部变量可以获得优化的寄存器使用,即选择哪些局部变量放在寄存器中,而不是栈上,越少使用局部变量,方法内的控制分支越少,JIT就会更方便的找到最适合的寄存器中的那一些。

6、保证代码的尽可能的清晰可读,也就让JIT更容易的分析并做出优化,方法越简单越好,因为它适合内联,不过虚方法和Try/catch块中的方法不会被内联。

第二部分:.NET资源管理
十二、推荐使用成员初始化而不是赋值语句
1、在声明变量时就进行初始化。

而不是在每个构造函数中进行。

若没有指定构造函数,C#编译器将为你的类型创建一个默认的构造函数。

2、如下三种情况下要避免使用初始化器:
(1)想要初始化的对象为0或null时,因为0或null的初始化很底层,会直接使用CPU指令将一整块内存设置为0,此时再执行一次额外的0初始化是多此一举;
(2)对同一个对象执行不同初始化方式,使用初始化器会产生垃圾对象,降低代码的执行效率;
(3)需要对字段进行异常处理时。

十三、正确初始化静态成员变量
1、静态初始化器会在一个类的任何方法、变量或者属性访问之前执行,甚至在调用基类的静态构造函数之前执行;
2、使用静态构造函数而不是初始化器最常见的理由是处理异常,在使用静态初始化器时,无法捕获异常。

十四、尽量减少重复的初始化逻辑
1、默认参数让我们在处理构造函数时有了更多的选择,但使用了new()约束的泛型类不支持所有参数都有默认值的构造函数,所以类必须提供显示的无参构造函数。

2、sring.Empty不是一个编译期常量,只是sring类的一个属性,所以sring.Empty 不能用作参数默认值。

3、C#4.0以前不支持默认参数,这时可以使用构造器链,让一个构造函数调用声明在同一个类中的另一个构造函数,而不是创建一个公用的辅助方法。

4、构造函数定义中只能使用一个初始化器,要么使用This()委托给另一个构造函数,要么使用base()调用基类的构造函数,二都不可兼得。

5、推荐使用This()委托,并使用初始化器来初始化简单资源,使用构造函数来初始化需要复杂逻辑的成员,同时不将调用抽取到一个构造函数中,以便减少重复。

十五、利用using和try/finally语句来清理资源
1、使用非托管系统资源的类型必须显示调用IDisposable接口的Dispose()来释放,Using()语句可以保证调用到Dispose(),Using()语句将生成一个Try/finally块,包裹住分配的对象。

2、仅在编译期类型支持IDisposable接口时,你才能使用Using语句,对于编译期无法确定的对象,则不适用。

若需要同时分配多个IDisposable对象,应该使用Try/finally。

十六、尽量减少内存垃圾,避免创建非必要对象
1、分配和销毁一个堆上的对象都要花费额外的处理器时间,所有的引用类型,即使是局部变量,都是在堆上分配的,引用类型的局部变量在函数退出后马上成为垃圾;
2、减少分配对象数量的技巧:经常使用的局部变量提升为成员变量(字段);提供一个类,用于存储某个类型常用实例的单例对象。

3、用StringBuilder进行复杂的字符串操作。

十七、实现标准的销毁模式
1、使用非托管系统资源的类型必须提供一个终结器,GC运行时,会清理掉那些没有提供终结器的托管垃圾对象,而提供了终结器的对象则会停留在内存中,添加到终结队列中,由GC调用另一个线程来执行终结器。

2、实现IDisposable接口是一种标准做法,用来通知使用者和运行时系统对该对象包含的资源需要及时释放。

在需要IDisoposable接口的类型中,即使我们不需要一个终结器也应该实现一个终结器。

为需要多态的类型添加一个受保护的虚方法Dispose(),派生类通过重写这个方法来释放自己的任务;
3、使用IDisposable.Dispose()方法需要做四个方面的工作:释放所有的非托管资源;释放所有的托管资源;设置一个状态标记来表示是否已经执行了Dispose();调用
GC.SuppressFinalize(this)取消对象的终结操作;
十八、区分值类型和引用类型
1、值类型不支持多态,其最佳用途是存放在应用程序中用到的数据。

2、用值类型表示底层数据存储的类型,用引用类型来封装程序的行为。

3、使用接口而不是使用类型可以避免装箱,即将值类型从接口实现,然后通过接口调用成员。

十九、保证0为值类型的有效状态
1、在创建枚举值时,请确保0是一个有效的选项。

2、系统初始化过程中将把所有的值类型都设置为0。

因此应该尽量将0设置为最可能的默认值,对于作为标志的枚举,应用用0表示所有选项都没有设置。

二十、保证值类型的常量性和原子性
1、常量性指变量创建后其值就保持不变的特性,具有常量性的类型是线程安全的,还可以安全的暴露给外界。

2、注意常量类型中的可变引用类型字段。

第三部分:用C#表达你的设计
二十一、限制类型的可见性
1、使用接口来暴露类型的功能,可以使我们更方便地创建内部类,同时又不会限制他们在程序集外的可用性;
2、暴露给外界的接口越少,越方便日后的修改维护及单元测试。

二十二、定义并实现接口优于继承类型
1、不相关的类型可以共同实现一个共同的接口,而且实现接口比继承更容易;
2、接口比较稳定,他将一组功能封装在一个接口中,作为其他类型的实现合同,而基类则可以随着时间的推移进行扩展。

3、扩展方法可以应用在接口上,使接口看上去仿佛可以有具体的实现。

二十三、明辨接口实现和虚方法重写
1、在基类中实现一个接口时,派生类需要使用new来隐藏对基类方法的使用;
2、可以将基类接口的方法申明为虚方法,然后再派生类中实现。

二十四、使用委托表示回调
1、委托提供了类型安全的回调定义,多用Linq功能实现查询,因为Linq所有功能都依赖于委托实现,是委托的简化版;
2、委托是运行时回调的最好方式,通过显示调用委托链上的每个委托目标可以避免多播委托仅返回最后一个委托的输出。

3、委托对象本身不提供任何异常捕获,所以任何的多播委托调用都会结束整个调用链。

二十五、使用事件模式实现通知
1、.net中的事件就是观察者模式的一个语法上的快捷实现,当需要创建事件时,应当声明为共有的事件,让编译器为我们创建add和renmove方法;
2、使用ponentModel.EventHandlerList容器来存储各个事件处理器,在类型中包含大量事件时可以使用他来隐藏所有事件的复杂性。

3、EventHandlerList并没有提供内建的泛型实现,基于Dictionary可以自行构建。

二十六、避免返回内部类对象的引用
1、由于值类型对象的访问会创建一个该对象的副本,所以定义一个值类型的的属性完全不会改变类型对象内部的状态;
2、常量类型可以避免改变对象的状态;
3、定义接口将访问限制在一个子集中从而最小化对对象内部状态的破坏;
4、定义一个包装器对象来限制另一个对象的访问;
5、希望客户代码更改内部数据元素时可以实现Observer模式,以使对象可以对更改进行校验或相应。

二十七、让类型支持序列化
1、.net的序列化支持非常简单,没有理由不提供,只要添加一个Serializable特性。

2、.net序列化会将对象中的所有成员变量保存到一个输出流中,且还支持任意的对象图,即使对象中有循环引用,也可以正确的存储和恢复。

3、Serializable特性同时支持二进制序列化和SOAP序列化。

4、可以通过添加【NonSerialized】特性来阻止对象被序列化,使用了该特性的对象在序列化时将只地得到系统默认值:0或Null。

在添加了NonSerializedAttribute的非序列化的属性时可以通过实现IDeserializationCallback的OnDeserialization()方法装入默认值。

二十八、提供粗粒度的因特网服务API
1、程序中最耗时的是与远程服务器之间的数据传输,尽可能把一次通信把需要的所有数据就位后,再让客户端一次性的把所有信息发送给服务器;
2、在合理设计中,既要降低每次传输的数据量,又要尽量减少传输的次数;
二十九、支持泛型协变和逆变
1、类型变体即所谓的协变和逆变,定义了在何种情况下,某个类型可以代替另一个类型使用。

若不能将一个类型替换成另一个,则这个类型就叫不变量,在C#4.0之前,所有泛型类型都不变量,应该尽可能的让泛型接口和委托支持协变和逆变。

2、协变和逆变是两种不同形式的类型替换,如果某个返回类型可以由其派生类型替换,那么就是支持协变;若某个参数类型可以由其基类替换,那么这个类型就是支持逆变的。

面向对象语言一般都支持协变。

3、应该尽可能的为泛型接口加上In和Out修饰,例IEnumerable<T>返回类型支持协变,IComparable<in T>方法参数支持逆变。

4、支持逆变的类型参数仅可作为方法参数或在委托参数的某些位置中出现。

第四部分:使用框架
三十、重写优于事件处理器
1、一个事件处理器抛出异常,则事件链上的其他处理器将不会被调用,而重写的虚方法则不会出现这种情况;
2、重写要比关联事件处理器高效得多,事件处理器需要迭代整个请求列表,这样占用了更多的CPU时间;
3、覆写只能用于派生类中,其他所有类型必须使用事件机制;
4、事件是运行时绑定,具有更多的灵活性,可以对同一个事件关联多个响应。

三十一、使用IComparable<T>和IComparer<T>实现顺序关系
1、IComparable接口定义了类型的自然顺序,而IComparer则用来描述其他的顺序;
2、IComparable接口用于为类型实现最自然的排序关系,重载四个比较操作符,可以提供一个重载版的CompareTo()方法,让其接受具体类型作为参数;
3、实现IComparable时请使用显示接口实现,并提供一个强类型版本的重载,这个强类型的重载能提高性能,并降低使用者误用CompareTo方法的可能。

三十二、避免使用ICloneable接口
1、对于值类型永远不需要支持ICloneable接口,使用默认的赋值操作即可,并比Clone()更高效;对于叶子类,仅在真正需要复制操作时再添加ICloneable支持。

2、ICloneable弊大于利,这也是.NET Framework在升级支持泛型时没有添加ICloneable<T>的原因。

3、对于可能需要支持ICloneable接口的基类,应该为其创造一个受保护的复制构造器,并应当避免支持IConeable接口。

三十三、仅用new修饰符处理基类更新
1、new修饰符必须小心谨慎的使用。

如果它是有歧意的,你就等于在类上创建了个模糊的方法。

2、只有在特殊情况下才使用,那就是升级基类时与你的类产生冲突时。

即使在这种情况下,也应该小心的使用它。

最重要的是,其它任何时候都不要用它。

三十四、避免重载基类中定义的方法
尽量选用不一样的方法名称。

注意:这个原则不针对于覆写。

三十五、PLINQ如何实现并行计算
1、Linq to SQL或Entity Framework会借助数据引擎来执行并行查询;
2、每个并行查询都开始于一个分区操作,分区是PLINQ最重要的步骤之一,分区不应该占用太多时间,主要有范围分区、区块分区、条带分区、散列分区;
3、通过在查询开始时添加AsParallel()方法,将查询表达式转换成并行执行。

4、除了分区,PLINQ还有时种算法让任务并行执行:管道(Pipelining)、停止并进行(Stop&Go)和反向枚举。

5、并行算法受限于Amdahl定律——使用多处理器带来程序性能的提升受限于程序的顺序执行部分。

三十六、理解PLINQ在I/O密集场景中的应用
1、并行任务库(Parallel Task Library)在默认情况下就能很好的处理I/O密集操作;
2、最后不要写自己的诊断库,.NET FCL 已经拥有了我们需要的核心库。

三十七、注意并行算法中的异常
1、使用并行计算时必须注意异常处理,添加异常控制;
2、在任何一个算法中,处理异常都很复杂,并行任务则更增加了其复杂性,最好的做法是不要让并行任务中执行的代码抛出异常。

第五部分:C#中的动态编程
三十八、理解动态类型的优劣
1、C#动态类型是为了让静态代码能够更加平滑地与其他使用动态类型的环境进行交互,并不是为了鼓励在一般场景中使用动态编程;
2、使用动态类型和在运行时创建表达式树都会带来性能上的影响。

3、当你能控制程序中所有涉及的类型时,可以引入一个接口,而不必使用动态编程,这样C#类型系统将保证代码中很少出现类型错误,编译器也能生成高效的代码。

三十九、使用动态类型表达泛型类型参数的运行时类型
1、System.Linq.Enumerable.Cast<T>将序列中的对象转换成了T,从而使得LINQ 可以配合IEnumerable工作;
2、Conver<T>要比Cast<T>适用性更广,但同时也会执行更多的工作。

四十、将接受匿名类型的参数声明为dynamic
1、你可以在需要配合匿名类型使用的方法中使用动态类型,但一定不要创建太多的动态调用;
2、长远来看,具体类型更易于维护,编译器和类型系统也会为其提供更好的支持;不过若要编写一两个使用匿名类型的辅助方法,那么动态调用则是一个简单易行的方法。

四十一、用DynamicObject或IDynamicMetaObjectProvider实现数据驱动的动态类型
1、动态编程的好处在于可以依使用方式,类型的仅有接口可以在运行时改变,创建动态功能类型的最简单方法就是继承System.Dynamic.DynamicObject;
2、实现IDynamicMetaObjectProvider就意味着实现一个方法GetmetaObject。

3、创建动态类型时首先选择继承方式,如果必须使用其他基类,还可以手工实现IDynamicMetaObjectProvider接口,但所有的动态类型都会带来性能上的损失,而手工实现所带来的损失往往更大一些。

四十二、如何使用表达式API
1、传统的反射API可以用表达式和表达式树更好的替代,表达式可以直接编译为委托;
2、对于一些简单的问题,使用表达式的解决方法比较麻烦,但表达式API比以前的反射API简单了很多;
2、接口使我们可以得到一个更为清晰、也更具可维护性的系统,反射是一个很强大的晚期绑定机制,.NET框架使用它来实现Windows控件和Web控件的数据绑定。

四十三、使用表达式将延迟绑定转换为预先绑定
1、延迟绑定API要使用符号(symbol)信息来实现,而预先编译好的API则无需这些信息,表达式API正是二者之间的桥梁;
2、延迟绑定常见于Silverlight和WPF中使用的属性通知接口,通过实现INotifyPropertyChanged和INotifyPropertyChanging接口来实现属性变更的预绑定。

四十四、尽量减少在公有API中使用动态对象
1、在静态系统中,动态对象仍有一些“水土不服”,C#是一门静态语言,只在某些领域引入了动态特性;
2、在使用C#时,应该在大多数时候使用静态类型,并尽可能降低动态类型的作用范围,特别不要在公有接口中使用,而且应该将动态语言创建的对象封装到C#对象中;
3、应该尽可能地将动态类型隔绝到最小的范围内,使用原则是仅当某处确实需要使用动态特性时,保证动态对象绝不会离开方法的作用域。

第六部分:杂项
四十五、尽量减少装箱和拆箱
1、值类型是数据的容器,并不支持多态。

关注一个类型到System.Object的隐式转换,同时值类型不应该被替换为System.Object类型;
2、装箱和拆箱会在你无意中创建许多副本,导致许多难以发现的Bug;
3、使用接口而不是使用类型可以避免装箱,即将值类型从接口实现,然后通过接口调用成员。

四十六、为应用程序创建特定的异常类
1、需要不同的异常类的唯一原因是让用户在编写catch处理器时能够方便地对不同的错误采取不同的做法;
2、可能有不同的修复行为时,我们才应该创建多种不同的异常类,通过提供异常基类所支持的所有构造器,可以为应用程序创建功能完整的异常类,使用InnerException属性可以保存更低级别错误条件所产生的所有错误信息。

四十七、使用强异常安全保证
1、强异常保证在从异常中恢复和简化异常处理之间提供了最好的平衡,若一个操作因为异常而中断,程序的状态保持不变;
2、对将要修改的数据做防御性的复制,对这些数据的防御性复制进行修改,这中间的操作可能会引发异常,将临时的副本和原对象进行交换;
3、终结器、Dispose()方法和委托对象所绑定的目标方法在任何情况下都应当确保他们不会抛出异常。

四十八、尽量使用安全代码
1、尽可能的避免访问非托管内存,隔离存储不能防止来自托管代码和受信用户的访问;
2、程序集在Web上运行时可以考虑使用隔离存储,当某些算法确实需要更高的安全许可时,应该将那些代码隔离在一个单独的程序集中。

四十九、尽可能实现与CLS兼容的程序集。

相关文档
最新文档