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


    前言:最近在看其他人写的旧项目代码的时候,发现有三个让我极其不习惯也隐隐感到不舒服的地方。一个是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

  • 相关阅读:
    Spring--IOC
    神奇的小东西
    视图层发起请求的方式
    jdbc “贾琏欲执事”
    线程的五种状态
    java的<<左移,>>右移,>>>无符号右移
    直接插入排序(单链表排序)
    单链表相关知识以及指针引用相关知识
    将一个数n分解为若干个从小到大排列的质数的积 ,求质数因子
    最小二乘法
  • 原文地址:https://www.cnblogs.com/jeffwongishandsome/p/1893822.html
Copyright © 2020-2023  润新知