编写高质量代码改善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进行判断之后再进行转型的操作,以避免转型失败。

时间: 2024-08-04 11:54:58

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

编写高质量代码改善C#程序的157个建议——建议45:为泛型类型参数指定逆变

建议45:为泛型类型参数指定逆变 逆变是指方法的参数可以是委托或者泛型接口的参数类型的基类.FCL4.0中支持逆变的常用委托有: Func<int T,out TResult> Predicate<in T> 常用委托有: IComparer<in T> 下面例子演示了泛型类型参数指定逆变所带来的好处: class Program { static void Main() { Programmer p = new Programmer { Name = "Mi

编写高质量代码改善C#程序的157个建议——建议27:在查询中使用Lambda表达式

建议27:在查询中使用Lambda表达式 LINQ实际上是基于扩展方法和Lambda表达式的.任何LINQ查询都能通过扩展方法的方式来代替. var personWithCompanyList = from person in personList select new { PersonName = person.Name, CompanyName = person.CompanyID==0?"Micro":"Sun" }; foreach (var item in

编写高质量代码改善C#程序的157个建议——建议26:使用匿名类型存储LINQ查询结果

建议26:使用匿名类型存储LINQ查询结果 从.NET3.0开始,C#开始支持一个新特性:匿名类型.匿名类型有var.赋值运算符和一个非空初始值(或以new开头的初始化项)组成.匿名类型有如下基本特性: 即支持简单类型也指出复杂类型.简单类型必须是一个非空初始值,复杂类型则是一个以new开头的初始化项. 匿名类型的属性是只读的,没有属性设置器,它一旦被初始化就不可更改. 如果两个匿名类型的属性值相同,那么就认为这两个匿名类型相等. 匿名类型可以再循环中用作初始化器. 匿名类型支持智能感知. 匿名

编写高质量代码改善C#程序的157个建议——建议20:使用泛型集合代替非泛型集合

建议20:使用泛型集合代替非泛型集合 在建议1中我们知道,如果要让代码高效运行,应该尽量避免装箱和拆箱,以及尽量减少转型.很遗憾,在微软提供给我们的第一代集合类型中没有做到这一点,下面我们看ArrayList这个类的使用情况: ArrayList al=new ArrayList(); al.Add(0); al.Add(1); al.Add("mike"); foreach (var item in al) { Console.WriteLine(item); } 上面这段代码充分演

编写高质量代码改善C#程序的157个建议——建议12: 重写Equals时也要重写GetHashCode

建议12: 重写Equals时也要重写GetHashCode 除非考虑到自定义类型会被用作基于散列的集合的键值:否则,不建议重写Equals方法,因为这会带来一系列的问题. 如果编译上一个建议中的Person这个类型,编译器会提示这样一个信息: “重写 Object.Equals(object o)但不重写 Object.GetHashCode()” 如果重写Equals方法的时候不重写GetHashCode方法,在使用如FCL中的Dictionary类时,可能隐含一些潜在的Bug.还是针对上一

编写高质量代码改善C#程序的157个建议——建议13: 为类型输出格式化字符串

建议13: 为类型输出格式化字符串 有两种方法可以为类型提供格式化的字符串输出.一种是意识到类型会产生格式化字符串输出,于是让类型继承接口IFormattable.这对类型来 说,是一种主动实现的方式,要求开发者可以预见类型在格式化方面的要求.更多的时候,类型的使用者需为类型自定义格式化器,这就是第二种方法,也是最灵活 多变的方法,可以根据需求的变化为类型提供多个格式化器.下面就来详细介绍这两种方法. 最简单的字符串输出是为类型重写ToString方法,如果没有为类型重写该方法,默认会调用Obj

编写高质量代码改善C#程序的157个建议——建议90:不要为抽象类提供公开的构造方法

建议90:不要为抽象类提供公开的构造方法 首先,抽象类可以有构造方法.即使没有为抽象类指定构造方法,编译器也会为我们生成一个默认的protected的构造方法.下面是一个标准的最简单的抽象类: abstract class MyAbstractClass { protected MyAbstractClass(){} } 其次,抽象类的方法不应该是public或internal的.抽象类设计的本意是让子类继承,而不是用于生成实例对象的.如果抽象类是public或internal的,它对于其它类型

编写高质量代码改善C#程序的157个建议——建议85:Task中的异常处理

建议85:Task中的异常处理 在任何时候,异常处理都是非常重要的一个环节.多线程与并行编程中尤其是这样.如果不处理这些后台任务中的异常,应用程序将会莫名其妙的退出.处理那些不是主线程(如果是窗体程序,那就是UI主线程)产生的异常,最终的办法都是将其包装到主线程上. 在任务并行库中,如果对任务运行Wait.WaitAny.WaitAll等方法,或者求Result属性,都能捕获到AggregateException异常.可以将AggregateException异常看做是任务并行库编程中最上层的异

编写高质量代码改善C#程序的157个建议——建议89:在并行方法体中谨慎使用锁

建议89:在并行方法体中谨慎使用锁 除了建议88所提到的场合,要谨慎使用并行的情况还包括:某些本身就需要同步运行的场合,或者需要较长时间锁定共享资源的场合. 在对整型数据进行同步操作时,可以使用静态类Interlocked的Add方法,这就极大地避免了由于进行原子操作长时间锁定某个共享资源所带来的同步性能损耗.回顾建议83中的例子. static void Main(string[] args) { int[] nums = new int[] { 1, 2, 3, 4 }; int total