什么,又是字符串拼接,我有些不淡定了

前言:最近在看其他人写的旧项目代码的时候,发现有三个让我极其不习惯也隐隐感到不舒服的地方。一个是sql查询语句中出现 SELECT * 的概率极高,第二个是实体转换的时候竟然还要手写遍历和转换,最后一个就是他们在项目中使用了大面积的字符串拼接。前两个问题说明项目开发自动化做的不够,后一个就比较黑色幽默一点,再次证明字符串拼接是人民群众喜闻乐见的编程方式。对于代码完美主义者来说,大量的字符串拼接肯定是逃不过被重构的命运的,但是既然已经在项目中蔚为壮观地我存在你崇拜没有更好的办法了,咱也不能改动的太离谱。

1、乱弹StringJoiner的横空出世

为了这种字符串拼接,不论是C#还是Java都有对应的改进的类和方法。比如我们熟知的C#下的StringBuilder类,string的format方法,还有Java中不是也有StringBuffer类吗?但是实际开发中,很多人还是喜欢直接用string加过来加过去,replace也会适时出现几次,提升自己的曝光度。虽然写代码的人很省事,可读性严格来说也不是那么的差,而且运行起来性能也不是离谱的一无是处,完事之后说不定哪一天写代码的人就收拾金银细软走人了,大大小小若干项目都是这种代码可就……碰巧后来维护这种代码的人非常争气,想到了好的解决方案,捏住鼻子含怒重构了这种低效的代码之后,发扬无私共享的风格在园子里贴了出来造福大众,一不小心还成了“拯救那些性能低下的字符串拼装代码“的先驱,CoolCode,我说的对吗?

关于StringJoiner的前世今生,请参考CoolCode的大作:

StringJoiner 拯救那些性能低下的字符串拼装代码

 

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

posted on 2010-12-01 21:05  JeffWong  阅读(2922)  评论(3编辑  收藏  举报