首页 > 代码库 > Effective C# Chapter1-Language Elements

Effective C# Chapter1-Language Elements

《EffectiveC#》这本书讲了一些关于C#语言的使用技巧和经验. 该系列文章是备忘录和自己的一些见解.程序猿们最喜欢这类问题了,欢迎讨论~


菜单

Item 1 使用属性取代公共成员变量

Item 2 优先考虑readonly而不是const

Item 3 使用is/as取代转换操作符来进行对象类型转换

Item 4 使用ConditionalAttribute 取代 #if

Item 5 总是提供ToString()方法

Item 6 理解Value Types 和 Referance Types的差别

Item 7 Perfer Immutable Atomic Value Type

Item 8 确保0(对象默认值,default(T)) 是一个有效状态(已定义状态)

Item 9 理解比較关系的方法:ReferanceEquals(), static Equals(), instance Equals() 和 operator ==

Item 10 理解GetHashCode()里的陷阱

Item 11 优先使用foreach

Item 1 使用属性取代公共成员变量

这个是C++转C#程序猿最開始纠结的地方。


1、假设直接是public DataMember ,这肯定不正确,不管是C++还是C#,都须要进行封装。

特别的,在C#中,假设使用DataMember,会导致在跨Assembly使用时,假设该变量有所变化(比方默认值),全部用到该变量的Assembly都得重编译.
这在多DLL的项目里会变成一种灾难.

2、Porperty 和 Indexer 是C#里的概念和语言上的特性类型

那么C++的Access Methods 为什么也不要用了呢?由于Access Methods是C++里的东东; 对成员的訪问控制,使用Property会得到一些C#提供的编程便利,比方能够直接对一个Property应用某个[Attribute],而使用Access Methods则须要分别处理Get 和 Set, 实现起来更加繁琐;
很多其它的语言特性也是基于"Property"的,这个语言属性在反射相关的编码活动中能得到类型上的差别,而Access Methods是编码风格,C#不正确其在语言上直接给予支持。

3、性能没区别

JIT为Property实现的是inline property accessor,所以性能和DataMember等价.

总结:

总是使用属性来向外提供数据訪问的能力(ValueType可能会有特例,比方Vector3, Matrix44之类);
总是使用Indexer来向外提供数据的队列或定位的能力
数据成员所有是private的
(或protected,个人看法);
Property和Indexer都不得有异常抛出;


Item 2 优先考虑readonly而不是const

1、C#语言里有两种版本号的常量:编译期常量(const)和执行期常量(readonly)

const在编译成IL的时候,会直接将常量解释成字面值,而readonly则解释成某种引用

2、const效率最高,可是有潜在风险

两者都有价值,const无性能开销,是最高效的,由于直接使用字面值常量来生成IL代码。
关于潜在风险,考虑例如以下一种情况:
程序集的A版本号公布,里面有个const 值为4,因为是DataMember(參考Item1 的理由1),使用该程序集的client全部用到的代码都被编译成4了.
数周后,程序集更新到B版本号,const的值改为5了. 不论什么没又一次编译的相关程序集里该const使用处的地方的值还是4.(被坑过的同学请举手)

3、readonly有一定程度的灵活性,当然也有少量的性能开销

readonly在编译期和const在语法特性上一致,在执行期能够避免const带来的潜在风险--全部使用处都是记着指定程序集的指定变量,而不是直接写成字面值.
自然,这样的引用会带来性能开销,只是也就是个inline的开销罢了.

个人觉得:readonly于const, 就像 property 于 data member
总结:
仅仅有特别强调性能的场合才使用const,其它不论什么场合使用readonly
使用const的常量必须确保在程序不断更新版本号时也不会发生改变,否则使用readonly


Item3 使用is/as取代转换操作符来进行对象类型转换

这个这个...就和C++里使用 xxxcast取代强制转换的条款类似。只是C#里强制转换失败时会抛出异常,而不像C++那样悄无声息。
我们知道面向对象的语言就不该直接转,所以该条款没啥可商议的。

总结:
总是使用as/is进行类型转换


Item 4 使用ConditionalAttribute 取代 #if

嗯,这是个C++程序猿没见过的新奇玩意
[Conditional("DEBUG")]
void fun()
{
        // do something in debug model
}

1、这个函数能够在不论什么地方调用,在编译Release的时候,这个函数和其调用代码就像根本不存在一样。
这个语法特性从某种程度上降低了我们编写须要条件编译的代码的工作量。
2、除了DEBUG,TRACE这样的已经内置到IDE里的条件编译变量,它支持随意条件编译变量
3、能够 [Conditional(A),Conditional(B)] ,等于 #if A || B, 假设要表达A and B, 须要预先定义
#if A && B
#define C
#endif
4、仅仅支持函数

总结:
看着用吧,事实上无法全然取代#if 。


Item 5 总是提供ToString()方法

总结:
不用列理由了,亲们,你们dump对象内容的代码写的还少嘛?在C#的世界里,每一个类都应该有ToString,让我们一起努力让这样的美好持续下去吧!
一些须要支持特定format的类,须要实现IFormattable接口。


Item 6 理解Value Types 和 Referance Types的差别

这是一个能够长篇大论的条款,有太多的文章解说两者的差别。在这里我列一些相关的知识点吧
1、值传递和引用传递
2、C#没有常引用,常量函数
3、装箱(boxing) 和 拆箱(unboxing)
4、ValueTypes不支持实现继承,但支持接口继承,通过接口使用ValueType会导致boxing操作.
5、非常多C#默认实现都是基于object的,这些实现会导致ValueType的boxing操作.

Item 7 Perfer Immutable Atomic Value Type

1、Immutable特性
不可变特性的原子级ValueType。嗯,老实说我不觉得ValueType须要是Immutable的,可是Immutable的特性还是非常实用的。
strcut A
{
 public int x{get;set;}
}
这个ValueType就不是 immutable 的, 由于当一个对象持有该类实例时,比方 G.a = new A();
我们能够通过 G.a.x = 3 来改变 a 的内部状态。
为了使 A 不可变,须要去掉全部的 set方法. 这个类就是Immutable类了--可是一个无法改变内部状态的类有啥用?稍后会说明。

2、Atomic特性
再考虑这样一种情况:
strcut Address
{
	public int ZipCode{get;set;}
	public string CityName{get;set;}
}


这是一个Address类,可是它有个问题,就是我们从开放的接口层面同意使用者用错:仅仅更改ZipCode而不更改CityName,这会导致邮编和城市对不上!
这个类就不是Atomic的。

为了维护对象内部数据的统一性,须要给出一些特定的原子级訪问接口,比方 Address 能够提供一个特殊的改动函数来维持其Atomic特性。
struct Address
{
	private int _zipCode;
	private string _cityName;

	public int ZipCode
	{
		get { return _zipCode; }
	}
	public string CityName
	{
		get { return _cityName; }
	}

	public Address(int zipCode, string cityName)
	{
		_zipCode = zipCode;
		_cityName = cityName;
	}
	public void Modify(int zipCode, string cityName)
	{
		_zipCode = zipCode;
		_cityName = cityName;
	}
}


尽管我们不会在使用Address时出错了,但这个新的Atomic Address还不是immutable的,由于我们能够改动其内部状态。那么我们如今将它改成Immutable的:
	public Address Modify(int zipCode, string cityName)
	{
		return new Address(zipCode, cityName);
	}

能够看到,我们仅仅改了一个函数,Modify不再更改对象内部的属性,而是创建了一个新的Address实例返回出去了。那么这个类就是具有 Immutable和Atomic属性的ValueType。

在理解了Immutable 和 Atomic 的概念之后,我们能够做一些扩展讨论了:
1、Immutable Or Atomic 不一定非要是Value Type
string 是一个 Immutable Referance Type 所以用起来和built-in的ValueType 一样,就像一个int。而Atomic就更无所谓了。
2、类假设持有ReferanceType 的DataMember,要维护其Immutable属性,将很困难(还是能够做到),主要是由于其引用传递的特性所导致。
这也是为什么推荐使用ValueType来实现Immutable特性的数据结构:Immutable对象不会改变数据内部状态,而ValueType在赋值时是值传递,不会导致owner的内部状态发生变化。
3、Immutable属性用来做HashCode之类的事情很合适.
其实GetHashCode默认使用Object的第一个DataMember的GetHashCode来作为整个对象的hash值。

Item 8 确保0(对象默认值,default(T)) 是一个有效状态(已定义状态)

C#和C++在对象初始化的时候有些不同:不管是Debug还是Release,C#总是将各种变量初始化为0---valueType就是0, refType就是null.
总结:
1、代码里应该处理好这样的事情
2、enum一定要包括0作为一个有效值--即使它是无意义的值,也代表了我们考虑过一个enum对象被默认的初始化为0时的情况。

Item 9 理解比較关系的方法:ReferanceEquals(), static Equals(), instance Equals() 和 operator ==

public static bool ReferanceEquals(object left,object right)
public static bool Equals(object left,object right)
public virtual bool Equals(object right)
public static bool operator==(T left, T right)

so many 比較函数...刚从C++过来的人预计一下子就晕了吧。以下的4条分别解释上述4种比較函数的特点和差别。
1、ReferanceQuals 用来比較两个引用是否相等,程序猿从来不会自己改动这个函数
在C#里,有具名的handler 和 不具名的 object 这样的概念,就等同于变量 和 变量的值一样。 这个函数就是检查 handler 指向的引用是否一致。
这个函数无法作用于ValueType或EnumType,由于仅仅是检查引用,对于值类型,调用此函数会导致boxing,所以包装出来的是2个object了,肯定不同,enum也是一样。
2、static bool Equals 是默认实现,默认调用left.Equals(right) ,程序猿从来不会自己改动这个函数
这是一种默认机制,在比較两者是否相等时,能够通过多态+System.Object是全部类型的基类来达到检查随意两个object是否相等的目的。參考事实上现
public static bool Equals(object left,object right)
{
	if(left == right)
		return true;
	if(left==null || right == null)
		return false;
	return left.Equals(right);
}

能够看到,这个函数先调用==,再调用instance.Equals ,很的完备,是吧?这里是有陷阱的。
这个函数我们无法重载,而它是使用object作为參数类型的,这就意味着,使用Enum或者ValueType时,会有boxing操作,这会带来性能开销
3、对于ValueType,这个函数总是要重载,对于RefType,除非要更改其默认行为,否则不重载,RefType的默认行为是仅仅比較引用。
假设不ValueType的Equals,工作的也对,可是因为其boxing操作,为了让随意的valueType的随意DataMember能正确的得到比較,C#使用了反射,这比boxing要慢1000000倍!重载的意义在于排除不必要的反射操作:能够直接推断下right的类型是不是我们要比較的。
而对于RefType,除非我们要定制其比較规则,否则都能够依赖C#自己的默认实现,这里没有boxing,也没有reflection. RefType仅仅推断是否引用相等,不推断内容是否相等
4、operator==(T,T) 这个是为消除boxing和cast准备的,绝大多数情况下,仅仅有ValueType才会明白重载该操作符。RefType应该总是使用Equals来进行比較。
RefType的内容比較,最后都须要到ValueType上进行比較。我们能够调用ValueType的Instance.Equals(有性能开销,最少也有boxing),也能够调用其 operator==() , 这个是能够直接给出特定类型的比較的,直接排除了各种boxing 和 reflection的可能。
5、RefType是否重载3和4,须要看详细场合是否关心ref的值相等问题,并且大部分情况下,仅仅须要实现instance.Equals,4仅仅在有语法糖需求的场合实现。

Item 10 理解GetHashCode()里的陷阱

这个条目在理解Immutable的概念后,就非常easy理解了:假设作为hash的dataMember都能够变来变去,那么就得不到一个稳定的结果了。
再一个就是,对于一个对象,假设使用非readonly的数据成员作为其hash的计算基础,在计算完毕、对象被储存到某个基于hash的容器内,他的hash成员变量又发生变化了的话,他就再也无法通过hash值在容器中被检索出来了。C#不禁止你这么做,所以风险也须要自己去承担。

总结:
1、ValueType 的 GetHashCode()假设要工作正常,必须让自己的第一个数据成员是readonly的immutable类型对象,否则结果【可能】不对--假设你能冒这个险。
2、假设须要自己写HashCode的返回值,大部分情况下,使用全部immutable成员的GetHashCode()的值进行xor,该结果值作为本类型的hash值。

Item 11 优先使用foreach

1、编译器会对foreach自己主动做出最优的代码翻译
比方 Array类型的数据结构,foreach 等价于 for(int i = 0 ....)
而对于关联容器,则使用 using( var e = container.GetEnumartor()){ ...} 这样的结构。
用户无需关心实现细节。
2、用中间变量记着count的结构也无法和foreach比性能
由于foreach在IL层面做过优化了,比包括中间计算的手工代码要更快。

总结:用foreach来实现循环吧!
(个人经验,因为Unity3D使用的Mono.dll的BUG,必须手工实现 while(e.MoveNext() ){...} 的结构,避免无意义的GC Alloc)