Effective C#笔记(1)

本文探讨了C#编程中的多个关键问题,包括Property与Variable的使用选择、ReadOnly与Const的区别、Equals方法的不同形式及实现细节、GetHashCode的重要性和实现方法等。此外,还深入分析了foreach语句的特点及其在不同场景下的效率表现。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

(1)  使用Property的效率问题

其实使用Property的效率并不会很差,C#编译器会把一些Property编译成inline的方式,这样和Variable的效率是一样的。即使没有被编译成inline,其效率也只是比Variable差一点,并且没有到足于需要我们考虑的时候

(2) 先使用Variable,必要的时候再转成Property

这样会引起Binary Compatible问题,就是DLL兼容性问题了。因为Variable和Property编译生成的IL是不同的,这样如果以后更新的时候,必须把所有关连的DLL都编译一遍,否则会造成DLL之间的不兼容问题。

(3) 为什么建议使用ReadOnly而不是Const

因为Const在编译的时候就已经替换到变量中去了,如果以后某个DLL中的Const更新,而没有重新编译使用到这个Const的DLL,就会造成不一致性(看来是需要很重视DLL之间的兼容性问题)
比如,DLLA中定义了Const ConstValue = 100,DLLB中使用了这个ConstValue值。在DLL更新过程中,把DLLA里面的ConstValue值更新为200,而这时候如果没有重新编译DLLB,则DLLB使用的ConstValue值仍然是没有更新的100,而不是200。

(4) C#编译器根据编译时的类型而不是运行时的类型来生成代码

其实这个很明显,编译器当然不可能根据运行时来生成代码了:) 但遇到一些需要应用这个逻辑的时候,却经常想不起来。
比如Class A中定义了MyType这种Operator
public static implicit operator MyType(A t)
在使用过程中,我们可能会用到
Object obj = (create a new object typeof A)
MyType t = (MyType)obj;
我们会认为以上的Cast执行的是A自定义的MyType方法,其实编译器在编译的时候只知道obj的类型是Object,而不知道其运行时的类型是A,因此在编译的时候就已经生成了这条语句的执行IL,就是使用Object的Cast方法,而不是A的MyType方法。

(5) foreach会产生CastException

其实也很容易,因为foreach要对Value Type和Class Type都有效,而as对于Value Type是不起作用的,所以foreach会生成类似以下的语句
IEnumerator it = collection.GetEnumerator();
While (it.MoveNext())
{
       CustomType t = (CustomType)it.Current;
       ....
}
因为有Cast,所以就有可能产生CastException.
那至于为什么Value Type不能使用as操作符呢?试想如果int a = o as int; 如果o不是int,那就应该返回null值,但int并没有null值,所以也就不能使用as操作符了。

(6) 使用了as,就不需要使用is了

因为如果使用了as的时候还使用is就会写出多余的代码
比如
Object o = (Create New Object A);
A a = null;
if (o is A)
{a = o as A;}
完全可以用A a = o as A来代替。

(7) ToString方法

如果对一个类实现了IFormattable.ToString(format, formatProvider)接口,则必须保证"G",""和null format能够返回Object.ToString()一样的结果。因为.NET FCL使用IFormattable.ToString如果一个类实现了这个接口,而不是使用Object.ToString(),FLC经常调用IFormattable.ToString,传入null参数,而一些地方则使用"G"去表示返回一般的格式。如果这些结果不一致,会破坏一些转换的规则。

另外,对于用户定义的IFormatProvider和ICustomFormatter,调用的顺序如下(比如语句Console.WriteLine(string.Format(new CustomFormatterProvider(), "{0}", value)):
a. 某个ICustomFormatter实例(注:为什么说某个,是因为我不知道,应该是FCL里面的某个实例)会调用CustomFormatterProvider.GetFormat取得ICustomFormatter实例,也就是说CustomFormatterProvider.GetFormat的实现里面必须判断传入的Type是不是为ICustomFormatter
public object GetFormat(Type formatType)
{
     if (formatType == typeof(ICustomFromatter) {return ...}
}
b.如果以上的方法返回null的ICustomFormatter实例,则如果类实现了IFormattable接口,则会调用IFormattable.ToString(format, formatProvider)方法
c. 否则如果类没有实现IFormattable接口,则会调用value类本身的Object.ToString()方法。

(8) 值类型和引用类型

内存分配上的差别:值类型在声明的时候已经分配好内存了,并且值类型是在栈上分配内存的。引用类型在声明的时候并没有分配好内存,是分配在堆上的
比如:MyType [] var = new MyType[101],如果MyType是值类型,则会在栈上分配100个连续的空间来存储100个MyType值;而如果MyType是引用类型,则会首先在栈上分配100个连续的空间来存储100个"指针",这些"指针"指向的值是未定的(相当于null值),当使用MyType[i] = new MyType()的时候,才在堆上分配一个空间给MyType值,并把第i个指针指向新申请的空间。

至于为什么值类型分配在栈上,而引用类型分配在堆上?我个人认为因为值类型的有效范围肯定在声明之后才使用,直至栈上要删除这个值的时候,该值类型的作用域已经完成了;而引用类型因为要在程序里面引用多次,比如在一个函数里面声明的变量可能会通过返回值给其它函数使用,其空间不能随着函数作用域的结束而释放,所以需要存储在堆上。

值类型在空间使用效率上比引用类型高:连续分配;在栈上分配减少垃圾;调用的时候可以直接找到,而不需要通过"指针"来转向。引用类型能够支持多态,能够支持子类,代表了对象的行为,这些是值类型所没有的。

(9) 不可变的原子值类型

不可变的原子值类型能够很好地支持多线程,支持基于Hash的集合。定义不可变的原子值类型可以考虑以下几点:(a)提供不同的构造函数,使得更容易使用;(b)使用工厂类的方法创建值类型,比如Color就提供了Color.FromKnownColor和Color.FormName来创建Color类;(c)提供可变的类,比如StringBuilder是String的可变类型,通过可变的类来创建好值后再转换成不可变的原子类。

(10) 对于值类型,0值要有意义

值类型的0值要有意义。其实我觉得这一条主要是针对Enum类型来说的,我们经常会设计如下的Enum类型 enum MyEnum { A=1, B=2, C=3},但对于有些用户,可能会通过MyEnum var = new MyEnum() 来声明Enum值,这时候就使得var处于无效和状态下。

(11) Equals方法

对于Equals方法,C#提供了四种不同的比较方法:
public static bool ReferenceEquals(object left, object right)
public static bool Equals(object left, object right)
public virtual bool Equals(object right)
public static bool operator==(MyClass left, MyClass right)
以下对这四种方法进行分析
(a) ReferenceEquals函数,不能重写这种函数。这个函数比较两个对象的Identity是否相等,对于引用类型的对象来说,如果两个对象指向同一个对象,则这个函数返回True;而对于值类型对象,这个函数永远返回False。比如int i = 5; Object.ReferenceEquals(i, i) = false.这个是比较奇怪的地方。
(b) static Equals函数,不能重写这个函数。这个函数的实现如下所示
if (left == right) return true;
if (left == null || right == null) return false;
return left.Equals(right);
所以这个函数依赖于下面要提到的Equals方法的实现。
(c) Equals函数。对于引用类型来说,如果没有重写这个函数,则其比较方法和ReferenceEquals函数相同。对于值类型对象,值类型的基类ValueType已经重写了这个函数,只要值类型的所有的内容都相同,则这个函数返回True。但对于值类型对象有个效率的问题,因为ValueType要对所有继承其的值类型都要适用,所以只能使用反射来取得所有的属性,众所周知,反射的效率是非常非常慢的。因此,对于所有的值类型对象,都要重写这个函数。而对于引用类型的对象,如果需要在语义的层次定义其相等性,则可以重写这个函数。
对于这个函数的实现,有一般的实现方式
public override bool Equals(object right)
{
    if (right == null) return false;
    if (Object.ReferenceEquals(this, right)) return true;
    if (this.GetType() != right.GetType()) return false;
    // Compare other members.
}
至于为什么要用this.GetType来比较,而不用LeftClass rightAsLeftClass = right as LeftClass,再判断rightAsLeftClass是否为Null的方式来比较?因为如果我们采用这种方式来比较,则left as RightClass和right as LeftClass中有可能会有一个永远返回Null(很明显,子类可以转换成基类,基类不能转换成子类),则这时候会违反传递性的原则,即left.Equals(right)可能返回True,而right.Equals(left)永远返回False。
另外,对于Equals方法是不能抛异常的。在子类的Equals方法中,一般要调用基类的Equals方法,除非基类是Object类型。
(d) ==函数,对于值类型,要重写这个函数,其原因和Equals函数的原因是一样的,都是为了效率。而对于引用类型的对象,则不建议重写这个函数,因为默认的行为就是要判断引用是否指向同一个对象。

(12) GetHashCode方法

这个函数只用于一个地方:用来定义基于哈希的集合的键值,比如Hashtable和Dictionary。所以如果你的类不是用在哈希的Key中,则不需要考虑这个方法。
对于所有重写这个该当的函数,必须满足以下三个条件:
(a) 如果两个对象是相等的(通过==比较,不是Equals函数),则这个函数必须返回相同的值。
(b)对于一个类A的对象,其GetHashCode方法必须永远返回相同的值,不管如何操作这个对象,如何调用这个对象上的函数。这是为了保证一个对象存储进哈希集合后,能够被正确读取出来。
(c)这个函数对于所有的输入要生成随机均匀的分布。
首先来看看Object.GetHashCode和ValueType.GetHashCode的实现方法有什么特别的地方。
对于Object.GetHashCode,这个方法是正确的,但其效率不好(即违反了(c)规则)。Object的这个函数返回的是里面存储的一个不可变的整数值,这个整数值随着对象的创建从1开始退增。这个值是不可变的。但对于不同的输入,其产生的分布是不均匀的,因此效率并不高。
对于ValueType.GetHashCode,其返回第一个变量的HashCode值,比如public struct MyStruct {private string _msg; private int _d;}返回的是_msg的GetHashCode()值。因此值类型的GetHashCode函数非常依赖于第一个变量。如果第一个变量参于==函数的计算,即只有第一个变量相等时,两个值类型才会相等,则其满足(a)规则,否则是不满足的。如果第一个变量是不可变的,则其满足(b)规则,否则是不满足的。如果第一个变量的HashCode随机均匀分布,则其满足(c)规则,否则也是不满足的。
当我们重写GetHashCode方法时,就要从以上三个规则出发。所有在==函数中参与的变量都要在GetHashCode函数参与HashCode的生成,虽然可以只要求==函数中的部分变量参与HashCode的生成,但势必会造成分布不均匀的情况。而参与HashCode生成的所有变量都要是不可变的,否则规则(b)就没办法满足。而对于如何生成分布均匀的哈希值,并没有一个必然成立的规律。比较好的一个办法是对所有参与生成HashCode的变量的哈希值作异或(XOR)的操作。

(13) foreach

尽量使用foreach来访问集合里面的每一个元素。因为
(a) C#编译器会根据集合元素的类型来生成不同的访问代码,如对于数组MyClass [] array来说,会生成类似for(int inex = 0; index < array.Length; index ++) { //Visit array[index] }的代码,而对于实现在了IEnumerator接口的集合,则会生成类似IEnumerator it = foo.GetEnumerator(); while(it.MoveNext()) {...}的代码。
(b) foreach是支持不同的数组上界和下界的,比如如果有些数组是从1开始的,则foreach也能够很好地生成相应的代码。
(c) foreach能够支持多维数组。比如MyClass [,] multiArray = new MyClass[N, N],则foreach(MyClass var in multiArray)能够访问所有的二维数组的元素。
(d) 当修改了集合类型后,可以不修改代码而能够直接使用。

有个很有趣的现象,以下两段代码:
int [] foo = new int[100]
(a) for (int index = 0; index < foo.Length; index ++) {}
(b) int len = foo.Length;
      for (int index = 0; index < len; index ++) {}
(b)通过C#编译器生成的代码比(a)的代码效率要低得多,其生成的代码类似如下:
int len = foo.Lenght;
for (Int index = 0; index < len; index++)
{
    if (index < foo.Length) {...}
    else { throw IndexOutOfRangeException(); }
}
可以看到在循环里面,生成的代码又检查了index是否小于foo.Length。因为C#编译器无法预知len是否在foo数组的合法范围内,因此编译器会生成额外的代码去检查index是否在合法的范围内,否则抛出IndexOufOfRangeException异常。而对于(a)类代码来说,因为编译器已经预知index会在0...foo.Length内,因此是合法的,不需要抛任何异常。

如果用户自定义的集合类型也希望能够使用foreach关键词来访问,则只需要通过以下三种方式之一就可以实现:(a) 提供public GetEnumerator()函数;(b) 实现IEnumerable接口;(c) 实现IEnumerator接口。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值