阅读笔记_《effective-csharp》

记录一下 《effective-csharp》 这本书里觉得比较有用的东东
gitbook:https://wizardforcel.gitbooks.io/effective-csharp/content/


索引器(indexers )

  • 类似数组下标的方式 在类中使用

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    class Abs {
    public Dictionary<string, string> _map = new Dictionary<string, string>();
    public string this[string key] {
    get {
    return _map.ContainsKey(key) ? _map[key] : null;
    }

    set {
    _map.Add(key, value);
    }
    }
    }

    Abs a = new Abs();
    a["aaa"] = "sdf";

属性访问器(getter setter)

  • 在写c++的时访问属性一般都会提供个 getter setter 方法,方法体不大的时候编译器会做 inline 优化,在csharp中有这样的 语法糖,它提供了 inline 优化

    1
    2
    3
    4
    5
    6
    7
    class AAA {
    int mA = 0;
    public int A {
    get { return mA; }
    set { mA = value; }
    }
    }
  • 如果不想被 inline 优化,可以使用 System.Runtime.CompilerServices.MethodImpl 特性, 指定方法不被内联

    1
    2
    [MethodImpl(MethodImplOptions.NoInlining)]
    public int A(){}
  • 不会被 inline 优化的情况:虚函数 或者 包含 try/catch 的函数


偏爱 readonly 而不是 const

  • C# 有两种常量:编译时常量 const 和 运行时常量 readonly。

  • 区别:

    1. 编译时常量 会比 编译时常量 稍微快点, 但更不灵活
    2. 编译时常量 还可以在方法体中声明。运行时常量 不能在方法体重声明
    3. 编译时常量 和 运行时常量 访问方式不同导致不同的行为。 在目标代码中 编译时常量 会被替换成 常量值;运行时常量 的值是在运行时得到的。 当你引用一个只读(read-only) 常量, IL 会引用一个readonly 变量而不是直接使用值
    4. 编译时常量只能在基本类型(内建整数和浮点数类型) , 枚举类型, 或字符串。 编译时常量要求类能用有意义的常量赋值初始化。 而只有基本类型才能在 IL 代码中使用常量(literal values) 来替换。 不能使用使用 new 操作法初始化编译时常量 ,即使它是一个值类型 。
    5. const编译时 编译到程序集,如果别的程序集使用了这个变量,这个变量将不会有任何改变;而readonly是运行时get到值,所以readonly跟灵活
  • 示例:

    1
    2
    3
    4
    // Compile time constant:
    public const int Millennium = 2000;
    // Runtime constant:
    public static readonly int ThisYear = 2004;

选择 is 或 as 而不是强制类型转换

  • 使用as做类型转换,而不是强制装换(XXX)obj,如果obj没有继承关系,as编译时会报错,而(XXX)obj不会。

  • 不能使用as的情况:参数为 值类型,此时可以用is来判断。(is只用于as无法使用的情况,否则多余,如果知道参数是引用类型就用 as)

  • 示例

    1
    2
    3
    4
    5
    object o = Factory.GetObject();
    MyType t = o as MyType;
    if (t != null) {
    //Do somethind about t.
    }

使用条件特性(conditional attribute) 代替 #if/#endif

  • 假如调试代码是 CheckState 函数里的

    1
    2
    3
    4
    5
    [Conditional("DEBUG")]
    private void CheckState()
    {
    // same code as above
    }
  • 如果 DEBUG 变量被定义, 你的代码编译出来是这样的

    1
    2
    3
    4
    5
    6
    public string LastName {
    get {
    CheckState();
    return lastName;
    }
    }
  • 如果 DEBUG 变量 没有 定义, 你的代码编译出来是这样的

    1
    2
    3
    4
    5
    public string LastName {
    get {
    return lastName;
    }
    }
  • 如果使用 #if/#endif 块包住,也会产生一次 CheckState 函数的调用,即使 DEBUG 没有定义

    1
    2
    3
    4
    5
    6
    private void CheckState()
    {
    #if
    // same code as above
    #endif
    }
  • Conditional 只能使用在 void 返回值的函数中,否则编译错误。

  • 这种是 或 的表达式,如果要表达 与,拆分成两个方法,各用一个红

    1
    2
    3
    4
    5
    6
    7
    8
    9
    [Conditional("DEBUG"), Conditional("DEBUG222")]
    private void CheckState() {

    }

    //等价于
    #if DEBUG || DEBUG222

    #endif

使用恰当的方式对静态成员进行初始化

  • 静态初始化语法 会比 静态构造函数 更早执行,静态初始化语法 和 静态构造函数 是最干净, 最清晰的方式去初始化静态成员变量
  • 实例第一次构造对象时的顺序

    1. static 变量默认存储为0。
    2. static 变量初始化执行。
    3. 基类静态构造函数执行。
    4. 静态构造函数执行。
    5. 实例成员变量默认存储为0。
  1. 实例成员变量初始化执行。
  2. 恰当的基类实例构造函数执行。
  3. 实例构造函数执行。
  • 后续同一类型的对象初始化从第5步开始因为类初始化只会执行一次。 而且, 步骤6和7会被优化构造函数初始化语法会引起编译器移除重复的指令。

使用构造函数链

  • 构造函数时 调用其他构造函数,减少各个构造函数中重复的初始化代码。类似重载函数的概念

    1
    2
    3
    4
    5
    6
    7
    8
    9
    public class MyClass {// collection of data
    private List<Circle> coll;
    private string name;
    public MyClass() : this(0, string.Empty) { }
    public MyClass(int initialCount = 0, string name = "") {
    coll = (initialCount > 0) ? new List<Circle>(initialCount) : new List<Circle>();
    this.name = name;
    }
    }

using 语法糖

  • using 语法糖会产生 try/finally 块

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    SqlConnection myConnection = null;
    // Example Using clause:
    using (myConnection = new SqlConnection(connString))
    {
    myConnection.Open();
    }

    // 等价于
    // example Try / Catch block:
    try
    {
    myConnection = new SqlConnection(connString);
    myConnection.Open();
    } finally {
    myConnection.Dispose();
    }
  • 快速保护方法 是使用 as 语句可以转换为安全可回收对象不管是否实现 IDisposable 接口

    • using 语句对在编译时期类型实现 IDisposable 接口才能正常工作

      1
      2
      3
      4
      5
      // Does not compile. 编译报错
      // Object does not support IDisposable.
      using (object obj = Factory.CreateResource()) {
      Console.WriteLine(obj.ToString());
      }
    • 快速保护方法

      1
      2
      3
      4
      5
      6
      // The correct fix.
      // Object may or may not support IDisposable.
      object obj = Factory.CreateResource();
      using (obj as IDisposable) { //如果 obj 没有实现 IDisposable 接口,则这行代码 等价于 using(null) {
      Console.WriteLine(obj.ToString());
      }

字符串拼接

  • 使用 sting.Format 或者 StringBuilder 替代 string 的 += 拼接操作
  • StringBuilder 类似 Vector 一样是动态扩容。

限制你的类型的可见性

  • 尽可能的减少类的可见性,例如某些类里需要一些数据结构类,应该设为 private 或 这 internal ,不让外部知道

让接口支持协变和逆变

  • 让接口支持 逆变、协变,限制 泛型只能是参数 或者 只能是返回值,例如Action 和 Func 这两个委托,这个有点抽象

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    public interface IMyList2<out T> {
    T GetElement();
    //void ChangeT(T t); // 这里编译错误,因为 T 被 out 修饰,所以 T 只能做返回值,不能做参数
    }

    public class MyList2<T> : IMyList2<T> {
    public T GetElement() {
    return default(T);
    }

    public void ChangeT(T t) {
    //Change T
    }
    }

    //-------------------------------
    public interface IMyList<in T> {
    //T GetElement(); // 这里编译错误,因为 T 被 in 修饰,所以 T 只能做参数,不能做返回值
    void ChangeT(T t);

    }

    //没有指定 in 或者 out,就可以即做 参数 又做 返回值
    public class MyList<T> : IMyList<T> {
    public T GetElement() {
    return default(T);
    }

    public void ChangeT(T t) {
    //Change T
    }
    }

上面这些是个人感觉比较能用到的东西