前言

  本文主要来学习记录前三个建议。

  建议1、正确操作字符串

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

  建议3、区别对待强制转换与as和is

其中有很多需要理解的东西,有些地方可能理解的不太到位,还望指正。

建议1、正确操作字符串

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

  1、确保尽量少的装箱

  2、避免分配额外的内存空间

先来介绍第一个方面,请看下面的两行代码:

String str1="str1"+9;
String str2="str2"+9.ToString();

从IL代码可以得知,第一行代码在运行时完成一次装箱的行为,而第二行代码中并没有发生装箱的行为,它实际调用的是整型的ToString()方法,效率要比装箱高。所以,在使用其他值引用类型到字符串的转换并完成拼接时,应当避免使用操作符“+”来完成,而应该使用值引用类型提供的ToString()方法。

第二方面,避免分配额外的内存空间。对CLR来说,string对象(字符串对象)是个很特殊的对象,它一旦被赋值就不可改变。在运行时调用System.String类中的任何方法或进行任何运算(如“=”赋值、“+”拼接等),都会在内存中创建一个新的字符串对象,这也意味着要为该新对象分配新的内存空间。像下面的代码就会带来运行时的额外开销。

private static void NewMethod1()
{
string s1="abc";
s1="123"+s1+"456"; ////以上两行代码创建了3个字符串对象对象,并执行了一次string.Contact方法
} private static void NewMethod2()
{
string re=9+"456"; ////该方法发生了一次装箱,并调用一次string.Contact方法
}

关于装箱拆箱的问题大家可以查看我之前的文章http://www.cnblogs.com/aehyok/p/3504449.html

而以下代码,字符串不会在运行时进行拼接,而是会在编译时直接生成一个字符串。

private static void NewMethod3()
{
string re2="123"+"abc"+"456"; ///该代码等效于///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 = 0;
float j = 0;
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]的更多相关文章

  1. C#程序编写高质量代码改善的157个建议【20-22】[泛型集合、选择集合、集合的安全]

    建议20.使用泛型集合来替代非泛型集合 http://www.cnblogs.com/aehyok/p/3384637.html 这里有一篇文章,是我之前专门来介绍泛型的.我们应尽量的使用泛型集合.因 ...

  2. C#程序编写高质量代码改善的157个建议【16-19】[动态数组、循环遍历、对象集合初始化]

    前言   软件开发过程中,不可避免会用到集合,C#中的集合表现为数组和若干集合类.不管是数组还是集合类,它们都有各自的优缺点.如何使用好集合是我们在开发过程中必须掌握的技巧.不要小看这些技巧,一旦在开 ...

  3. C#程序编写高质量代码改善的157个建议【13-15】[为类型输出格式化字符串、实现浅拷贝和深拷贝、用dynamic来优化反射]

    前言 本文已更新至http://www.cnblogs.com/aehyok/p/3624579.html .本文主要学习记录以下内容: 建议13.为类型输出格式化字符串 建议14.正确实现浅拷贝和深 ...

  4. C#程序编写高质量代码改善的157个建议【10-12】[创建对象时需要考虑是否实现比较器、区别对待==和Equals]

    前言 建议10.创建对象时需要考虑是否实现比较器 建议11.区别对待==和Equals 建议12.重写Equals时也要重写GetHashCode 建议10.创建对象时需要考虑是否实现比较器 有对象的 ...

  5. C#程序编写高质量代码改善的157个建议【4-9】[TryParse比Parse、使用int?来确保值类型也可以为null、readonly和const、0值设为枚举的默认值、避免给枚举类型的元素提供显式的值、习惯重载运算符]

    建议4.TryParse比Parse好 如果注意观察,除string之外的所有的基元类型.会发现它们都有两个将字符串转换为自身类型的方法:Parse和TryParse.以类型double为例. 两者最 ...

  6. 博友的 编写高质量代码 改善java程序的151个建议

    编写高质量代码 改善java程序的151个建议 http://www.cnblogs.com/selene/category/876189.html

  7. 编写高质量代码--改善python程序的建议(六)

    原文发表在我的博客主页,转载请注明出处! 建议二十八:区别对待可变对象和不可变对象 python中一切皆对象,每一个对象都有一个唯一的标识符(id()).类型(type())以及值,对象根据其值能否修 ...

  8. 编写高质量代码--改善python程序的建议(八)

    原文发表在我的博客主页,转载请注明出处! 建议四十一:一般情况下使用ElementTree解析XML python中解析XML文件最广为人知的两个模块是xml.dom.minidom和xml.sax, ...

  9. 编写高质量代码改善java程序的151个建议——导航开篇

    2014-05-16 09:08 by Jeff Li 前言 系列文章:[传送门] 下个星期度过这几天的奋战,会抓紧java的进阶学习.听过一句话,大哥说过,你一个月前的代码去看下,慘不忍睹是吧.确实 ...

随机推荐

  1. 那些Java架构师必知必会的技术

    Java基础 Java 7 和 Java 8 中的 HashMap原理解析 Java7 和 Java8 中的 ConcurrentHashMap 原理解析 Java中自定义注解 Java函数式编程和l ...

  2. python 类内部装饰器的实现 与 参数解构学习

    学习了函数的装饰器的写法,然后想到如果要在类中初始化或获取信息时能用装饰器做过滤和验证应该怎么写呢, 在网上查了下相关信息,感觉这样也是可以的,不知道会不会有什么问题class Ctj(): clas ...

  3. mysql group by分组查询后 查询个数

    mysql group by分组查询后 查询个数2个方法随便你选 <pre>select count(distinct colA) from table1;</pre>< ...

  4. 来了!GitHub for mobile 发布!iOS beta 版已来,Android 版即将发布

    北京时间 2019 年 11 月 14 日,在 GitHub Universe 2019大会上,GitHub 正式发布了 GitHub for mobile,支持 iOS 与 Android 两大移动 ...

  5. JavaScript闭包使用姿势指南

    目录 JavaScript闭包使用姿势指南 引言 闭包的定义 来个有趣的例子吧 现在我们换个例子吧 我们再来个有趣的例子 内存泄露问题 用闭包解决递归调用问题 用闭包模拟私有方法 在循环中使用闭包 性 ...

  6. map集合中哪些是线程安全的

    为什么HashMap是线程不安全的 总说 HashMap 是线程不安全的,不安全的,不安全的,那么到底为什么它是线程不安全的呢?要回答这个问题就要先来简单了解一下 HashMap 源码中的使用的存储结 ...

  7. vue 前端处理监听关键字搜索

    根据组件的业务需要,有时候搜索是把关键字返回给后台,后台处理后再把数据返回给前端渲染(多次请求服务器):有时候是前端把页面的数据全部获取下来,前端处理关键字的搜索(影响页面加载) 我这个文章是介绍第二 ...

  8. nyoj 122-Triangular Sums (数学之读懂求和公式的迭代)

    122-Triangular Sums 内存限制:64MB 时间限制:3000ms 特判: No 通过数:5 提交数:7 难度:2 题目描述: The nth Triangular number, T ...

  9. 领扣(LeetCode)错误的集合 个人题解

    集合 S 包含从1到 n 的整数.不幸的是,因为数据错误,导致集合里面某一个元素复制了成了集合里面的另外一个元素的值,导致集合丢失了一个整数并且有一个元素重复. 给定一个数组 nums 代表了集合 S ...

  10. 阿里云ECS服务器部署HADOOP集群(三):ZooKeeper 完全分布式集群搭建

    本篇将在阿里云ECS服务器部署HADOOP集群(一):Hadoop完全分布式集群环境搭建的基础上搭建,多添加了一个 datanode 节点 . 1 节点环境介绍: 1.1 环境介绍: 服务器:三台阿里 ...