编写高质量代码改善C#程序的157个建议[正确操作字符串、使用默认转型方法、却别对待强制转换与as和is]

前言

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

  建议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进行判断之后再进行转型的操作,以避免转型失败。

 

posted @ 2014-03-27 14:13  aehyok  阅读(8778)  评论(13编辑  收藏  举报