编写高质量代码改善C#程序的157个建议[1-3]
前言
本文主要来学习记录前三个建议。
建议1、正确操作字符串
建议2、使用默认转型方法
建议3、区别对待强制转换与as和is
其中有很多需要理解的东西,有些地方可能理解的不太到位,还望指正。
建议1、正确操作字符串
字符串应该是所有编程语言中使用最频繁的一种基础数据类型。如果使用不慎,我们就会为一次字符串的操作所带来的额外性能开销而付出代价。本条建议将从两个方面来探讨如何规避这类性能开销:
1、确保尽量少的装箱
2、避免分配额外的内存空间
先来介绍第一个方面,请看下面的两行代码:
- String str1="str1"+;
- String str2="str2"+.ToString();
从IL代码可以得知,第一行代码在运行时完成一次装箱的行为,而第二行代码中并没有发生装箱的行为,它实际调用的是整型的ToString()方法,效率要比装箱高。所以,在使用其他值引用类型到字符串的转换并完成拼接时,应当避免使用操作符“+”来完成,而应该使用值引用类型提供的ToString()方法。
第二方面,避免分配额外的内存空间。对CLR来说,string对象(字符串对象)是个很特殊的对象,它一旦被赋值就不可改变。在运行时调用System.String类中的任何方法或进行任何运算(如“=”赋值、“+”拼接等),都会在内存中创建一个新的字符串对象,这也意味着要为该新对象分配新的内存空间。像下面的代码就会带来运行时的额外开销。
- private static void NewMethod1()
- {
- string s1="abc";
- s1=""+s1+""; ////以上两行代码创建了3个字符串对象对象,并执行了一次string.Contact方法
- }
- private static void NewMethod2()
- {
- string re=+""; ////该方法发生了一次装箱,并调用一次string.Contact方法
- }
关于装箱拆箱的问题大家可以查看我之前的文章http://www.cnblogs.com/aehyok/p/3504449.html
而以下代码,字符串不会在运行时进行拼接,而是会在编译时直接生成一个字符串。
- private static void NewMethod3()
- {
- string re2=""+"abc"+""; ///该代码等效于///string re2="123abc456";
- }
- private static void NewMethod4()
- {
- const string a="t";
- string re="abc"+a; ///因为a是一个常量,所以该代码等效于string=re="abc"+"t"; 最终等效于string re="abct";
- }
由于使用System.String类会在某些场合带来明显的性能损耗,所以微软另外提供了一个类型StringBuilder来弥补String的不足。
StringBuilder并不会重新创建一个string对象,它的效率源于预先以非托管的方式分配内存。如果StringBuilder没有先定义长度,则默认分配的长度为16。当StringBuilder字符串长度小于等于16时,StringBuilder不会重新分配内存;当StringBuilder字符长度大于16小于32时,StringBuilder又会重新分配内存,使之成为16的倍数。在上面的代码中,如果预先判断字符串的长度将大于16,则可以为其设定一个更加合适的长度(如32)。StringBuilder重新分配内存时是按照上次容量加倍进行分配的。当然,我们需要注意,StringBuilder指定的长度要合适,太小了,需要频繁分配内存,太大了,浪费空间。
查看以下代码,比较下面两种字符串拼接方式,哪种效率更高:
- private static void NewMethod1()
- {
- string a = "t";
- a += "e";
- a += "s";
- a += "t";
- }
- private static void NewMethod2()
- {
- string a = "t";
- string b = "e";
- string c = "s";
- string d = "t";
- string result = a + b + c + d;
- }
结果可以得知:两者的效率都不高。不要以为前者比后者创建的字符串对象更少,事实上,两者创建的字符串对象相等,且前者进行了3次string.Contact方法调用,比后者还多了两次。
要完成这样的运行时字符串拼接(注意:是运行时),更佳的做法是使用StringBuilder类型,代码如下所示:
- public static void NewMethod()
- {
- ////定义了四个变量
- string a = "t";
- string b = "e";
- string c = "s";
- string d = "t";
- StringBuilder sb = new StringBuilder(a);
- sb.Append(b);
- sb.Append(c);
- sb.Append(d);
- ///提示是运行时,所以没有使用以下代码
- //StringBuilder sb = new StringBuilder("t");
- //sb.Append("e");
- //sb.Append("s");
- //sb.Append("t");
- //string result = sb.ToString();
- }
微软还提供了另外一个方法来简化这种操作,即使用string.Format方法。string.Format方法在内部使用StringBuilder进行字符串的格式化,代码如下所示:
- public static void NewMethod4()
- {
- string a = "t";
- string b = "e";
- string c = "s";
- string d = "t";
- string result = string.Format("{0}{1}{2}{3}", a, b, c, d);
- }
对于String和StringBuilder的简单介绍也可以参考我之前的一篇文章http://www.cnblogs.com/aehyok/p/3505000.html
建议2、使用默认转型方法
1、使用类型的转换运算符,其实就是使用类型内部的一方方法(即函数)。转换运算符分为两类:隐式转换和显式转换(强制转换)。基元类型普遍都提供了转换运算符。
所谓“基元类型”,是指编译器直接支持的数据类型。基元类型包括:sbyte、byte、short、ushort、int、uint、long、ulong、char、float、double、bool、decimal、object、string。
- int i = ;
- float j = ;
- j = i; ///int 到float存在一个隐式转换
- i = (int)j; ///float到int必须存在一个显式转换
用户自定义的类型也可以通过重载转换运算符的方式提供这一类转换:
- public class Ip
- {
- 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();
- }
- }
- class Program
- {
- public static void Main(string[] args)
- {
- Ip ip = "192.168.1.1"; //通过Ip类的重载转换运算符,实现字符串到Ip类型的隐式转换
- Console.WriteLine(ip.ToString());
- Console.ReadLine();
- }
- }
提供的就是字符串到类型Ip之间的隐式转换。
2、使用类型内置的Parse、TryParse,或者如ToString、ToDouble、ToDateTime等方法
比如从string转换为int,因为其经常发生,所以int本身就提供了Parse和TryParse方法。一般情况下,如果要对某类型进行转换操作,建议先查阅该类型的API文档。
3、使用帮助类提供的方法
可以使用System.Convert类、System.BitConverter类来进行类型的转换。
System.Convert提供了将一个基元类型转换为其他基元类型的方法,如ToChar、ToBoolean方法等。值得注意的是,System.Convert还支持将任何自定义类型转换为任何基元类型,只要自定义类型继承了IConvertible接口就可以。如上文中的IP类,如果将Ip转换为string,除了重写Object的ToString方法外,还可以实现IConvertible的ToString()方法
继承IConvertible接口必须同时实现其他转型方法,如上文的ToBoolean、ToByte,如果不支持此类转型,则应该抛出一个InvalidCastException,而不是一个NotImplementedException。
4、使用CLR支持的转型
CLR支持的转型,即上溯转型和下溯转型。这个概念首先是在Java中提出来的,实际上就是基类和子类之间的相互转换。
就比如: 动作Animal类、Dog类继承Animal类、Cat类也继承自Amimal类。在进行子类向基类转型的时候支持隐式转换,如Dog显然就是一个Animal;而当Animal转型为Dog的时候,必须是显式转换,因为Animal还可能是一个Cat。
- Animal animal = new Animal();
- Dog dog = new Dog();
- animal = dog; /////隐式转换,因为Dog就是Animal
- ///dog=animal; ////编译不通过
- dog = (dog)animal; /////必须存在一个显式转换
建议3、区别对待强制转换与as和is
首先来看一个简单的实例
- FirstType firstType = new FirstType();
- SecondType secondType = new SecondType();
- secondType = (SecondType)firstType;
从上面的三行代码可以看出,类似上面的应该就是强制转换。
首先需要明确强制转换可能意味这两件不同的事情:
1、FirstType和SecondType彼此依靠转换操作来完成两个类型之间的转换。
2、FirstType是SecondType的基类。
类型之间如果存在强制转换,那么它们之间的关系要么是第一种,要么是第二种。不可能同时是继承的关系,又提供了转型符。
针对第一种情况:
- public class FirstType
- {
- public string Name { get; set; }
- }
- public class SecondType
- {
- public string Name { get; set; }
- public static explicit operator SecondType(FirstType firstType)
- {
- SecondType secondType = new SecondType() { Name = "转型自:" + firstType.Name };
- return secondType;
- }
- }
- class Program
- {
- static void Main(string[] args)
- {
- FirstType firstType = new FirstType() { Name="First Type"};
- SecondType secondType = (SecondType)firstType; ///此转换是成功的
- secondType = firstType as SecondType; ///编译不通过
- Console.ReadLine();
- }
- }
这里上面也有添加注释,通过强制转换是可以转换成功的,但是使用as运算符是不成功的编译就不通过。
这里就是通过转换符进行处理的结果。
接下来我们再在Program类中添加一个方法
- static void DoWithSomeType(object obj)
- {
- ///编译器首先判断的是,SeondType和ojbect之间有没有继承关系。
- ///因为在C#中,所有的类型都是继承自object的,所以这里编译没有什么问题。
- ///但编译器会自动产生代码来检查obj在运行时是不是SecondType,这样就绕过了操作转换符,导致转换失败。
- SecondType secondType = (SecondType)obj;
- }
如注释所说的,编译通过执行报错的问题。
如果类型之间都上溯到了某个共同的基类,那么根据此基类进行的转换(即基类转型为子类本身),应该使用as。子类与子类之间的转换,则应该提供转换操作符,以便进行强制转换。
现在可以如上方法改写为
- static void DoWithSomeType(object obj)
- {
- SecondType secondType = obj as SecondType;
- }
保证编译执行都不会报错。as操作符永远不会抛出异常,如果类型不匹配(被转换对象的运行时类型既不是所转换的目标类型,也不是其派生类型),或者转型的源对象为null,那么转型之后的值也为null。改造前的DoWithSomeType方法会因为引发异常带来效率问题,而使用as后,就可以完美的避免这种问题。
现在来看第二种情况,即FirstType是SecondType的基类。这种情况下,既可以使用强制转型又可以使用as操作符。
- public class FirstType
- {
- public string Name { get; set; }
- }
- public class SecondType : FirstType
- {
- }
- class Program
- {
- static void Main(string[] args)
- {
- SecondType secondType = new SecondType() { Name="aehyok"};
- FirstType firstType = (FirstType)secondType;
- firstType = secondType as FirstType;
- Console.ReadLine();
- }
- }
但是,即使可以使用强制转型,从效率的角度来看,也建议大家使用as进行转型。
下面再来看一下is操作符。
- static void DoWithSomeType(object obj)
- {
- if (obj is SecondType)
- {
- SecondType secondType = obj as SecondType;
- }
- }
这个版本的效率显然没有上一个版本的效率高。因为当前这个版本进行了两次类型检测。但是,as操作符有个问题,就是它不能操作基元类型。如果涉及到基元类型的算法,那么就要使用is进行判断之后再进行转型的操作,以避免转型失败。
编写高质量代码改善C#程序的157个建议[1-3]的更多相关文章
- 读书--编写高质量代码 改善C#程序的157个建议
最近读了陆敏技写的一本书<<编写高质量代码 改善C#程序的157个建议>>书写的很好.我还看了他的博客http://www.cnblogs.com/luminji . 前面部 ...
- 编写高质量代码改善C#程序的157个建议——建议157:从写第一个界面开始,就进行自动化测试
建议157:从写第一个界面开始,就进行自动化测试 如果说单元测试是白盒测试,那么自动化测试就是黑盒测试.黑盒测试要求捕捉界面上的控件句柄,并对其进行编码,以达到模拟人工操作的目的.具体的自动化测试请学 ...
- 编写高质量代码改善C#程序的157个建议——建议156:利用特性为应用程序提供多个版本
建议156:利用特性为应用程序提供多个版本 基于如下理由,需要为应用程序提供多个版本: 应用程序有体验版和完整功能版. 应用程序在迭代过程中需要屏蔽一些不成熟的功能. 假设我们的应用程序共有两类功能: ...
- 编写高质量代码改善C#程序的157个建议——建议155:随生产代码一起提交单元测试代码
建议155:随生产代码一起提交单元测试代码 首先提出一个问题:我们害怕修改代码吗?是否曾经无数次面对乱糟糟的代码,下决心进行重构,然后在一个月后的某个周一,却收到来自测试版的报告:新的版本,没有之前的 ...
- 编写高质量代码改善C#程序的157个建议——建议154:不要过度设计,在敏捷中体会重构的乐趣
建议154:不要过度设计,在敏捷中体会重构的乐趣 有时候,我们不得不随时更改软件的设计: 如果项目是针对某个大型机构的,不同级别的软件使用者,会提出不同的需求,或者随着关键岗位人员的更替,需求也会随个 ...
- 编写高质量代码改善C#程序的157个建议——建议153:若抛出异常,则必须要注释
建议153:若抛出异常,则必须要注释 有一种必须加注释的场景,即使异常.如果API抛出异常,则必须给出注释.调用者必须通过注释才能知道如何处理那些专有的异常.通常,即便良好的命名也不可能告诉我们方法会 ...
- 编写高质量代码改善C#程序的157个建议——建议152:最少,甚至是不要注释
建议152:最少,甚至是不要注释 以往,我们在代码中不写上几行注释,就会被认为是钟不负责任的态度.现在,这种观点正在改变.试想,如果我们所有的命名全部采用有意义的单词或词组,注释还有多少存在的价值. ...
- 编写高质量代码改善C#程序的157个建议——建议151:使用事件访问器替换公开的事件成员变量
建议151:使用事件访问器替换公开的事件成员变量 事件访问器包含两部分内容:添加访问器和删除访问器.如果涉及公开的事件字段,应该始终使用事件访问器.代码如下所示: class SampleClass ...
- 编写高质量代码改善C#程序的157个建议——建议150:使用匿名方法、Lambda表达式代替方法
建议150:使用匿名方法.Lambda表达式代替方法 方法体如果过小(如小于3行),专门为此定义一个方法就会显得过于繁琐.比如: static void SampeMethod() { List< ...
随机推荐
- BZOJ 3172([Tjoi2013]单词-后缀数组第一题+RMQ)
3172: [Tjoi2013]单词 Time Limit: 10 Sec Memory Limit: 512 MB Submit: 268 Solved: 145 [ Submit][ St ...
- 使用cm-12.0源代码编译twrp
Select the newest branch available. This step is not necessary with Omni because Omni already includ ...
- Android Bundle类别
即使在今天发现自己Bundle类不明确,因此,花时间去研究了一下. 依据google官方文件(http://developer.android.com/reference/android/os/Bun ...
- 非正确使用浮点数据由项目产生BUG讨论的问题
乘分配 当小学学会了乘法分配.详细乘法分配:并与多个两个数相乘的,他们能够把这个数字乘以,然后加入.由于一个恒定.乘法分配律也能够使用表达式的定义"(a+b)×c = a×c+b×c&quo ...
- Java没有源代码的同步集合~
存档~~~~ Synchronized,volatile,lock 锁定->[就绪队列,阻塞队列] 第一个是jvm等级达到. 与volatile所不同的是,后者不能从工作存储器写回到主存储器. ...
- base 64 编解码器
base 64 编解码 1. base64的编码都是按字符串长度,以每3个8bit的字符为一组, 2. 然后针对每组.首先获取每一个字符的ASCII编码. 3. 然后将ASCII编码转换成8bit的二 ...
- unity3d脚本
一 创建和使用脚本 1 概述 GameObject的行为都是被附加到其上面的组件控制,脚本本质上也是一个组件. 在unity中创建一个脚本.默认内容例如以下: using UnityEngine; u ...
- OpenCV+MFC显示图像
1.首先下载openCV. 2.安装OpenCV.现在的版本号2.4.9,你并不需要配置环境变量. 3.设置包括文件夹,设定project库文件夹. 4.配置链接库.注意,链接库包含了文件名中包含一个 ...
- webstorm创建nodejs + express + jade 的web 项目
webstorm创建nodejs + express + jade 的web 项目 前简单了解过nodejs,觉得用nodejs来做个网站也太麻烦了,要自己拼html的字符串返回,这能做网站嘛? 最近 ...
- 【应用篇】Activiti显示器(抽象)简单的应用程序和服务的颗粒结合(两)
Activiti简单的应用程序,业务颗粒与工作流程结合.让流程带动业务颗粒运行的过程.此次的监听我们应用抽象的监听来实现,也就是说全部的普通业务类均应用此抽象监听,而不须要每个类一个监听的来操作. 新 ...