改进C#代码的157个建议总结

建议1: 正确操作字符串

字符串应该是所有编程语言中使用最频繁的一种基础数据类型。如果使用不慎,我们就会为一次字符串的操作所带来的额外性能开销而付出代价。本条建议将从两个方面来探讨如何规避这类性能开销:

1.确保尽量少的装箱
2.避免分配额外的内存空间

在这里插入图片描述
第一个会使用装箱完成int 到string 的转换
第二个通过使用ToString() 在内存中操作完成转换

在使用其他值引用类型到字符串的转换并完成拼接时,应当避免使用操作符“+”来完成,而应该使用值引用类型提供的ToString方法。

在自己编写的代码中,应当尽可能地避免编写不必要的装箱代码。注意装箱之所以会带来性能损耗,因为它需要完成下面三个步骤:
1)首先,会为值类型在托管堆中分配内存。除了值类型本身所分配的内存外,内存总量还要加上类型对象指针和同步块索引所占用的内存。
2)将值类型的值复制到新分配的堆内存中。
3)返回已经成为引用类型的对象的地址。

第二个方面:避免分配额外的内存空间。对CLR来说,string对象(字符串对象)是个很特殊的对象,它一旦被赋值就不可改变。在运行时调用System.String类中的任何方法或进行任何运算(如“=”赋值、“+”拼接等),都会在内存中创建一个新的字符串对象,这也意味着要为该新对象分配新的内存空间
在这里插入图片描述
**由于使用System.String类会在某些场合带来明显的性能损耗,所以微软另外提供了一个类型StringBuilder来弥补String的不足。StringBuilder并不会重新创建一个string对象,它的效率源于预先以非托管的方式分配内存。如果StringBuilder没有先定义长度,则默认分配的长度为16。当StringBuilder字符长度小于等于16时,StringBuilder不会重新分配内存;当StringBuilder字符长度大于16 小于32时,StringBuilder又会重新分配内存,使之成为16的倍数。在上面的代码中,如果预先判断字符串的长度将大于16,则可以为其设定一个更加合适的长度(如32)。StringBuilder重新分配内存时是按照上次的容量加倍进行分配的。当然,我们需要注意,StringBuilder指定的长度要合适,太小了,需要频繁分配内存;太大了,浪费空间。
在这里插入图片描述
**
微软还提供了另外一个方法来简化这种操作,即使用string.Format方法。string. Format方法在内部使用StringBuilder进行字符串的格式化,
在这里插入图片描述
建议2: 使用默认转型方法
除了字符串操作外,程序员普遍会遇到的第二个问题是:如何正确地对类型实现转型
1,使用类型的转换运算符
2,使用类型内置的Parse、TryParse,或者如ToString、ToDouble和ToDateTime等方法。
3 ,使用帮助类提供的方法
4 ,使用CLR支持的转型

1. 使用类型的转换运算符
转换运算符分为两类:隐式转换显式转换(强制转换)
基元类型普遍都提供了转换运算符
在这里插入图片描述
基元类型包括sbyte、byte、short、ushort、int、uint、long、ulong、char、float、double、bool、decimal、object、string。
2. 使用类型内置的Parse、TryParse,或者如ToString、ToDouble、ToDateTime等方法
在FCL中,如果某个类型经常需要进行转型操作,类型自身则会带有一些转型方法。比如从string转型为int,因为其经常发生,所以int本身就提供了Parse和TryParse方法
3. 使用帮助类提供的方法
可以使用如System.Convert类、System.BitConverter类来进行类型的转换。
System.Convert提供了将一个基元类型转换为其他基元类型的方法,如ToChar、ToBoolean方法等。值得注意的是,System.Convert还支持将任何自定义类型转换为任何基元类型,只要自定义类型继承了IConvertible接口就可以。如上文中的Ip类,如果将Ip转型为string,除了重写Object的ToString方法外,还可以实现IConvertible的ToString方法
继承IConvertible接口必须同时实现其他转型方法,如上文中的ToBoolean,如果不支持此类转型,则应该抛出一个InvalidCastException,而不是一个NotImplementedException。
4. 使用CLR支持的转型
在这里插入图片描述
建议3: 区别对待强制转型与as和is
强制转型可能意味着两件不同的事情:
1)FirstType和SecondType彼此依靠转换操作符来完成两个类型之间的转型。
2)FirstType是SecondType的基类
如果类型之间都上溯到了某个共同的基类(Object),那么根据此基类进行的转型(即基类转型为子类本身)应该使用as。子类与子类之间的转型,则应该提供转换操作符,以便进行强制转型。
as操作符永远不会抛出异常,如果类型不匹配(被转换对象的运行时类型既不是所转换的目标类型,也不是其派生类型),或者转型的源对象为null,那么转型之后的值也为null。改造前的DoWithSomeType方法会因为引发异常带来效率问题,而使用as后,就可以完美地避免这种问题。
FirstType是SecondType的基类。在这种情况下,既可以使用强制转型,也可以使用as操作符
在这里插入图片描述
从效率的角度来看,也建议大家使用as操作符。
as操作符有一个问题,即它不能操作基元类型。如果涉及基元类型的算法,就需要通过is转型前的类型来进行判断,以避免转型失败。

在这里插入图片描述
建议4: TryParse比Parse好
在这里插入图片描述
两者最大的区别是,如果字符串格式不满足转换的要求,Parse方法将会引发一个异常;TryParse方法则不会引发异常,它会返回false,同时将result置为0。
要注意,引发异常这个过程会对性能造成损耗
不建议为所有的类型都提供TryParse模式,只有在考虑到Do方法会带来明显的性能损耗时,才建议使用TryParse
建议5: 使用int?来确保值类型也可以为null
1)数据库中一个int字段可以被设置为null。在C#中,值被取出来后,为了将它赋值给int类型,不得不首先判断一下它是否为null。如果将null直接赋值给int类型,会引发异常。
2)在一个分布式系统中,服务器需要接收并解析来自于客户端的数据。一个int型数据可能在传输过程中丢失或被篡改了,转型失败后应该保存为null值,而不是提供一个初始值。
从.NET 2.0开始,FCL中提供了一个额外的类型:可以为空的类型Nullable。它是一个结构体,声明如下:
在这里插入图片描述
因为是结构体,所以只有值引用类型才可以作为“可以为空的类型”(引用类型本身就可以为null)。一个可以为空的int类型表示为:
在这里插入图片描述
语法T?是Nullable的简写,两者可以相互转换。可以为null的类型表示其基础值类型正常范围内的值再加上一个null值。例如,Nullable,其值的范围为-2147 483648~2147 483647,再加上一个null值。
现在来看看可空类型和基元类型的互相转换。基元类型提供了其对应的可空类型的隐式转换,如下所示:
在这里插入图片描述
反过来,可空类型不可隐式转换为对应的基元类型,正确的转换形式如下:
在这里插入图片描述
但是,这段代码看上去是不是有点烦琐?所以,在阐述可空类型的时候,不得不提到??运算符。??最大的用处就是将可空类型的值赋值给对应的基元类型进行简化,上文代码的一个简化形式就是:
在这里插入图片描述
int j = i ?? 0;表示的意思是,如果i的HasValue为true,则将i的value赋值给j;否则,就给j赋值为0。
建议6: 区别readonly和const的使用方法
在我看来,要使用const的理由只有一个,那就是效率。但是,在大部分应用情况下,“效率”并没有那么高的地位,所以我更愿意采用readonly,因为readonly赋予代码更多的灵活性。
1,const是一个编译期常量,readonly是一个运行时常量
2,const只能修饰基元类型、枚举类型或字符串类型,readonly没有限制。

因为const是编译期常量,所以它天然就是static的,不能手动再为const增加一个static修饰符
在这里插入图片描述
readonly变量是运行时变量,其赋值行为发生在运行时。readonly的全部意义在于,它在运行时第一次被赋值后将不可以改变。当然,“不可以改变”分为两层意思:
*1)对于值类型变量,值本身不可改变(readonly,只读)。
2)对于引用类型变量,引用本身(相当于指针)不可改变。

引用本身不可改变,引用所指的实例的值,却是可以改变的,下面的代码将会被允许:*
在这里插入图片描述

在这里插入图片描述
ReadOnlyValue首先在初始值指定项(也称为初始化器)中被赋值为100,后来,在构造方法中又被赋值为200。实际上,应该把初始化器理解成构造方法的一部分,它其实是一个语法糖。在构造方法内,我们确实可以多次对readonly赋值。
在这里插入图片描述
建议7: 将0值作为枚举的默认值
应该始终将0值作为枚举类型的默认值
在枚举类型Week中,可以将显式为元素赋值去掉,编译器会自动从0值开始计数,然后逐个为元素的值+1。
建议8: 避免给枚举类型的元素提供显式的值
一般情况下,没有必要给枚举类型的元素提供显式的值。创建枚举的理由之一,就是为了代替使用实际的数值。不正确地为枚举类型的元素设定显式的值,会带来意想不到的错误。

枚举元素允许设定重复的值
注意本建议也有例外。例如,当为一个枚举类型指定System.FlagsAttribute属性时,就意味着可以对这些值执行AND、OR、NOT和XOR按位运算,这样一来,就要求枚举的每个元素的值都是2 的若干次幂,指数依次递增
在这里插入图片描述
建议9: 习惯重载运算符

CLR支持在类型中,通过使用operator关键字定义静态成员函数来重载运算符,让开发人员可以像使用内置基元类型一样使用该类型。Salary重载“+”运算符的版本看起来应该像以下形式:
在这里插入图片描述
建议10: 创建对象时需要考虑是否实现比较器
当创建List 时 想使用List 里的sort 方法 ,默认会先看list 里面元素类型里面有没有排序 重写方法
在这里插入图片描述
上面代码中CompareTo方法有一条注释的代码,其实本方法完全可以使用该注释代码代替,因为利用了整型的默认比较方法。实现了接口IComparable后,我们就可以根据BaseSalary对Salary进行排序了

在这里插入图片描述
现在,问题来了:如果不想以基本工资BaseSalary进行排序,而是以奖金Bonus进行排序,该如何处理呢?这个时候,接口IComparer的作用就体现出来了,可以使用IComparer来实现一个自定义的比较器。如下所示:
在这里插入图片描述
在这里插入图片描述
如果我们稍有经验,就会发现上面的代码使用了一个已经不建议使用的集合类ArrayList(当泛型出来后,就建议尽量不使用所有非泛型集合类)。至于原因,从上面的代码中我们也可以看出端倪。
在这里插入图片描述
以上代码中的ArrayList,应该换成List,对应地,我们就该实现IComparable和IComparer

在这里插入图片描述
在这里插入图片描述
建议11: 区别对待==和Equals

无论是操作符“==”还是方法“Equals”,都倾向于表达这样一个原则:
对于值类型,如果类型的值相等,就应该返回True。
对于引用类型,如果类型指向同一个对象,则返回True。

,无论是操作符“”还是“Equals”方法都是可以被重载的。比如,对于string这样一个特殊的引用类型,微软觉得它的现实意义更接近于值类型,所以,在FCL中,string的比较被重载为针对“类型的值”的比较,而不是针对“引用本身”的比较
重载类的比较方法
在这里插入图片描述
在这里插入图片描述
这里,再引出操作符“
”和“Equals”方法之间的一点区别。一般来说,对于引用类型,我们要定义“值相等性”,应该仅仅去重载Equals方法,同时让“”表示“引用相等性”。注意由于操作符“”和“Equals”方法从语法实现上来说,都可以被重载为表示“值相等性”和“引用相等性”。所以,为了明确有一种方法肯定比较的是“引用相等性”,FCL中提供了Object. ReferenceEquals方法。该方法比较的是:两个示例是否是同一个示例。
建议12: 重写Equals时也要重写GetHashCode
除非考虑到自定义类型会被用作基于散列的集合的键值;否则,不建议重写Equals方法,因为这会带来一系列的问题。如果编译上一个建议中的Person这个类型,编译器会提示这样一个信息:
在这里插入图片描述
建议13: 为类型输出格式化字符串

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值