《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),使用该程序集的客户端所有用到的代码都被编译成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的概念后,就很容易理解了:如果作为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)