您可以捐助,支持我们的公益事业。

1元 10元 50元





认证码:  验证码,看不清楚?请点击刷新验证码 必填



  求知 文章 文库 Lib 视频 iPerson 课程 认证 咨询 工具 讲座 Modeler   Code  
会员   
 
   
 
 
     
   
 订阅
  捐助
编写高质量代码改善C#程序的建议读书笔记
 
作者:dotnetgeek 来源:博客园 发布于 2015-02-13
  2476  次浏览      15
 

开篇

学生时代,老师常说,好记性不如烂笔头,事实上确实如此,有些知识你在学习的时候确实滚瓜烂熟,但是时间一长又不常用了,可能就生疏了,甚至下次有机会使用到的时候,还需要上网查找资料,所以,还不如常常摘录下来,即使下次忘记具体细节还能从我自己的博客中轻易的找出来呢,还能和各位园友分享知识,还有一点就是,读书是一件持之以恒的事情,我大学期间试过从图书馆借回来的书,三个月限期已到了还没读完又还回去了,说到底就是没有读书的动力,所以开一个读书笔记的文章系列也是很有必要的,督促自己要把这本书啃完。

建议1:正确操作字符串拼接,避免Boxing

1、string str1 = "str1" + 9;
2、string str2 = "str2" + 9.ToString();

从IL代码得知,第一行代码会产生装箱行为,而第二行代码9.ToString()并没有发生装箱行为,它是通过直接操作内存来完成int到string的转换,效率要比装箱高,所以,在使用其他值类型到字符串的转换来完成拼接时,避免使用“+”来完成,而应该使用FCL提供的ToString()方法进行类型转换再拼接;另外,由于System.String类对象的不可变特性,进行字符串拼接时都要为该新对象分配新的内存空间,所以在大量字符串拼接的场合建议使用StringBuilder。

建议2:使用默认转型方法

1、使用类型的转换运算符

其实就是使用内部的一个方法,转换运算符分两类:隐式转换、显式转换(强制转换)基元类型普遍都提供了转换运算符。

int i = 0;
float j = 0;
j = i; //int到float存在隐式转换
i = (int)j; //float到int需要显式转换

自定义类型通过重载转换运算符来实现这一类的转换:

class program
{
static void main(string[] args)
{
Ip ip = "127.0.0.1"; //通过Ip类的重载转换运算符,实现字符串到Ip类型的隐式转换
Console.WriteLine(ip.ToString());
}
}
public class Ip : Object
{
IPAddress value;
//构造函数
public Ip(string ip)
{
value = IPAddress.Parse(ip);
}
//重载转换运算符,implicit 关键字用于声明隐式的用户定义类型转换运算符。
public static implicit operator Ip(string ip)
{
Ip iptemp = new Ip(ip);
return iptemp;
}
//重写基类ToString方法
public override string ToString()
{
return value.ToString();
}
}

2、使用类型内置的Parse

在FCL中,类型自身会带有一些转换方法,比如int本身提供Parse、TryParse方法……

3、使用帮助类提供的方法

System.Convert提供将一个基元类型转换到其他基元类型的方法,如ToChar、ToBoolean等,如果是自定义类型转换为任何基元类型,只要自定义类型实现IConvertible接口并且实现相关的转换方法即可;

ps:基元类型是指编译器直接支持的数据类型,即直接映射到FCL中的类型,包括sbyte、byte、short、ushort、int、uint、long、ulong、char、float、double、bool、decimal、object、string。

4、CLR支持的转换

即子类与父类的上溯转换和下溯转换;子类向父类转换的时候,支持隐式转换,而当父类向子类转换的时候,必须是显式转换,就好比,狗(子类)是动物(父类),但,动物不一定是狗,也可能是猫。

建议3:区别对待强制转型、as、is

secondType = (SecondType)firstType;

以上代码发生强转换类型,意味着下面两种事情的其中一件;

1)FirstType和SecondType彼此依靠转换操作符来完成两个类型的转换;

2)FirstType是SecondType的基类;

第一种情况:FirstType和SecondType存在转换操作符

public class FirstType
{
public string Name { get; set; }
}
public class SecondType
{
public string Name { get; set; }
//explicit 和 implicit 属于转换运算符,explicti:显式转换,implicit可以隐式转换
public static explicit operator SecondType(FirstType firstType)
{
SecondType secondType = new SecondType()
{
Name = firstType.Name
};
return secondType;
}
}

这种情况,必须使用强转换,而不能使用as操作符

我们再看看这种情况,这段代码编译成功,但是运行时报错,其原因是万类都继承自object,但是编译器会检查o在运行时是不是SecondType类型,从而绕过了转换运算符,所以建议,如果类型之间存在继承关系,首选使用as,子类之间的转换应该提供转换运算符以便进行强制转换。

第二种情况:FirstType是SecondType的基类
这种情况,既可以使用as也可以使用强制转换,从效率和代码健壮性来看,建议使用as,因为as操作符不会抛出异常,类型不匹配的时候,返回值为null。

is和as:

object o = new object();
if (o is SecondType)
{
secondType = (SecondType)o;
}

这段代码实际效率不高,因为执行了2次类型检测,is操作符返回boolean返回值,只是检测并没有转换,而as操作符会进行转换,如果转换失败则返回null;

建议4:TryParse比Parse好

//Parse
int a = int.Parse("123");

//TryParse
int x = 0;
if (int.TryParse("123", out x))
{
//转换成功,x=123
}
else
{
//转换失败,x=0
}

这个应该不必多说了,相信很多人都经常使用的,从.NET2.0开始,FCL开始为基元类型提供TryParse方法以解决在Parse转换失败的时候触发的异常所带来的性能消耗;

在效率方面,如果Parse和TryParse都执行成功的话,它们的效率是在同一个数量级的,甚至在书中的实验中,TryParse还比Parse高,如果Parse和TryParse都执行失败的话,Parse的执行效率就大大低于TryParse了。

建议5:使用int?确保值类型也可以为null

在开发的过程中,可能你也遇到过值类型不够用的场景,比如,数据表字段设置为int类型,并且允许为null,这时反映在C#中,如果将null赋值给int类型的变量也不对,会报错;

所以,从.NET2.0开始,FCL提供一种可以为Null的类型Nullable<T> 它是一个结构体:

public struct Nullable<T> where T: struct

但是结构体Struct是值类型,应该也不能为空才对啊,书中也没有解释得很深入,很模糊的一两句就带过了,于是我继续深入探讨,首先使用Reflector对mscorlib.dll反编译;

public struct Nullable<T> where T: struct
{
private bool hasValue;
internal T value;
public Nullable(T value);
public bool HasValue { get; }
public T Value { get; }
public T GetValueOrDefault();
public T GetValueOrDefault(T defaultValue);
public override bool Equals(object other);
public override int GetHashCode();
public override string ToString();
public static implicit operator T?(T value);
public static explicit operator T(T? value);
}

不知道什么原因,当我展开这些方法的时候,都是空空的,但是,我发现它有重载转换运算符,implicit 是隐式转换,explicit 是显式转换
然后在写一个小程序,代码如下:

protected void Page_Load(object sender, EventArgs e)
{
Nullable<int> a = null;
}

然后对这个web应用程序进行反编译查看:

protected void Page_Load(object sender, EventArgs e)
{
int? a = new int?();
}

可以看出,Nullable<int> a = null; 最终是进行了初始化,而此时,hasValue属性的值也应该为False;

所以,我猜想,Nullable<int> 或者 int? ……等可空的基元类型设置为null的时候,实际上并不是像引用类型那样为null了,而是进行了初始化,并且hasValue属性的值为False。

猜想完之后,我去MSDN搜了一下,得到验证:http://msdn.microsoft.com/zh-cn/library/ms131346(v=vs.100).aspx

建议6:区别readonly和const的使用方法

这个建议我打算自己写一个比较简明的例子来说明,而不使用书本的例子,即使有些工作几年的朋友,也可能一下子说不清楚const与readonly的区别,感觉它们实现的效果也是一样的,都表示一个不可变的值,其实它们的区别在于:

·const是编译时常量(编译时确定下来的值)

·readonly是运行时常量(运行时才确定)

下面建立一个DEMO来举例说明:

1、新建一个类库,新建Person类,设置如下两个常量:

namespace ClassLibrary
{
public class Person
{
public const int height = 100;
public readonly static int weight = 100;
}
}

2、在主程序中添加ClassLibrary类库的引用,输出常量:

protected void Page_Load(object sender, EventArgs e)
{
Response.Write("身高:" + ClassLibrary.Person.height);
Response.Write("体重:" + ClassLibrary.Person.weight);
}

此时毫无疑问的,输出结果为:身高:100体重:100,

3、修改Person类中的height、weight常量为:170,,并且编译该类库(注意:只生成该类库,而不生成主程序)

此时再运行主程序页面,输出结果为:身高:100体重:170 ;

究其原因,height为const常量,在第一次编译期间就已经将值100HardCode在主程序中了,而第二次修改值之后,并没有生成主程序,所以,再次运行的时候,还是第一次的值,我们使用ILDASM来看看编译后的IL代码吧。

建议7:将0值作为枚举的默认值

允许使用的枚举类型有byte、sbyte、short、ushort、int、uint、long、ulong、应该始终将0值作为枚举的默认值;书中这个建议举的例子我不太明白,我的理解大概是这样子的,假如有如下的枚举

enum Week
{
Money = 1,
Tuesday = 2,
Wednesday = 3,
Thursday = 4,
Friday = 5,
Saturday = 6,
Sunday = 7
}

万一你一不小心代码写成这样

static Week week;
protected void Page_Load(object sender, EventArgs e)
{
Response.Write(week);
}

输出的结果为0,就会让人觉得是多了第八个值出来了,所以,建议使用0值作为枚举的默认值。

建议8:避免给枚举类型的元素提供显式的值

“一般情况下,没有必要为枚举元素提供显示的值”

我觉得这个建议是可有可无了,这个看个人习惯,作者的建议是假如我们在上面的枚举中,增加一个元素,代码如下:

enum Week
{
Money = 1,
Tuesday = 2,
TempValue,
Wednesday = 3,
Thursday = 4,
Friday = 5,
Saturday = 6,
Sunday = 7
}

此时,TempValue的值是什么呢?

Week week = Week.TempValue;
Response.Write(week);
Response.Write(week==Week.Wednesday);

ValueTemp的结果却是:Wednesday True;

如果没有为元素显式赋值,编译器会逐个为元素的值+1,也就是自动在Tuesday=2的基础上+1,最终TempValue和Wednesday的值都是3,然后作者的意愿是希望干脆就不要指定值了,因为编译器会自动帮我们+1,但是,我的想法是,如果不指定值的话,当我们下次来看看这个枚举的话,难道要数一数该元素排行第几才能知道代表的Value吗?而且,万一枚举有修改的话就有可能不小心修改而导致Value乱掉的情况了。

System.FlagsAttribute属性

当一个枚举指定了System.FlagsAttribute属性之后,就意味着可以对这些值进行AND、OR、NOT、XOR按位运算,这就要求枚举中的每个元素的值都是2的n次幂指数了,其目的是任意个元素想加之后的值都不会和目前枚举中的任一元素的值相同,书中关于这方面说得很少,只是提了个大概,于是我参考了些资料,做了个DEMO更加深入的研究。

[Flags]
enum Week
{
None = 0x0,
Money = 0x1,
Tuesday = 0x2,
Wednesday = 0x4,
Thursday = 0x8,
Friday = 0x10,
Saturday = 0x20,
Sunday = 0x40
}
protected void Page_Load(object sender, EventArgs e)
{
//利用“|”运算,将各个元素组合起来
Week week = Week.Sunday | Week.Tuesday | Week.Thursday;
Response.Write(GetDayOfWeek(week));
}
private string GetDayOfWeek(Week week)
{
string temp = string.Empty;
foreach (Week w in Enum.GetValues(typeof(Week)))
{
//利用“&”运算拆分
if ((week & w) > 0)
temp += string.Format("{0} <br>", w.ToString());
}
return temp;
}

输出结果为:

Tuesday 
Thursday
Sunday

这种设计是利用了计算机基础中的二进制数的“与”“或”运算,从而可以巧妙的将各个元素组合起来成为一个数据,并且能最后拆分出来,这种设计思想可以广泛的应用在权限设计、收费方式……等需要多种数据组合的地方。

我再说说其中的原理吧,首先看我定义枚举的值,对应出来的二进制数为:

0001、0010、0100、1000 ……

举个例子:比如0x1和0x8组合,对应的二进制数是:0001、1000,那么他们通过“|”运算组合起来之后的值是:1001,

也就是调用GetDayOfWeek方法的时候,参数值为1001了,然后遍历枚举的时候进行&运算拆分

Monday:1001 & 0001 = 0001 结果大于0,符合条件

Tuesday:1001 & 0010 = 0000 结果等于0,不符合条件

Wednesday: 1001 & 0100 = 0000 结果等于0,不符合条件

Thursday: 1001 & 1000 = 1000 结果大于0,符合条件

于是,通过这种方法,就能找出当初组合起来的2个元素了。

建议9:习惯重载运算符

上几个建议当中,我们接触过重载转换符,使得可以实现类似IPAddress ip="127.0.0.1";之类的不同类型的对象之间的转换,使得代码更加直观简洁,同样的对于下面2段代码:

(1)int total=x+y;

(2)int total=int.Add(x,y);

我们当然希望看到的是第一种而不是第二种,因为第一种语法特性我们大多数人看得习惯明解,所以,构建自己的类型的时候,我们应该考虑是否可以进行运算符重载。

class Salary
{
public int RMB { get; set; }
public static Salary operator +(Salary s1, Salary s2)
{
s2.RMB += s1.RMB;
return s2;
}
}

进行重载之后,就可以这样使用了,方便多了。

Salary s1 = new Salary() { RMB = 10 };
Salary s2 = new Salary() { RMB = 20 };
Salary s3 = s1 + s2;

建议10:创建对象时需要考虑是否实现比较器

有对象的地方就会存在比较,过年回家,你妈也会把你跟人家的孩子来比,实现IComparable 接口即可实现比较排序功能;

我们先来新建一个基础的类来一步步看看是如何实现比较器的;

 class Salary  
{
public string Name { get; set; }
public int BaseSalary { get; set; }
public int Bonus { get; set; }
}

因为ArrayList有sort()这个排序方法,那岂不是不用实现也能进行对比排序了吗?事实果真如此的美好吗?

ArrayList companySalary = new ArrayList();
companySalary.Add(new Salary() { Name = "A", BaseSalary = 2000 });
companySalary.Add(new Salary() { Name = "B", BaseSalary = 1000 });
companySalary.Add(new Salary() { Name = "C", BaseSalary = 3000 });
companySalary.Sort(); //排序
foreach (Salary item in companySalary)
{
Response.Write(item.Name + ":" + item.BaseSalary);
}

现实却如此悲惨,因为对象类里面有很多字段,编译器不会智能到知道你要使用哪个字段来作为排序对比的字段的。

so,我们必须对Salary类实现IComparable接口,并且实现接口成员CompareTo(object obj)

class Salary : IComparable
{
public string Name { get; set; }
public int BaseSalary { get; set; }
public int Bonus { get; set; }
//实现IComparable接口的CompareTo方法,比较器的原理
public int CompareTo(object obj)
{
Salary staff = obj as Salary;
if (BaseSalary > staff.BaseSalary)
{
return 1; //如果自身比较大,返回1
}
else if (BaseSalary == staff.BaseSalary)
{
return 0;
}
else
{
return -1;//如果自身比较小,返回1
}
}
}

调用地方的代码不用修改,程序再次跑起来,运行结果为:

B:1000 A:2000 C:3000

OK,我们再次深入一点,假设这个月结算不以BaseSalary来排序,而是以Bonus奖金来排序,那该怎么办?当然,重新修改Salary类内部的CompareTo接口成员肯定是可以的,但是,比较聪明的方法就是自定义比较器接口IComparer(注意,刚才实现接口名字叫IComparable,而自定义的比较器接口是IComparer)

class BonusComparer : IComparer
{
public int Compare(object x, object y)
{
Salary s1 = x as Salary;
Salary s2 = x as Salary;
return s1.Bonus.CompareTo(s2.Bonus);
//实际上,上例也可以使用内部字段的CompareTo方法
//但是由于演示比较器内部原理,则写了几个if了。
}
}

Sort方法接受一个实现了IComparer接口的类对象作为参数,所以,我们可以这样子进行传参

//提供非默认的比较器BonusComparer
companySalary.Sort(new BonusComparer());

关于比较器的内容,书中说到这里就应该结束了,接下来是考虑比较的时候性能的问题,可以想象,如果一个集合成千上万的数据甚至更多需要比较的话,而上面的例子中,使用了类型转换Salary s1 = x as Salary;这是非常消耗性能的,泛型的出现,可以很好的避免类型转换的问题:

1、ArrayList可以使用List<T>来代替

2、使用IComparable<T> 、 IComparer<T> 来代替

Just Look Like That

class Salary : IComparable<Salary>
{
public string Name { get; set; }
public int BaseSalary { get; set; }
public int Bonus { get; set; }
public int CompareTo(Salary staff)
{
return BaseSalary.CompareTo(staff.BaseSalary);
}
}
class BonusComparer : IComparer<Salary>
{
public int Compare(Salary x, Salary y)
{
return x.Bonus.CompareTo(y.Bonus);
}
}

建议11:区别对待 == 和Equals

CLR中将“相等性”分为两类:

1、值相等性:两个变量包含的数值相等。

2、引用相等性:两个变量引用的是内存中的同一个对象。

但并不是所有的类型的比较都是按照其本身,比如string是一个特殊的引用类型,但是在FCL中,string的比较就被重载为针对“类型的值”的比较,而不是“引用本身”的比较。对于自定义类型来说,如果想要实现这样的值比较而不是引用比较的话,则需要重载Equals方法,比如对于Person类,如果IDCode相同,我们可以认为他们是同一个人。

class Person
{
public string IDCode { get; private set; }
public Person(string idCode)
{
this.IDCode = idCode;
}
public override bool Equals(object obj)
{
return IDCode == (obj as Person).IDCode;
}
}

此时通过Equals去比较的话,则就会通过重载后的方法来进行了。

 object a = new Person("ABC");
object b = new Person("ABC");
Console.WriteLine(a == b); //False
Console.WriteLine(a.Equals(b)); //True

说到这里,作者依然没说白“==”和“Equals”的区别,只是说了一句建议的话:“对于引用类型,我们要定义值相等性,应该仅仅去重载Equals方法,同时让==表示引用相等性”。

同时,为了明确有一种方法来肯定比较的是“引用相等性”,FCL提供了Object.ReferenceEquals方法。

bool equal= object.ReferenceEquals(object a,object b);

外事不决问Google,内事不决靠反编译、MSDN了。为了弄懂==和Equals的区别,我作如下搜集整理:

1、==是运算符,而Equals是方法;

2、对于值类型、string类型,==和Equals都是比较值内容相等,使用ILSpy对Int类型进行反编译观察;int类型中的Equals方法内部逻辑就是“==”;

// int
[__DynamicallyInvokable, TargetedPatchingOptOut("Performance critical to inline across NGen image boundaries")]
public bool Equals(int obj)
{
return this == obj;
}

string类型则是判断引用地址是否相同或者值内容是否相同,两者有一个符合条件则视为“相等”,请看string类的反编译代码。

// string
[__DynamicallyInvokable, ReliabilityContract(Consistency.WillNotCorruptState, Cer.MayFail), TargetedPatchingOptOut("Performance critical to inline across NGen image boundaries")]
public bool Equals(string value)
{
if (this == null)
{
throw new NullReferenceException();
}
return value != null && (object.ReferenceEquals(this, value) || (this.Length == value.Length && string.EqualsHelper(this, value)));

}

3、对于引用类型,==和Equals都是比较栈内存中的地址是否相等,并且自定义类型中可以进行运算符重载== 或者Override Equals 来改写认为两对象相等的条件,比如Person类中,我认为只要IDCard相同即对象相同等,此时可以进行重写或者重载。

看到这里,是不是觉得有点迷茫?==好像跟Equals差不多啊,为了想弄清这个问题,我加了作者陆敏技的QQ,以下是聊天记录:

建议12:重写Equals也要重写GetHashCode

坑爹啊!上一个建议的代码原来编译成功,但编译器会友情提示的,这里作者又引出了另外一个建议,何时了啊!

这是因为如果重写Equals方法而不重写GetHashCode方法,在使用Dictionary类的时候,可能会有一个潜在的Bug。

static Dictionary<Person, string> personValues = new Dictionary<Person, string>();
protected void Page_Load(object sender, EventArgs e)
{
AddPerson();
Person mike = new Person("Mike");
Response.Write(personValues.ContainsKey(mike)); //False
}
void AddPerson()
{
Person mike = new Person("Mike");
personValues.Add(mike, "mike");
Response.Write(personValues.ContainsKey(mike)); //True
}
本段代码输出结果:True False

这段代码的意思是,执行AddPerson()的时候,将idCode=Mike的Person对象存进Dictionary中,然后在Page_Load方法内,也同样new一个idCode=Mike的Person对象,使用ContainsKey方法搜索是否存在此对象Key,结果是不存在此对象。

你可以会问,上一个建议中,我们已经重写了Person类的Equals方法了,只要idCode相等,我们就可以认为他们是相等的了,为什么此处会找不到Mike呢?
答:这是由于CLR已经优化了Dictionary这种查找,实际上是根据Key值的HashCode来查找Value值的。CLR首先调用Person类型的GetHashCode方法,发现这货根本就没有重写,于是就向上找Object的GetHashCode方法,Object为所有的CLR类型都提供GetHashCode默认实现,每new一个对象,CLR都会为该对象生成一个固定整形值,在对象生命周期内不会改变,对象默认的GetHashCode实现就是该整型值的HashCode,所以,虽然Mike值相等,但是HashCode是不相等的。

若要修正此问题,就必须重写GetHashCode方法

public override int GetHashCode()
{
return this.IDCode.GetHashCode();
}

进一步改进:GetHashCode方法存在一个问题,它返回的是一个整形类型,而整形类型的容量长度远远无法满足字符串的长度,也就是说,值不相同的情况下,HashCode可能存在相同的情况,为了减少产生相同HashCode的情况,做改进版本:

public override int GetHashCode()
{
return (System.Reflection.MethodBase.GetCurrentMethod().DeclaringType.FullName + "#" + this.IDCode).GetHashCode();
}

小结:这个建议至少让我了解了HashCode,以前重写ToString方法的时候,就经常看到GetHashCode这个东东。

建议13:为类型输出格式化字符串

这个建议我读了两次才明白啊。

1、实现IFormattable接口实现ToString()输出格式化字符串

一般我们为类型提供格式化字符串的输出的做法是重写ToString(),但是这种方法提供的字符串输出是非常单一的,所以我们可以实现IFormattable接口的ToString方法,可以让类型根据用户的输入而格式化输出,因为重写的ToString方法没有参数,而实现 IFormattable接口的的ToString方法有参数,还是看代码最清晰。

public class Person : IFormattable
{
public string FirstName { get; set; }
public string LastName { get; set; }
//重写的ToString方法输出字符串比较单一
public override string ToString()
{
return string.Format("{0},{1}", FirstName, LastName);
}
//实现IFormattable接口的ToString方法因为有参数,所以可以实现复杂的逻辑
public string ToString(string format, IFormatProvider formatProvider)
{
if (format == "ch")
return string.Format("中文名字:{0},{1}", FirstName, LastName);
else
return string.Format("EnglishName:{0},{1}", FirstName, LastName);
}
}

这样子调用:

Person p = new Person() { FirstName="wayne", LastName="chan" };
Response.Write(p.ToString());
Response.Write(p.ToString("ch",null));
Response.Write(p.ToString("english", null));

2、格式化器

上面的方法是在预见类型会存在格式化字符串输出的需求的时候,提前为类型实现了接口IFormattable,如果类型本身没有提供格式化字符串输出的功能,这时“格式化器”就派上用场了。

 //针对Person的格式化器
class PersonFormatter : IFormatProvider, ICustomFormatter
{
//IFormatProvider成员
public object GetFormat(Type formatType)
{
if (formatType == typeof(ICustomFormatter))
return this;
else
return null;
}
//ICustomFormatter成员
public string Format(string format, object arg, IFormatProvider formatProvider)
{
Person person = arg as Person;
if (person == null)
return string.Empty;
switch (format)
{
case "Ch":
return string.Format("{0}{1}", person.LastName, person.FirstName);
case "Eg":
return string.Format("{0}{1}", person.FirstName, person.LastName);
default:
return string.Format("{0}{1}", person.FirstName, person.LastName);
}
}
}

一个典型的格式化器应该要实现IFormatProvider, ICustomFormatter 接口,如果使用的话,就先初始化一个格式化器,如下:

Person person = new Person() { FirstName = "wayne", LastName = "chan", IDCode = "aaaa" };
//初始化格式化器
PersonFormatter pFormatter = new PersonFormatter();
Response.Write(pFormatter.Format("Ch", person, null));

其实看到这里,我觉得这个建议已经是非常细致的.NET知识了,一般人遇到这种情况,直接就会使用上一种方法了,在看书的时候,我也想直接跳过算了,但最后想,还是把他也记录下吧,毕竟这也是对自己的提高啊,即使以后还是会把这个知识点遗忘掉,还是可以在本博客找回来啊。

建议14:正确实现浅拷贝和深拷贝

浅拷贝和深拷贝的区别:

浅拷贝:

修改副本的值类型字段不会影响源对象对应的字段,修改副本的引用类型字段会影响源对象,因为源对象复制给副本对象的时候,是引用类型的引用地址,也就是两者引用的是同一个对象。

深拷贝:

无论值类型还是引用类型的字段,修改副本对象不会影响源对象,即使是引用类型,也是重新创建了一个新的对象引用。
要想自定义类型具有Clone拷贝的能力,就得继承ICloneable接口,然后根据需求,实现Clone方法以便实现浅拷贝或者深拷贝。

浅拷贝示例:

namespace WebApplication
{
public class Employee : ICloneable
{
public string IDCode { get; set; }
public int Age { get; set; }
public Department Department { get; set; }
//实现ICloneable接口成员
public object Clone()
{
return this.MemberwiseClone();
}
}
public class Department
{
public string Name { get; set; }
public override string ToString()
{
return this.Name;
}
}
public partial class WebForm1 : System.Web.UI.Page
{
protected void Page_Load(object sender, EventArgs e)
{
//初始化Employee对象employeeA
Employee employeeA = new Employee() { IDCode = "A", Age = 10, Department = new Department() { Name = "DepartmentA" } };
//从employeeA 浅拷贝出 employeeB
Employee employeeB = employeeA.Clone() as Employee;
//修改employeeB对象的属性
employeeA.IDCode = "B";
employeeA.Age = 15;
employeeA.Department.Name = "DepartmentB";
//输出以便验证
Response.Write(employeeB.IDCode); // A
Response.Write(employeeB.Age); //10
Response.Write(employeeB.Department.ToString()); //DepartmentB
}
}
}

从输出结果可以验证得到结果:

1、IDCode即使是string引用类型,Object.MemberwiseClone 依然为其创造了副本,在浅拷贝中,我们可以将string当做值类型来看待。

2、Employee的Department属性是引用类型,改变源对象employeeA中的值,会影响到副本对象employeeB

深拷贝示例

建议使用序列化的形式进行深拷贝:

//实现ICloneable接口成员
public object Clone()
{
//浅拷贝
//return this.MemberwiseClone();

//使用序列化进行深拷贝
using (Stream objectStream = new MemoryStream())
{
IFormatter formatter = new BinaryFormatter();
formatter.Serialize(objectStream, this);
objectStream.Seek(0, SeekOrigin.Begin);
return formatter.Deserialize(objectStream) as Employee;
}
}

复制代码

这里我按照书中的代码来运行程序,结果爆黄页错误了,提示信息是:

中的类型“WebApplication.Employee”未标记为可序列化。

因为之前有相关的开发经验,知道那是因为实体类没有被标记为序列化属性,难道作者编写示例的时候没有检查出这个错误?或者是其他原因?
我们在实体类上标记一下即可运行成功,这是修改源对象employeeA中的值也不会影响到副本对象employeeB了。

[Serializable]
public class Employee : ICloneable
[Serializable]
public class Department

建议15:使用dynamic来简化反射实现

dynamic是Framework4.0的新特性,dynamic的出现让C#具有了弱语言类型的特性,编译器在编译的时候,不再对类型进行检查,不会报错,但是运行时如果执行的是不存在的属性或者方法,运行程序还是会抛出RuntimeBinderException异常。

var 与 dynamic 的区别

var是编译器给我们的语法糖,编译期会匹配出实际类型并且替换该变量的声明。

dynamic 被编译后,实际是一个object类型,只不过编译器对dynamic做特殊处理,将类型检查放到了运行期。

这从VS的编译器窗口可以看出来,var 声明的变量在VS中有智能提示,因为VS能推断出来实际类型;dynamic声明的变量没有智能提示。

利用dynamic 简化反射

public class DynamicSample
{
public string Name { get; set; }
public int Add(int a, int b)
{
return a + b;
}
}
public partial class DynamicPage : System.Web.UI.Page
{
protected void Page_Load(object sender, EventArgs e)
{
//普通的反射做法
DynamicSample dynamicSample = new DynamicSample();
var addMethod = typeof(DynamicSample).GetMethod("Add");
int res = (int)addMethod.Invoke(dynamicSample, new object[] { 1, 2 });

//dynamic的做法,简洁,推荐
dynamic dynamicSample2 = new DynamicSample();
int res2 = dynamicSample2.Add(1, 2); //Add不会智能提示出来
}
}

使用dynamic还有一个优点就是,比没有优化过的反射性能好,跟优化过的反射性能相当,但代码整洁度高,作者也是贴了代码并贴出运行结果而已,没有作过多的介绍,所以此处作罢了。

建议16:元素数量可变的情况下不应使用数组

1、从内存使用角度看,数组在创建时被分配一段固定长度的内存,数据的存储结构一旦被分配,就不能再变化;

2、ArrayList是链表结构,可以动态增减内存空间;

3、List<T>是ArrayList的泛型实现,省去了拆箱和装箱带来的开销。

基于数组本身在内存的特点,因此,在使用数组的时候需要注意大对象(占用内存找过85000字节的对象)的问题,因为他们会被分配在大对象堆里,在回收过程中效率极低,所以,数组的长度不宜过份大。

再来回应本建议主旨,现在我们知道数组是不可变的,如果非得让数组变成“可变”的,那就只有像String那样,重新构造一个新的数组,再Copy过去了,这样可想性能是如此的差啊。

public static class ClassForExtensions
{
public static Array ReSize(this Array array, int newSize)
{
//返回当前数组、指针或引用类型包含的或引用的对象的 System.Type
Type t = array.GetType().GetElementType();
//构造一个满足需要的新数组
Array newArray = Array.CreateInstance(t, newSize);
//将旧数组的内容Copy到新数组
Array.Copy(array, 0, newArray, 0, Math.Min(array.Length, newSize));
return newArray;
}
}

总结:

此建议跟“如果大规模string字符串拼接就用StringBuilder”异曲同工。

建议17:多数情况下使用foreach进行循环遍历

为什么会有这个建议,我就有些不解了,作者先是参照IEnumerator、IEnumerable自己实现了一个类似的迭代器,然后说它的内部实现用了for循环或者是while循环,写法都有点啰嗦,然后就说foreach出现了,还说foreach最大限度简化了代码,然后开始分析IL了,关于这个建议点,我觉得说得挺含糊的,不过根据作者的观点,foreach循环除了提供简化的语法外,还有两个优势。

1、自动将代码置入try-finally块

2、若类型实现IDispose接口,foreach会在循环结束后自动调用Dispose方法。

建议18:foreach不能代替for

foreach不支持循环时对集合进行增删操作,而for循环可以,其原因是foreach循环使用了迭代器进行集合的遍历,在迭代器里维护了一个集合版本的控制,我们对集合进行增删操作的时候,都会产生一个新的版本号,当foreach循环调用MoveNext 方法遍历元素时会对版本号进行检测,一旦检测版本号变动,则抛出异常,以下是我使用ILSpy反编译得出的代码, IEnumerator接口只定义了MoveNext成员,具体实现需要反编译其实现类,我是对List<T>进行反编译的。

// System.Collections.Generic.List<T>.Enumerator
[__DynamicallyInvokable]
public bool MoveNext()
{
List<T> list = this.list;
if (this.version == list._version && this.index < list._size)
{
this.current = list._items[this.index];
this.index++;
return true;
}
return this.MoveNextRare();
}

List<T>中对版本号的检测没有抛出异常,而某些实现类则会,比如:ArrayList类

// System.Collections.ArrayList.ArrayListEnumeratorSimple
public bool MoveNext()
{
if (this.version != this.list._version)
{
throw new InvalidOperationException(Environment.GetResourceString("InvalidOperation_EnumFailedVersion"));
}
// other code
}

而for循环则不会出现这个问题,我们通常在for循环的内部使用索引器来对集合成员的访问,不对版本号进行判断检测。以下是对List<T>的索引器的反编译代码。

// System.Collections.Generic.List<T>
[__DynamicallyInvokable]
public T this[int index]
{
[__DynamicallyInvokable, TargetedPatchingOptOut("Performance critical to inline across NGen image boundaries")]
get
{
if (index >= this._size)
{
ThrowHelper.ThrowArgumentOutOfRangeException();
}
return this._items[index];
}
[__DynamicallyInvokable, TargetedPatchingOptOut("Performance critical to inline across NGen image boundaries")]
set
{
if (index >= this._size)
{
ThrowHelper.ThrowArgumentOutOfRangeException();
}
this._items[index] = value;
this._version++;
}
}

可以看出,get属性没有对_version版本号进行检测,只要索引不超过size即可,而set属性,会对_version版本号+1。

建议19:使用更有效的对象和集合初始化

这个建议应该很多人都知道或者都已经在用了,如果你还不知道,那你就out了。

List<Person> list = new List<Person>();
Person p = new Person();
p.ID = 1;
p.Name = "Tommy";
list.Add(p);

骚年,你还在这样进行对象、集合初始化吗?奥特了,借助了.NET的高级语法,我们可以使用对象和集合的初始化器来写出更加优雅的代码。设定项在大括号中对属性进行赋值

List<Person> lst = new List<Person>()
{
new Person(){ ID=1,Name="Tommy"},
new Person(){ ID=2,Name="Sammy"}
};

初始化设定项除了为对象、集合初始化方便外,还为Linq查询时的匿名类型进行属性的初始化的方便。

List<Person> lst = new List<Person>()
{
new Person(){ Age = 10,Name="Tommy"},
new Person(){ Age = 20,Name="Sammy"}
};
var entity = from p in lst
select new { p.Name, AgeScope = p.Age > 10 ? "Old" : "Young" };
foreach (var item in entity)
{
Response.Write(string.Format("name is {0},{1}", item.Name, item.AgeScope));
}

AgeScope 属性是经过计算得出的,有了如此方便的初始化方式,使得代码更加优雅灵活。

建议20:使用泛型集合代替非泛型集合

这个建议老生长谈了,尽量不要使用ArrayList,而是应该使用List<T> ,关于装箱拆箱的,不多说了,相信看过以上建议的朋友都比较熟了。

   
2476 次浏览       15
相关文章

深度解析:清理烂代码
如何编写出拥抱变化的代码
重构-使代码更简洁优美
团队项目开发"编码规范"系列文章
相关文档

重构-改善既有代码的设计
软件重构v2
代码整洁之道
高质量编程规范
相关课程

基于HTML5客户端、Web端的应用开发
HTML 5+CSS 开发
嵌入式C高质量编程
C++高级编程
最新课程计划
信息架构建模(基于UML+EA)3-21[北京]
软件架构设计师 3-21[北京]
图数据库与知识图谱 3-25[北京]
业务架构设计 4-11[北京]
SysML和EA系统设计与建模 4-22[北京]
DoDAF规范、模型与实例 5-23[北京]

WEB应用程序UI模版代码编写
C# 编码规范和编程好习惯
什么是防御性编程
善于防守-健壮代码的防御性
Visual C++编程命名规则
JavaScript程序编码规范
更多...   


设计模式原理与应用
从需求过渡到设计
软件设计原理与实践
如何编写高质量代码
单元测试、重构及持续集成
软件开发过程指南


某全球知名通信公司 代码整洁
横河电机 如何编写高质量代码
某知名金融软件服务商 代码评审
东软集团 代码重构
某金融软件服务商 技术文档
中达电通 设计模式原理与实践
法国电信 技术文档编写与管理
更多...