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

开篇

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

章节索引

建议1:正确操做字符串拼接,避免Boxing小程序

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

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

建议4:TryParse比Parse好性能

建议5:使用int?确保值类型也能够为null学习

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

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

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

建议9:习惯重载运算符3d

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

 

 

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

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

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

一、使用类型的转换运算符
其实就是使用内部的一个方法,转换运算符分两类:隐式转换、显式转换(强制转换)基元类型广泛都提供了转换运算符。
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();
        }
    }

 

二、使用类型内置的Parse
在FCL中,类型自身会带有一些转换方法,好比int自己提供Parse、TryParse方法……
 
 
三、使用帮助类提供的方法
System.Convert提供将一个基元类型转换到其余基元类型的方法,如ToChar、ToBoolean等,若是是自定义类型转换为任何基元类型,只要自定义类型实现IConvertible接口而且实现相关的转换方法便可;
ps:基元类型是指编译器直接支持的数据类型,即直接映射到FCL中的类型,包括sbyte、byte、short、ushort、int、uint、long、ulong、char、float、double、bool、decimal、object、string。
四、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来举例说明:
一、新建一个类库,新建Person类,设置以下两个常量:
namespace ClassLibrary
{
    public class Person
    {
        public const int height = 100;
        public readonly static int weight = 100;
    }
}
二、在主程序中添加ClassLibrary类库的引用,输出常量:
protected void Page_Load(object sender, EventArgs e)
{
      Response.Write("身高:" + ClassLibrary.Person.height);
      Response.Write("体重:" + ClassLibrary.Person.weight);
}
此时毫无疑问的,输出结果为:身高:100体重:100,
 

三、修改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 
这种设计是利用了计算机基础中的二进制数的“与”“或”运算,从而能够巧妙的将各个元素组合起来成为一个数据,而且能最后拆分出来,这种设计思想能够普遍的应用在权限设计、收费方式……等须要多种数据组合的地方。
我再说说其中的原理吧,首先看我定义枚举的值,对应出来的二进制数为:
000一、00十、0100、1000 ……
举个例子:好比0x1和0x8组合,对应的二进制数是:000一、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;这是很是消耗性能的,泛型的出现,能够很好的避免类型转换的问题:
一、ArrayList可使用List<T>来代替
二、使用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);
        }
    }
相关文章
相关标签/搜索