前言:最近在看其他人写的旧项目代码的时候,发现有三个让我极其不习惯也隐隐感到不舒服的地方。一个是sql查询语句中出现 SELECT * 的概率极高,第二个是实体转换的时候竟然还要手写遍历和转换,最后一个就是他们在项目中使用了大面积的字符串拼接。前两个问题说明项目开发自动化做的不够,后一个就比较黑色幽默一点,再次证明字符串拼接是人民群众喜闻乐见的编程方式。对于代码完美主义者来说,大量的字符串拼接肯定是逃不过被重构的命运的,但是既然已经在项目中蔚为壮观地我存在你崇拜没有更好的办法了,咱也不能改动的太离谱。
1、乱弹StringJoiner的横空出世
为了这种字符串拼接,不论是C#还是Java都有对应的改进的类和方法。比如我们熟知的C#下的StringBuilder类,string的format方法,还有Java中不是也有StringBuffer类吗?但是实际开发中,很多人还是喜欢直接用string加过来加过去,replace也会适时出现几次,提升自己的曝光度。虽然写代码的人很省事,可读性严格来说也不是那么的差,而且运行起来性能也不是离谱的一无是处,完事之后说不定哪一天写代码的人就收拾金银细软走人了,大大小小若干项目都是这种代码可就……碰巧后来维护这种代码的人非常争气,想到了好的解决方案,捏住鼻子含怒重构了这种低效的代码之后,发扬无私共享的风格在园子里贴了出来造福大众,一不小心还成了“拯救那些性能低下的字符串拼装代码“的先驱,CoolCode,我说的对吗?
关于StringJoiner的前世今生,请参考CoolCode的大作:
2、基本方法还可以再添加几个,命名还可以再装腔作势一点
在我的好友CoolCode童鞋的原文中,他没有把所有源码都贴出来。我在项目中使用的时候觉得还可以添加几个常用的方法,比如Replace、Remove和Clear方法等等,因为它们的使用频率也很高。同时,我们还可以考虑到将这个类“常识化”,说不定哪天大家都觉得这个好使,接着大面积推广使用代替string或者StringBuilder了,没有可能吗?所以我们还可以把string的Format静态方法,StringBuilder的Append和AppendFormat实例方法也给它弄进去。
比如Format静态方法,我们可以像如下定义:
public static StringBuffer Format(string format, object arg0) { StringBuffer sb = new StringBuffer(); sb.builder.AppendFormat(format, arg0); return sb; }
而两个实例方法Append和AppendFormat,对于直接字符串拼接的重构很少用到,貌似没有必要写进去(这两个实例方法也不是毫无作为,看项目需要,可以注释掉该扩展方法,作者补充),幸好我们还有扩展方法:
/// <summary> /// StringBuffer的扩展 /// </summary> public static class StringBufferExtension { public static StringBuffer Append(this StringBuffer sb, string input) { sb.builder.Append(input); //sb += input; return sb; } public static StringBuffer Append(this StringBuffer sb, object input) { sb.builder.Append(input); //sb += input; return sb; } public static StringBuffer AppendFormat(this StringBuffer sb, string format, params object[] args) { sb.builder.AppendFormat(format, args); return sb; } }
关于这个StringJoiner的命名好像稍微也不是很贴近大众。上面不是提到Java中的StringBuffer类么,像这种出类拔萃的命名,除了CoolCode,谁还能割舍得下呢 ( ^_^)? 本文最后采用了StringBuffer类名,demo中可以看到,不是说StringJoiner就不好,老实说这是我见过的最本土化的命名之一。再次感谢CoolCode的无私贡献,实际项目开发和维护中这个类拯救我不是一次两次了。
最后,demo下载:StringBuffer
参考文章:
http://www.cnblogs.com/coolcode/archive/2009/10/13/StringJoiner.html
http://blog.zhaojie.me/2009/11/string-concat-perf-1-benchmark.html
http://blog.zhaojie.me/2009/12/string-concat-perf-3-profiling-analysis.html
附:务必小心OutOfMemory和StackOverFlow两种异常
这几天晚上我重新看<<CLR via C#>>关于异常和状态管理的章节。结合自己的经验,发现除了空引用、参数和索引越界等等常见异常之外,书中提到还应该注意到OutOfMemory和StackOverFlow两种异常,虽然这两种异常在实际项目中出现的概率微乎其微。
一、“内存不够用”
OutOfMemoryException异常,字面理解,就是超出内存额定容量而抛出的异常。为什么会超出内存额定容量呢?很简单,内存空间是有限的,但是我们分配内存的要求是无限的(好像是某名言)。
1、程序中一次性要往内存存放的数据过多
举例来说,我们每次去取数据库的数据,如果每次都取个几百万上千万的数据,普通PC通常情况下内存都不是很大(我用过的最大也就4G而已),如果取回来的数据在内存中存储的数据结构再复杂一点(比如带嵌套结构的字典、双向链表等等),在取回数据进行内存分配的时候,第一次分配就挂了,CLR二话不说就抛出了OutOfMemoryException异常。
2、或者表面上看上去是连续多次动态分配内存
这个过程我们可以直观地简单理解成(严格来讲是错误的,本质上真正引发异常的还是一次性分配内存,而剩余内存空间不足)把1次分配大数据量的内存拆分成多次分配小内存空间。比如下面的程序:
StringBuffer str = string.Empty; str += "hello"; str += " "; str += "world"; str += Environment.NewLine; for (int i = 0; i < 24; i++) { str += str; } Console.WriteLine(str);
我们利用上面介绍的StringBuffer类来进行字符串拼接。在for循环的时候,程序是以几何级数(2的n次幂)拼接字符串的。所以如果我们循环次数过多,很容就就出现内存不足的异常了。在本地测试的时候,我的电脑到循环24次就出现异常了。如果我们知道可变的(mutable)字符串StringBuilder是如何在托管堆上动态分配内存的,那么这里抛出异常就不难理解了。
二、”堆栈爆掉“
StackOverFlowException,字面理解就是”堆栈爆掉“。说起这个异常,大家很容易联想到递归,下面写一段简单代码重现这个异常:
class Example { private string name; public string Name { get { //return name; return Name; //这里造成递归调用 } set { name = value; } } public Example() { name = "stack over flow"; } static void Main() { Example obj = new Example(); Console.WriteLine(obj.Name); Console.Read(); } }
平时我们谈到递归,通常立刻会想到方法的递归调用。这个程序在输出Name属性(属性的本质其实也是方法,这点通过查看IL可以一窥全豹,因为我们知道MSIL中除了类,方法和字段,是没有属性的)的时候发生了递归调用。Name属性的值是通过在get内返回Name(实际上应该是返回name)属性来获取,这样就导致了Name属性的获取发生了无限递归调用(注意,这里所谓”无限“递归调用,字面理解好像是正确的,但是真正递归的层数和你电脑的内存以及CLR有关系,可以肯定不是随心所欲的”无限“了)。避开这种异常的最简单方法就是程序里尽可能地不使用递归(比如通过迭代方法),或者使用优化过了的递归(请参考老赵博客),而对于本文的递归,只要把属性写正确就行了。
参考:
Jeffrey Richter <<CLR via C#>>
http://blog.zhaojie.me/2009/03/tail-recursion-and-continuation.html
http://blog.zhaojie.me/2009/04/tail-recursion-explanation.html