《Effective C#》笔记 C#语言习惯

阅读《Effective C#》一书的总结,第一部分,C#语言习惯。

C# Language Idioms

01 使用属性Property而不是直接访问成员数据

使用Property可支持Data binding;

使用Property更容易应对需求改变引起的实现的变化;

Property内部是基于Method,所以可以很容易支持多线程(set、get时加锁):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class Customer
{
private object syncHandle = new object();
private string name;
public string Name
{
get
{
lock (syncHandle)
return name;
}
set
{
if (string.IsNullOrEmpty(value))
throw new ArgumentException("Name cannot be blank","Name");
lock (syncHandle)
name = value;
}
}

// More Elided.
}

Property可以定义为抽象的,也可以在接口中定义;

Property可是实现超出成员变量以外的功能,如:

1
2
3
4
5
6
7
8
9
10
11
12
// 直接索引
public int this[int index]
{
get { return theValues[index]; }
set { theValues[index] = value; }
}

// 类似于多维数组
public int this[int x, int y]
{
get { return ComputeValue(x, y); }
}

02 使用readonly而不是const

readonly是运行时常量,const是编译时常量;

编译时常量速度更快,但务必保证不同版本的发布中其值是不变的。运行时常量速度稍慢,但是更灵活;

const仅适用于数值和字符串,readonly可用于带有构造函数的类型;

03 使用is或as操作符而不是类型转换

使用as,更安全,运行时更高效;

asis不会执行任何用户定义的类型转换操作符,即只有运行时类型匹配才会成功;

as更简洁,更易读,且避免了使用try ... catch ...

as无法处理值类型,可以先用is判断再cast

04 使用Conditional属性而不是#if

使用#if/#endif使代码难以阅读和调试;

Conditional属性仅适用于方法,且方法返回类型为void

Conditional属性不会影响其修饰的函数生成的代码,修改的是对函数的调用;

当函数有多个Conditional属性时,它们是“或”的关系;

05 总是提供ToString()

可以便捷地向使用者展示对象的信息;

对于更复杂的对象,实现IFormattable.ToString()

06 理解多种“相等”概念的关系

C#中当定义了类或者结构时,需要为这个类型定义何谓“相等”,C#中有下边四种相等的概念:

1
2
3
4
5
6
7
8
9
10
11
// 方法1
public static bool ReferenceEquals(object left, object right);

// 方法2
public static bool Equals(object left, object right);

// 方法3
public virtual bool Equals(object right);

// 方法4
public static bool operator ==(MyClass left, MyClass right);

通常前二者不应当重定义;

方法1ReferenceEquals用于比较二者是否是同一引用,如果传入参数为值类型,则会自动装箱;

以下代码总是输出”Always happens.”:

1
2
3
4
5
6
7
8
9
10
11
12
int i = 5; 
int j = 5;

if (Object.ReferenceEquals(i, j))
Console.WriteLine("Never happens.");
else
Console.WriteLine("Always happens.");

if (Object.ReferenceEquals(i, i))
Console.WriteLine("Never happens.");
else
Console.WriteLine("Always happens.");

方法2Object.Equals()内部会用到方法1和方法3,其实现类似于下边的逻辑:

1
2
3
4
5
6
7
8
9
10
11
public static new bool Equals(object left, object right) 
{
// Check object identity
if (Object.ReferenceEquals(left, right) )
return true;

// both null references handled above
if (Object.ReferenceEquals(left, null) || Object.ReferenceEquals(right, null))
return false;

return left.Equals(right);

对于方法3,实例方法Equals(),需要实现IEquatable<T>来覆写;

值类型(结构体),应该实现Equals()方法。否则在比较时会调用默认的比较方法(基于反射的逐成员对比,很低效);

引用类型在调用Equals()时默认会比较引用,如希望通过比较内容来判定,则覆写Equals()

值类型应当覆写operator ==,以避免低效的反射及不必要的装箱拆箱;

引用类型不要覆写operator ==,因为需要保持其比较引用的原始含义;

IStructuralEquality适用于System.ArrayTuple<>,使其使用值类型语义;

07 理解GetHashCode()的缺陷

应避免使用,仅用在基于hash的集合中为键定义hash值,如HashSet或Dictionary等;

对于引用类型,可以取到正确的结果但是很低效,对于值类型数据取到的是不正确的结果;

覆写GetHashCode()务必满足三个条件:

  • 相等的对象(operator==)得到相等的hash值;

  • 对于一个实例,hash值应保持固定不变;

  • 函数得到的hash值应当在所有的整数中随机分布;

08 使用查询语法而不是循环

使用查询语法使代码逻辑由可读性差的命令式(imperative)变成易于理解的陈述式(declarative);

查询语法和循环,二者的效率孰高孰低需要根据不同的情形而定;

一些使用查询语法的例子:

1
2
3
4
int[] foo = (from n in Enumerable.Range(0, 100) 
select n * n).ToArray();

foo.ForAll((n) => Console.WriteLine(n.ToString()));
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
33
private static IEnumerable<Tuple<int, int>> QueryIndices() 
{
return from x in Enumerable.Range(0, 100)
from y in Enumerable.Range(0, 100)
select Tuple.Create(x, y);
}

private static IEnumerable<Tuple<int, int>> QueryIndices2()
{
return from x in Enumerable.Range(0, 100)
from y in Enumerable.Range(0, 100)
where x + y < 100
select Tuple.Create(x, y);
}

private static IEnumerable<Tuple<int, int>> QueryIndices3()
{
return from x in Enumerable.Range(0, 100)
from y in Enumerable.Range(0, 100)
where x + y < 100
orderby (x*x + y*y) descending
select Tuple.Create(x, y);
}

private static IEnumerable<Tuple<int, int>> MethodIndices3()
{
return Enumerable.Range(0, 100).
SelectMany(x => Enumerable.Range(0,100),
(x,y) => Tuple.Create(x,y)).
Where(pt => pt.Item1 + pt.Item2 < 100).
OrderByDescending(pt =>
pt.Item1* pt.Item1 + pt.Item2 * pt.Item2);
}

09 在API中避免类型转换操作

使用类型转换操作可以实现不同的类的可替换性(substitutability),例如:

1
2
3
4
static public implicit operator Ellipse(Circle c) 
{
return new Ellipse(c.center, c.center, c.radius, c.radius);
}

注意这样的代码会有很大的隐患,当出现以下的场景时:

1
2
3
4
5
6
7
8
9
public static void Flatten(Ellipse e) 
{
e.R1 /= 2;
e.R2 *= 2;
}

// call it using a circle:
Circle c = new Circle(new PointF(3.0f, 0), 5.0f);
Flatten(c);

Flatten()会作用于一个临时对象,然后该对象会变成没有引用的垃圾。函数的调用并不会产生实际的效果。正确的方式是使用构造函数来代替类型转换:

1
2
3
4
5
6
7
8
Circle c = new Circle(new PointF(3.0f, 0), 5.0f);

// Work with the circle.
// ...

// Convert to an ellipse.
Ellipse e = new Ellipse(c);
Flatten(e);

10 使用可选参数来减少方法重载

使用具名参数和可选参数,会使代码逻辑更清晰;

具名参数的一个例子:

1
2
3
4
5
6
7
8
private void SetName(string lastName, string firstName) 

{
// elided

}

SetName(lastName: "Wagner", firstName: "Bill");

在维护程序的过程中,对于public或protected接口,尽量避免修改参数及参数名,而是提供重载方法。参数名也是接口的一部分;

11 理解小函数的魅力

C#代码到可执行的机器码需要两步,首先C#编译器将代码生成IL代码存在程序集中,JIT按需将各方法生成机器码;

小函数可以让JIT编译器更方便地按需编译,而不必一次性编译大量的不会立即使用的代码;

小函数跟利于JIT编译器的寄存器化(enregistration),即将一些局部变量放在寄存器而不是栈上。局部变量的数量越少越好;

小函数更容易被内联;

REFERENCE

《Effective C#: 50 Specific Ways to Improve Your C# (2nd Edition)》