在《Writing High-Performance .NET Code》这本书中,作者指出,抛出“异常”(Exception)的开销十分昂贵,很大一部分原因是.NET的异常对象中包含了丰富的信息,“异常”必须是为真正的异常情况服务,在这种情况下性能可以退居其次

作者还举了例子,说明了三个事实:

  1. 抛出“异常”的方法比空方法慢了数千倍。
  2. “异常”抛出的层数越深,速度就越慢。
  3. 和只用1个catch相比,多个catch语句的影响比较轻微,但仍有影响。

使用catch捕获异常的开销可能不大,但访问Exception对象的StackTrace属性开销可能会非常高。因为需要由“异常”指针重建调用栈,并转译成可读文本。作者反复强调,“异常”应该仅用于真正的异常情况,如果将“异常”作为正常的处理流程来使用,会让程序毫无性能可言。

避免使用正常情况下也会抛出异常的API


在后面的章节中,作者也指出因为“异常”的开销很高,所以应该把"异常"留待真正发生异常的时候使用。

比如大部分基本数据类型都提供了Parse方法,用于将string字符串类型转换为基本类型,当输入的字符串格式无法识别时,就会抛出FormatException,比如Int32.Parse,DateTime.Parse等。除非程序可以在Parse出错时完全退出,否则避免使用这些方法,而应该选用TryParse方法,如果解析失败会返回false。

另外一个例子是System.Net.HttpWebRequest类,如果从服务器接收到“200”以外的应答信息,就会抛出“异常”。这种行为,在.NET 4.5中的System.Net.Http.HttpClient中得到修正。

一个把异常作为正常流程处理的例子


在普通的编程过程中,有时为了偷懒或者避免异常溢出,不管三七二十一就去把一大段代码用try catch包起来作为兜底,从而不让程序崩溃。这种行为会严重影响程序的性能。有时候程序出现异常,让其自然退出是一件非常正确的事情,因为如果出现异常,就表示可能状态或者数据被损坏,继续运行就会出现更多问题。如果要避免出现异常而导致程序崩溃,就应该仔细的逐行分析代码中的可能出现异常的语句,把异常范围控制到最小,这样就能减少影响或者进一步方便从异常中恢复。

旗帜鲜明的反对对代码滥加try catch的行为。说到这一点,我想到了以前项目中的一段代码,这段代码将“异常”作为了正常的流程来处理。代码如下:

public static List<TasReader> LoadTAS(BinaryReader br)
{
    List<TasReader> trades = new List<TasReader>();
    try
    {
        while (true)
        {
            TasReader trade = new TasReader();
            trade.FullCode = br.ReadString();
            trade.Date = br.ReadInt32();
            trade.AskOrder = br.ReadInt32();
            trade.BidOrder = br.ReadInt32();
            trade.BSFlag = br.ReadByte();
            trade.Symbol = br.ReadString();
            trade.TradeType = br.ReadByte();
            trade.SN = br.ReadInt32();
            trade.FunctionCode = br.ReadByte();
            trade.Price = br.ReadInt32();
            trade.Time = br.ReadInt32();
            trade.Turnover = br.ReadInt32();
            trade.Volume = br.ReadInt32();
            if (trade.Price > 0 && trade.Volume > 0)
            {
                trades.Add(trade);
            }
        }
    }
    catch (EndOfStreamException e)
    {

    }
    return trades;
}

这段代码的意思是,从一个落地的二进制文件中读取行情的逐比成交数据,不断的从BinaryReader读取数据,直到抛出EndOfStreamException的异常。

 using (BinaryReader br = new BinaryReader(new FileStream(selectFileName, FileMode.Open, FileAccess.Read)))
{
     list = LoadTAS(br);
}

这里的代码使用了EndOfStreamException这个异常来判断是否读到了BinaryReader的末尾,这能完成任务,但正如前面所说,抛出异常会严重影响程序性能。并且,在这里我们实际上需要的是提前判断BinaryReader是否已经到达末尾,如果达到末尾了就不去读了。而不是说一直从BinaryReader读取数据,直到遇到EndOfStreamException就判断就说明读取到了末尾。

可能是当时写代码的人没有找到正确的如何判断是否读取到二进制流末尾的办法,所以才临时使用了这个有问题的解决方法。那么如何判断BinaryReader是否读取到了末尾呢?

解决方法


通过一番搜索,其实解决方法也挺简单,就是判断当前读取的位置是否已经达到整个二进制流的长度,代码修改如下:

public static List<TasReader> LoadTAS(BinaryReader br)
{
    List<TasReader> trades = new List<TasReader>();
    while (br.BaseStream.Position < br.BaseStream.Length)
    {
        TasReader trade = new TasReader();
        trade.FullCode = br.ReadString();
        trade.Date = br.ReadInt32();
        trade.AskOrder = br.ReadInt32();
        trade.BidOrder = br.ReadInt32();
        trade.BSFlag = br.ReadByte();
        trade.Symbol = br.ReadString();
        trade.TradeType = br.ReadByte();
        trade.SN = br.ReadInt32();
        trade.FunctionCode = br.ReadByte();
        trade.Price = br.ReadInt32();
        trade.Time = br.ReadInt32();
        trade.Turnover = br.ReadInt32();
        trade.Volume = br.ReadInt32();
        if (trade.Price > 0 && trade.Volume > 0)
        {
            trades.Add(trade);
        }
    }
    return trades;
}

代码非常漂亮,直接判断BinaryReader的BaseStream流里面当前的位置跟整个数据流的长度进行对比,如果小于长度,表示还可以继续读一个对象的所有字段。以上代码成功的去除了try catch,代码清晰明了。

 

参考: