Effective C# 读书笔记

PS:本文只是读书笔记,详细资料建议看书

C# 语言的编程习惯

第 1 条:优先使用隐式类型的局部变量

  • 局部变量可用 var 来声明,让编译器自动选择合适的类型

  • 不推荐用 var 声明数值类型的变量(精度转换问题)

第 2 条:考虑用 readonly 代替 const

  • readonly:声明运行期常量

  • const:声明编译期常量;只有当程序性能极端重要且常量取值不会随版本而变化的情况下,才可以考虑使用

第 3 条:优先考虑 is 或 as 运算符,尽量少用强制类型转换

第 4 条:用内插字符串取代 string.Format()

个人觉得使用时要注意代码简洁

第 5 条:用 FormattableString 取代专门为特定区域而写的字符串

第 6 条:不要用表示符号名称的硬字符串来调用 API

  • nameof() 方法

第 7 条:用委托表示回调

  • 注意:所有的委托都是多播委托(multicast delegate)
    • 多播委托调用目标函数时不捕获异常,因此只要有一个目标函数抛出异常,调用链就会中断
    • 多播委托的执行结果是最后调用的目标函数的返回值,因此当返回值不是 Void 时,可用 .GetInvocationList() 迭代返回值

第 8 条:用 null 条件运算符调用事件处理程序

  • 采用 null 条件运算符(?.)能线程安全地调用事件处理程序
1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 声明 Updated 为委托或事件,可为空

// 可能空引用异常
Updated();

// 非线程安全
if(Updated != null) Updated();

// 线程安全,但难理解
var handler = Updated;
if(handler != null) handler();

// 线程安全
Updated?.Invoke();

第 9 条:尽量避免装箱与取消装箱

  • 尽量不要在需要使用 System.Object 的地方直接使用值类型的值

  • 可使用 .ToString() 明确把值类型转换成字符串

第 10 条:只有在新版基类与现有子类之间的冲突时才应该使用 new 修饰符

  • new 修饰符可以重新定义从基类继承下来的非虚成员,但要慎用(不推荐用)

  • 使用 new 修饰符是为了解决新版基类和现有子类之间的冲突

.NET 的资源管理

第 11 条:理解并善用 .NET 的资源管理机制

  • 理解 C# GC

第 12 条:声明字段时,尽量直接为其设定初始值

  • 声明成员变量并直接把它的初始值写出来

  • 不应该编写初始化语句的情况:

    • 把对象初始化为 0 或 null
    • 不同的构造函数需要根据各自的方式来设定某个字段的初始值

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      public class MyClass2
      {
      private List<string> labels = new List<string>();

      MyClass2()
      {

      }

      MyClass2(int size)
      {
      labels = new List<string>(size);
      }
      }
    • 初始化变量的过程中有可能出现异常

第 13 条:用适当的方式初始化类中的静态成员

  • 静态构造函数

第 14 条:尽量删减重复的初始化逻辑

第 15 条:不要创建无谓的对象

  • 频繁使用的对象的复用

第 16 条:绝对不要在构造函数里面调用虚函数

在(基类的)构造函数里面调用虚函数会令代码严重依赖于派生类的实现细节,而这些细节是无法控制的

第 17 条:实现标准的 dispose 模式

合理地运用泛型

封闭式泛型类型 VS 开放式泛型类型

类型参数是引用类型 VS 类型参数是值类型

第 18 条:只定义刚好够用的约束条件

第 19 条:通过运行期类型检查实现特定的泛型算法

第 20 条:通过 IComparable 及 IComparer 定义顺序关系

第 21 条:创建泛型类时,总是应该给实现了 IDisposable 的类型参数提供支持

1
2
3
4
5
T driver = new T();
using (driver as IDisposable)
{
driver.DoWork();
}

第 22 条:考虑支持泛型协变与逆变

关键字:in VS out

第 23 条:用委托要求类型参数必须提供某种方法

第 24 条:如果有泛型方法,就不要再创建针对基类或接口的重载版本

第 25 条:如果不需要把类型参数所表示的对象设为实例字段,那么应该优先考虑创建泛型方法,而不是泛型类

如果某个类型拥有类型级别的数据成员,那么应该实现成泛型类(尤其是当成员的类型与泛型的类型有关时更应该这样做),反之,则应该实现成泛型方法。

如:静态工具类

第 26 条:实现泛型接口的同时,还应该实现非泛型接口

适用于三项内容:

  • 要编写的类以及这些类所支持的接口
  • public 属性
  • 打算序列化的那些元素

第 27 条:只把必备的契约定义在接口中,把其他功能留给扩展方法去实现

第 28 条:考虑通过扩展方法增强已构造类型的功能

合理地运用 LINQ

第 29 条:优先考虑提供迭代器方法,而不要返回集合

第 30 条:优先考虑通过查询语句来编写代码,而不要使用循环语句

声明式模型 VS 命令式模型

与采用循环语句所编写的命令式结构相比,查询语句(也包括实现了查询表达式模式的查询方法)能够更为清晰地表达开发者的想法

第 31 条:把针对序列的 API 设计得更加易于拼接

第 32 条:将迭代逻辑与操作、谓词及函数解耦

第 33 条:等真正用到序列中的元素时再去生成

第 34 条:考虑通过函数参数来放松耦合关系

第 35 条:绝对不要重载扩展方法

第 36 条:理解查询表达式与方法调用之间的映射关系

第 37 条:尽量采用惰性求值的方式来查询,而不要及早求值

第 38 条:考虑用 lambda 表达式来替代方法

第 39 条:不要在 Func 与 Action 中抛出异常

第 40 条:掌握尽早执行与延迟执行之间的区别

第 41 条:不要把开销大的资源捕获到闭包中

第 42 条:注意 IEnumerable 与 IQueryable 形式的数据源之间的区别

AsQueryable()

第 43 条:用 Single() 及 First() 来明确地验证你对查询结果所做的假设

第 44 条:不要修改绑定变量

合理地运用异常

第 45 条:考虑在方法约定遭到违背时抛出异常

第 46 条:利用 using 与 try/finally 来理清资源

第 47 条:专门针对应用程序创建异常

第 48 条:优先考虑做出强异常保证

第 49 条:考虑用异常筛选器来改写先捕获异常再重新抛出的逻辑

第 50 条:合理利用异常筛选器的副作用来实现某些效果