这是我边阅读《Effective C#中文版》边对各原则进行的摘写。
需要的时候才对原则进行说明,否则直接略过说明。因为有些情况下我这个菜鸟还没有遇到过,哈哈。
1. 始终使用属性(Property),而不是直接使用字段,对字段永远声明为私有的(Private)。
.Net中数据绑定只支持属性而不支持字段。
属性容易适应新需求,如书中的例子:
当没有对Customer的姓名不为空作检验,则在属性上进行维护更方便。而不需要修改所有调用了Customer的地方。
private string _name;
public string Name
{
get { // do something. }
set
{
if (string.IsNullOrEmpty(value))
{
// verify fail to do something.
}
_name = value;
}
}
由于属性是方法实现的,所有拥有方法的一切,如可以定义为虚拟(virtual),抽象(abstract)甚至可以成为接口的一部分。
属性的读取(get)和设置(set)器是两个独立的方法,所以可以控制其可见性。
2. 定义常量时使用readonly而不用const
声明运行时常量用readonly,而编译时常量是用const。
保留const编译时常量是因为对性能有苛刻的要求或在运行时永远保持值不变。
编译时常量效率高但容易出错。
编译时常量无法在运行期间实例化(new),如
private const DateTime from = new DateTime();
这将编译不通过。
编译时常量在IL中生成的是值,而不像运行时常量是使用一个变量的引用,如:
private const int to = 10;
for (int i = 0; i < to; i++)
{
if (i == to)
{
// do something.
}
}
跟如下代码效果一样:
for (int i = 0; i < 10; i++)
{
if (i == 10)
{
// do something.
}
}
运行时常量可在构造期间进行赋值,而构造完成后不得修改。
3. 使用as代替强制类型转换
无论何时,使用as总比盲目强制转换更安全,且效率更高。
强制转换错误时会抛出异常,而as却总会安全地返回一个null。
强制转换在对如long转换为short时,如果short范围不足会造成错误的数据,这种情况下不会抛出异常,使得你不得不到处查找出错的地方。
as无法用在值类型的转换上,因为as失败时返回的null无法赋值给值类型,除了对值类型进行可空。
4. 使用条件特性(attribute)而不是#if...#endif
条件特性:[Conditional(string parameter)]
在中文上attribute和property都翻译成属性。而.Net中attribute与property又不相同,attribute可以看成是附加上的。
在xml中attribute也与property不同。如钢铁天生具有导电性,而客户的名称是后天才有的。
5. 始终为你的所有类型提供ToString方法
6. 区分值类型数据和引用类型数据
把值类型当成是数据的容器。使用值类型数据时都会得到一个副本,就像一个复制(copy)操作。
使用引用类型时就像房子有一个地址。你得通过地址才能找到房子。
值类型数据在内存管理上有很好的性能。
引用类型数据在内存分配上需要保存地址跟值。
如果你拿不准你所期望使用的,就使用引用类型。就像使用结构(struct)和类(class)。因为有时需求的变化,不是简单的把一个结构修改成一个类。
7. 确保0是值类型的有效状态,因为0是值类型的默认初始值
因为值类型数据初始状态为0。
如果在枚举中,你是从1开始,如:
public enum Day
{
Monday = 1,
...
}
这种情况下,如果一个类中的属性的数据类型是Day,那么在实例化该类时,此类的Day数据类型属性的初始值为0。在未赋新值时错误的使用将是非常糟糕的情况。
所以应确保0是值类型数据的默认初始值,如:
public enum Day
{
Sunday = 0,
Monday = 1,
...
}
8. 理解ReferenceEquals(),static Equals(),instance Equals()和运算符==
object.ReferenceEquals(left, right)方法,该方法如其名称,只支持用于两个引用类型的参数进行相等比较。这个方法在开发上通常是不会需要重新定义的。
引用类型的比较,判断内存地址是否相同。
如:
Customer leftReferenceType = new Customer();
Customer rightReferenceType = new Customer();
Console.WriteLine("When data type of two variables were reference type:");
Console.WriteLine("Different memory address and value:");
Console.WriteLine(object.ReferenceEquals(leftReferenceType, rightReferenceType)); // false;
Console.WriteLine("Left reference type variable assign value to right reference type variable:");
rightReferenceType = leftReferenceType;
Console.WriteLine("Same memory address and value:");
Console.WriteLine(object.ReferenceEquals(leftReferenceType, rightReferenceType)); // true;
如代码中所示,第一次判断两个实例对象是否相同,因为两个都赋值不同,所以引用了不同的内存地址,结果就返回false。而把其中一个实例对象赋值给另一个,则结果返回true。这使得内存中已经存在了一块垃圾。rightReferenceType变量放弃了原先的内存地址。
如果给object.ReferenceEquals()方法传递两个值类型参数,则无论怎样都会返回false。如:
int leftValueType = 0;
int rightValueType = 0;
Console.WriteLine("ReferenceEquals");
Console.WriteLine("When data type of two variables were value type:");
Console.WriteLine(object.ReferenceEquals(leftValueType, rightValueType)); // false;
rightValueType = leftValueType;
Console.WriteLine("Left value type variable assign value to right value type variable:");
Console.WriteLine(object.ReferenceEquals(leftValueType, rightValueType)); // false;
Console.WriteLine("Because value type data is transmit copy.");
静态的object.Equals(left, right)方法与left.Equals(right)方法相同
因为object.Equals(left, right)方法最终调用的还是left.Equals(right)方法。
静态的object.Equals(left, right)方法通常用于在不清楚两个参数的运行类型时,检测它们是否相等。该方法在开发上通常是不会需要重新定义的。
而值类型使用的是值比较,如果其中一个参数的值不一样,结果返回false,如:
int leftValueType = 0;
int rightValueType = 0;
Console.WriteLine("When data type of two variables were value type:");
Console.WriteLine(leftValueType.Equals(rightValueType)); // true;
leftValueType = 1;
Console.WriteLine(leftValueType.Equals(rightValueType)); // false;
运算符==对于引用类型也是使用内存地址比较,而值类型使用的是比较他们的值是否相等。
运算符==默认的版本在比较值类型时使用了引用的方法,使得出现装箱操作。效率远不及自己所重新定义的。
值得注意的是,值类型在比较时非常麻烦,效率慢。因为System.ValueType是没有重载Object.Equals()方法的,System.ValueType是所有你所创建的值类型(结构struct)的基类。比较时都是在此基类上进行。为了提供正确的行为,它必须比较派生类的所有成员变量,而且是在不知道派生类的类型的情况下,这将使用反射。对于反射而言过多的使用会带来性能的损失。
在重载Equals()方法时不应抛出异常。两个变量要么相等要么不等。
重载Equals()方法时要注意确保数学相等性质:相等的自反性,对称性和传递性。
自反性:a == a一个对象等于它自己。
对称性:a == b那么b == a。
传递性:a == b,b == c,那么a == c。
不应重载静态的Object.ReferenceEquals()和静态的Object.Equals()方法,因为它们提供了正确的检测,忽略了运行时类型。你应该为了更好的性能而总是为值类型提供重载的Equals()方法和运算符==。
9. 对数组跟集合使用foreach
循环数组或集合有三种方式:
int[] foo = new int[100];
第一种:
foreach(int i in foo)
{
// do somethings.
}
第二种:
for (int index = 0; index < foo.Length; index++)
{
// do somethings.
}
第三种:
int len = foo.Length;
for (int index = 0; index < len; index++)
{
// do somethings.
}
对于C或C++,第三种循环效率最佳。但是在C#中是最差的,因为在访问每一个实际的集合时,运行时确保对每个集合的边界做检测。而将集合的大小赋值给一个变量,这使得每次进行for循环第二个条件参数总需要对边界进行两次检测。并使得JIT编译器在生成代码的时候做了更多的事情。
原始的C#编译器对第二种循环效率是最佳的,反而foreach循环效率差,主要是因为涉及到装箱操作。而数组类型是安全的情况下,foreach可以为数组生成不一样的IL代码,使得效率跟第二种循环一样。并且使用foreach可以写更少的代码。
10. 可能的情况下声明变量的同时进行初始化
如:
public class MyClass
{
private ArrayList _arr = new ArrayList();
}
在不需要依赖注入到构造器中进行初始化的变量,不要等到在构造器中初始化。因为当构造器过多时,会出现维护问题。
变量的直接初始化会在类的构造器执行前先执行。
当对自定义值类型的变量进行初始化时,如值类型默认的值为0,这种情况下不需要进行初始化。因为这种情况下会进行不必要的装箱和拆箱操作。
当有两个构造器时,不要出现这种低效率的方式:
public class MyClass
{
private ArrayList _arr = new ArrayList();
public MyClass() { }
public MyClass(int size)
{
_arr = new ArrayList(size);
}
}
11. 使用静态构造函数初始化类的静态成员
在一个类的的任何实例初始化前,你应该初始化它的静态成员变量。在C#里,可以在声明静态变量的同时初始化或者是使用静态构造函数。静态构造函数在类的实例构造函数,方法,变量或者属性被访问前执行。并且只执行一次。使用静态构造函数比声明方式能更好的处理静态变量的初始化,因为可以在其中捕捉异常:
public class MyClass
{
private static staticVar;
static MyClass()
{
try
{
staticVar = // initialize.
}
catch (Exception ex)
{
// deal exception.
}
}
}
如:
public class MyClass
{
public MyClass() : this(0) { }
public MyClass(int var1)
{
// do something.
}
}
如果使用base,正常会放到最后一个构造函数中去。在C#中构造函数不能同时使用base跟this。
13. 使用using和try释放资源
使用非托管资源的类型必须实现IDisposable接口的Dispose方法进行资源释放清理。任何时候在使用一个有Dispose方法的类型时,你都有责任调用Dispose方法释放资源。最好的方法是使用using或try。
using其实最后会在IL生成与使用try同样的代码。
如果要被释放的对象在多次调用,则需要在finally里释放资源时,判断对象是否为Null,如:
finally
{
if (command != null)
command.Dispose();
}
如果多个using嵌套,则使用一个try块是更好的做法。因为前面说过using会生成try块,那么嵌套的using会生成嵌套的try块。
还有,在对象提供了Close方法和Dispose方法供选择时,使用Dispose更佳,因为Close并没有真正释放资源,而是放到等待被释放资源的析构队列中。所以还要排队等待被释放。
14. 垃圾最小化
在Windows应用程序开发上,OnPaint画图句柄中最常见的“垃圾”做法:
protected override void OnPaint(PaintEventArgs e)
{
using (Font font = new Font("Arial", 10.0f)
{
e.Graphics.DrawString(DateTime.Now.ToString(), font, Brushes.Black, new PointF(0, 0));
}
base.OnPaint(e);
}
OnPaint函数调用非常频繁。每次都会生成一个新的Font对象。而它实际上是完全一样的内容。取而代之的做法:
private readonly Font font = new Font("Arial", 10.0f);
protected override void OnPaint(PaintEventArgs e)
{
e.Graphics.DrawString(DateTime.Now.ToString(), font, Brushes.Black, new PointF(0, 0));
base.OnPaint(e);
}
有些人说怎么可能会犯这种错误,可以说初学者最会犯错,因为初学者未了解底层的运行机制。
而在这个示例中,Brushes.Black演示了另一个避免重复创建对象的技术,如:
private static Brush _blackBrush;
public static Brush Black
{
get
{
if (_blackBrush == null)
_blackBrush = new SolidBrush(Color.Black);
return _blackBrush;
}
}
还有一个案例,在过多使用字符串对象连接的时候,使用String.Format方法或者StringBuilder对象。
15. 装箱拆箱最小化
C#中装箱就是将一个值类型数据存放到object中形成一个引用。而当需要访问到装在object中的值类型数据时,需要使用强制转换进行拆箱。
在C#编程中,无意中就在使用着装箱拆箱操作。在像Console.WriteLine(string, params object[])这个方法时,你会发现如果你传递的是一个int类型到object中就已经发生了装箱拆箱操作。如:
Console.WriteLine("A few numbers: {0}, {1}, {2}", 1, 2, 3);
这种情况下,数据123将装箱到object中,在生成字符串时,数字值将进行进行拆箱操作转换成字符串。如果对以上代码进行优化,可如:
Console.WriteLine("A few numbers: {0}, {1}, {2}", 1.ToString(), 2.ToString(), 3.ToString());
优化后的代码也不可避免的进行装箱操作。但是使用ToString()可以避免拆箱操作。
装箱拆箱操作总会在你不经意的时候做一些对象的拷贝。还有一种就是尽量选择一些接口而不是公共方法来访问箱子内部的数据,从而减少不必要的拆箱操作,如下面代码,一段是没有优化前,一段是优化后(但是优化后的代码却得多做点繁杂的工作):
优化前:
public struct Person
{
private string _name;
public string Name
{
get { return _name; }
set { _name = value; }
}
public override string ToString()
{
return _name;
}
}
ArrayList attendees = new ArrayList();
Person p = new Person("Old Name");
attendees.Add(p);
// Would work if Person was a reference type.
Person p2 = ((Person)attendees[0]);
p2.Name = "New Name";
Console.WriteLine(attendees[0].ToString());