编写高质量代码改善C#程序的157个建议读书笔记【1-10】

开篇

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

章节索引

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

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

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

建议4:TryParse比Parse好

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

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

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

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

建议9:习惯重载运算符

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

 

 

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

1string str1 = "str1" + 9;
2string 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);
        }
    }

 


posted @ 2014-02-20 22:23  dotnetgeek  阅读(3786)  评论(20编辑  收藏  举报