编写高质量代码改善C#程序的157个建议[正确操作字符串、使用默认转型方法、却别对待强制转换与as和is]
前言
本文主要来学习记录前三个建议。
建议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个建议[正确操作字符串、使用默认转型方法、却别对待强制转换与as和is]的更多相关文章
- 编写高质量代码改善C#程序的157个建议[1-3]
原文:编写高质量代码改善C#程序的157个建议[1-3] 前言 本文主要来学习记录前三个建议. 建议1.正确操作字符串 建议2.使用默认转型方法 建议3.区别对待强制转换与as和is 其中有很多需要理 ...
- 读书--编写高质量代码 改善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 ...
随机推荐
- 汇编中call printf参数压栈时错误理解
EAX, ECX,EDX,EBX均可以32bit,16bit,8bit访问,如下所示: <-------------------EAX------------------------>|& ...
- java Annotation Demo
Java 1.5引入了annotation,这个功能非常好用,是用c#等语言借鉴过来的一个特性. 首先编译器本身支持一些像overrides,supresswarning之类的注解. Spring,j ...
- openstack排错
一.排错方法: 1.查看日志路径为/var/log,具体哪个组件出了问题进入其目录查看. 2.debug root@sc-ctrl01:~# keystone --debug user-list ro ...
- Java 嵌套解析 json
1.首先需要安装org.json.jar 2.类JSONObject用于创建一个json对象.其中的JSONObject.put(KEY, VALUE)用于向其中添加条目 3.JSONObject.g ...
- ios开发之CoreData使用
1.在工程中添加build Phases的Link Binary with Libraries下添加CoreData的库. 2.在当前工程中新建文件选择CoreData——>DataModel, ...
- Stanford机器学习笔记-1.线性回归
Content: 1. Linear Regression 1.1 Linear Regression with one variable 1.1.1 Gradient descent algorit ...
- Eclipse打开xml文件报校验错误解决办法
XML文件在Eclipse中报校验错误: The content of element type "web-app" must match "(icon?,display ...
- 分享一例脚本发版和tomcat重启脚本
线上有个网站业务部署在tomcat上,由于频繁上线修改,需要经常启动tomcat.tomcat服务自带的bin下没有重启脚本,下面分享一例脚本发版和tomcat重启脚本: 1)现将业务代码从svn里下 ...
- eval() 函数
eval() 函数可计算某个字符串,并执行其中的的 JavaScript 代码. var str = '12+45*45'; alert(eval(str))//计算结果 还有一个重要作用可以把字符串 ...
- Android Studio Jar、so、library项目依赖
Eclipse跟AS的不同 从Eclipse到AS不要带着在Eclipse中的主观色彩去在AS中使用,从项目的构成到构建是不同的,下面列举在Eclipse和AS中的一些概念的区别: WorkSpace ...