编写高质量代码改善C#程序的157个建议[用抛异常替代返回错误、不要在不恰当的场合下引发异常、重新引发异常时使用inner Exception]

简介: 前言     自从.NET出现后,关于CLR异常机制的讨论就几乎从未停止过。迄今为止,CLR异常机制让人关注最多的一点就是“效率”问题。其实,这里存在认识上的误区,因为正常控制流程下的代码运行并不会出现问题,只有引发异常时才会带来效率问题。

前言  

  自从.NET出现后,关于CLR异常机制的讨论就几乎从未停止过。迄今为止,CLR异常机制让人关注最多的一点就是“效率”问题。其实,这里存在认识上的误区,因为正常控制流程下的代码运行并不会出现问题,只有引发异常时才会带来效率问题。基于这一点,很多开发者已经达成共识:不应将异常机制用于正常控制流中。达成的另一个共识是:CLR异常机制带来的“效率”问题不足以“抵消”它带来的巨大收益。CLR异常机制至少有一下几个优点:

  1、正常控制流会倍立即中止,无效值或状态不会在系统中继续传播。

  2、提供了统一处理错误的方法。

  3、提供了在构造函数、操作符重载及属性中报告异常的便利机制。

  4、提供了异常堆栈,便于开发者定位异常发生的位置。

  另外,“异常”其名称本身就说明了它的发生是一个小概率事件。所以,因异常带来的效率问题会倍限制在一个很小的范围内。实际上,try catch所带来的效率问题几乎忽略的。在某些特定的场合,如Int32的Parse方法中, 确实存在这因为滥用而导致的效率问题。在这种情况下,我们就应该考虑提供一个TryParse方法,从设计的角度让用户选择让程序运行得更快。另一种规避因为异常而影响效率的方法是:Tester-doer模式,下文将详细阐述。

  本章将给出一些在C#中处理CLR异常方面的通用建议,一帮助大家构建和开发一个运行良好和可靠的应用系统。

  本文已同步到http://www.cnblogs.com/aehyok/p/3624579.html。本文主要来学习以下几点建议

  建议58、用抛出异常代替返回错误代码

  建议59、不要在不恰当的场合下引发异常

  建议60、重新引发异常时使用inner Exception

58、用抛出异常代替返回错误代码  

  在异常机制出现之前,应用程序普遍采用返回错误代码的方式来通知调用者发生了异常。本建议首先阐述为什么要用抛出异常的方式来代替返回错误代码的方式。

  对于一个成员方法来说,它要么执行成功,要么执行失败。成员方法成功的情况很容易理解。但是如果执行失败了却没有那么简单,因为我们需要将导致执行失败的原因通知调用者。抛出异常和返回错误代码都是用来通知调用者的手段。

  假设我们要实现这样一个简单的功能:应用程序需要完成一次保存新建用户的操作。这是一个分布式的操作,保存动作除了需要将用户保存在本地外,还需要通过WCF在远程服务器上保存数据。负责保存用户的成员方法如下:

        public int SaveUser(User user)
        {
            if (!SaveToFile(user))
            {
                return 1;
            }
            if (!SaveToDataBase(user))
            {
                return 2;
            }
            return 0;
        }

        public bool SaveToFile(User user)
        {
            return true;
        }

        public bool SaveToDataBase(User user)
        {
            return true;
        }

如果单纯的看SaveUser方法,似乎一切都还不错,在约定好了错误代码后,调用者只要接收到1或2,就知道到底是那里出现了问题。但仔细研究会发现,如果方法执行失败,似乎还可以挖掘出更多的原因。

假设在SaveToFile方法中,我们可能会遇到:

1、程序无数据存储文件写权限导致的失败。

2、硬盘空间不足导致的失败。

在SaveToDataBase方法中,我们可能会遇到:

1、服务不存在导致的失败。

2、网络连接不正常导致的失败。

当我们想要告诉调用者更多的细节的时候,就需要与调用者约定更多的错误代码。于是我们很快就会发现,错误代码飞速膨胀,直到看起来似乎无法维护。因为我们总在查找并确认错误代码。

  采用接下来的方法,可能会省略很大一部分的错误代码:

        public bool SaveUser1(User user,ref string errorMessage)
        {
            if (!SaveToFile(user))
            {
                errorMessage = "本地保存失败";
                return false;
            }
            if (!SaveToDataBase(user))
            {
                errorMessage = "远程保存失败";
                return false;
            }
            return true;
        }

  这看上去不错,即使存在更多的错误也可以将失败信息呈现给调用者或者上层用户。然后仅仅呈现失败信息就可以了吗?我们来看看这样一种情况:给失败通知增加稍微复杂一点的功能。

  如果本地保存失败,要完成“通知运行本段代码的客户机管理员”的功能。通常情况下,仅仅只需要显示类似的信息:“本地保存失败,请检查用户权限”。如果远程保存失败,应用程序需要“发送一封邮件给远程服务器的系统管理员”。总金额个增加的功能导致我们不能像处理“本地保存失败”那样来处理“远程保存失败”。

  一切仿佛又回到了起点,在没有异常处理机制之前,我们只能返回错误代码,但是现在有了另一种选择,即使用异常机制。如果使用异常机制,那么最终的代码看起来应该是下面这样的:

        static void Main(string[] args)
        {
            try
            {
                SaveUser(new User());
            }
            catch (IOException e)
            {
                ///IO异常,通知当前用户
            }
            catch (UnauthorizedAccessException e)
            {
                ////权限异常,通知客户端管理员
            }
            catch (CommunicationException e)
            {
                ///网络异常,通知发送给网络管理员
            }
        }

        public static void SaveUser(User user)
        {
            SaveToFile(user);

            SaveToDataBase(user);
        }

  使用CLR异常机制后,我们会发现代码变得更清晰、更易于理解了。至于效率问题,还可以重新审视“效率”的立足点:throw exception产生的那点效率损耗与等待网络连接异常相比,简直微不足道,而CLR异常机制带来的好处却是显而易见的。

  这里需要稍加强调的是,在catch(CommunicationException)这个代码块中,代码所完成的功能是“通知发送”而不是“发送”本身,因为我们要确保在catch和finally中所执行的代码是可以倍执行的。换句话说,尽量不要在catch和finally中再让代码“出错”,那么让异常堆栈信息变得复杂和难以理解。

  在本例的catch代码块中,不要真得编写发送邮件的代码,因为发送邮件这个行为可能会产生更多的异常,而“通知发送”这个行为稳定性更高(即不“出错”)。

  以上通过实际的案例阐述了抛出异常相比于返回错误代码的优越性,以及在某些情况下错误代码将无用武之地,如构造函数、操作符重载及属性。语法特性决定了其不能具备任何返回值,于是异常机制倍当作取代错误代码的首要选择。

 

59、不要在不恰当的场合下引发异常  

  最常见不易引发异常的情况是对在可控范围内的输入和输出引发异常。如下面的代码所示:

        public void SaveUser2(User user)
        {
            if (user.Age < 0)
            {
                throw new ArgumentOutOfRangeException("Age不能为负数");
            }
        }

暂时可以发现此方法有两处不妥:

1、判断Age为负数。这是一个正常的业务逻辑,它不应该倍处理为一个异常。

2、应该采用Tester-Doer来验证输入。

我们现在来添加一个Tester方法

        public static bool CheckAge(int age,ref string errorMessage)
        {
            if (age < 0)
            {
                errorMessage = "Age不能为负数";
                return false;
            }
            else if (age > 100)
            {
                errorMessage = "Age不能大于100";
                return false;
            }
            return true;
        }

而调用的地方看起来是这样的

                string errorMessage = string.Empty;
                if (CheckAge(30, ref errorMessage))
                {
                    SaveUser(new User());
                }

程序员,尤其是类库开发程序员,要掌握的两条首要原则是:

正常的业务流程不应使用异常来处理。

不要总是尝试去捕获异常或引发异常,而应该允许异常向调用堆栈往上传播。

 

那么到底应该在什么情况下引发异常呢?

第一种情况 如果运行代码后会造成内存泄漏、资源不可用,或者应用程序状态不可恢复,则引发异常。

第二种情况 在捕获异常的时候,如果需要包装一些更有用的信息, 则引发异常。

这类异常的引发在UI层特别有用。系统引发的异常所带的信息往往更倾向于技术性的描述;而在UI层,面对异常的很可能是最终的用户。如果需要将异常信息呈现给用户,更好的做法是先包装异常,然后引发一个包含友好信息的新异常。

第三种情况 如果底层异常在高层操作的上下文中没有意义,则可以考虑捕获这些底层异常,并引发新的有意义的异常。

例如下面的代码中:

        public void CaseSample(object o)
        {
            if (o == null)
            {
                throw new ArgumentNullException("o");
            }
            User user = null;
            try
            {
                user = (User)o;
            }
            catch (InvalidCastException)
            {
                throw new ArgumentException("输入参数不是一个User", "o");
            }
        }

如果抛出InvalidCastException则没有任何意义,甚至会造成误解,所以更好的方式是抛出一个ArgumentException。

需要重点介绍的正确引发异常的典型例子就是捕获底层API错误代码,并抛出。查看如下代码:

        public void Test()
        {
            int errorCode=Marshal.GetLastWin32Error();
            if (errorCode == 6)
            {
                throw new InvalidOperationException("具体错误");
            }
        }

很显然当需要调用WIndows API或第三方API提供的接口时,如果对方的异常报告机制使用的是错误代码,最好重新引发该接口提供的错误,因为你需要让自己的团队更好地理解这些错误。

建议60、重新引发异常时使用inner Exception  

  当捕获了某个异常,将其包装或重新引发异常的时候,如果其中包含了Inner Exception,则有助于程序员分析内部信息,方便调试。

  可以先来查看以下代码

static void Main(string[] args)
{
    try
    {
        Test();
    }
    catch (Exception err)
    {
        Console.WriteLine(err.Message);
        if (err.InnerException != null)
        {
            Console.WriteLine(err.InnerException.Message);
        }
    }
}
 
 
public static void Test()
{
    try
    {
        SaveUser(new User());
    }
    catch (Exception err)
    {
        var ex = new Exception("网络链接失败,请稍后再试",err);
        //throw err; //这样抛出异常会丢掉异常原有的堆栈信息
        throw ex;
    }
}

如果不想使用Inner Exception,可以使用如下方式

        static void Main(string[] args)
        {
            try
            {
                Test();
            }
            catch (Exception err)
            {
                Console.WriteLine(err.Data["SockInfo"].ToString());
            }
        }


        public static void Test()
        {
            try
            {
                SaveUser(new User());
            }
            catch (Exception err)
            {
                err.Data.Add("SockInfo", "网络链接失败,请稍后再试");
                throw err;
            }       
        }

相当于把Test方法中的异常当作Inner  Exception,然后向上抛出。

意思其实也就是将异常进行简单的封装,然后继续向上抛出,让上层来捕获异常信息。

 

 

目录
相关文章
|
1月前
|
C# Windows
C#通过代码实现快捷键编辑
C#通过代码实现快捷键编辑
|
3月前
|
开发框架 .NET 编译器
C# 10.0中Lambda表达式的改进:更简洁、更灵活的代码编写体验
【1月更文挑战第21天】随着C#语言的不断发展,Lambda表达式作为一种简洁、高效的函数式编程工具,在C# 10.0中迎来了重要的改进。本文将详细探讨C# 10.0中Lambda表达式的新特性,包括参数类型的推断增强、自然类型的Lambda参数以及Lambda表达式的属性改进等。这些改进不仅简化了Lambda表达式的编写过程,还提升了代码的可读性和灵活性,为开发者带来了更优质的编程体验。
|
3月前
|
C# 开发者
C# 10.0中的文件范围命名空间:简化代码组织的新方式
【1月更文挑战第18天】C# 10.0引入了文件范围的命名空间,这是一种新的语法糖,用于更简洁地组织和管理代码。文件范围命名空间允许开发者在每个文件的基础上定义命名空间,而无需显式使用花括号包裹整个文件内容。本文将深入探讨文件范围命名空间的工作原理、使用场景以及它们为C#开发者带来的便利。
|
3月前
|
C# 开发者
C# 9.0中的模块初始化器:程序启动的新控制点
【1月更文挑战第14天】本文介绍了C# 9.0中引入的新特性——模块初始化器(Module initializers)。模块初始化器允许开发者在程序集加载时执行特定代码,为类型初始化提供了更细粒度的控制。文章详细阐述了模块初始化器的语法、用途以及与传统类型初始化器的区别,并通过示例代码展示了如何在实际项目中应用这一新特性。
|
3月前
|
编译器 C# 开发者
C# 9.0中的顶级语句:简化程序入口的新特性
【1月更文挑战第13天】本文介绍了C# 9.0中引入的顶级语句(Top-level statements)特性,该特性允许开发者在不使用传统的类和方法结构的情况下编写简洁的程序入口代码。文章详细阐述了顶级语句的语法、使用场景以及与传统程序结构的区别,并通过示例代码展示了其在实际应用中的便捷性。
|
1月前
|
Java C# 开发工具
第一个C#程序
第一个C#程序
12 0
|
1月前
|
数据采集 存储 C#
抓取Instagram数据:Fizzler库带您进入C#程序的世界
在当今数字化的世界中,数据是无价之宝。社交媒体平台如Instagram成为了用户分享照片、视频和故事的热门场所。作为开发人员,我们可以利用爬虫技术来抓取这些平台上的数据,进行分析、挖掘和应用。本文将介绍如何使用C#编写一个简单的Instagram爬虫程序,使用Fizzler库来解析HTML页面,同时利用代理IP技术提高采集效率。
抓取Instagram数据:Fizzler库带您进入C#程序的世界
|
2月前
|
数据采集 JSON 前端开发
从代码到内容:使用C#和Fizzler探索Instagram的深处
Instagram是一个流行的社交媒体平台,拥有数亿的用户和海量的图片和视频内容。如果您想要从Instagram上获取一些有用的信息或数据,您可能需要使用爬虫技术来自动化地抓取和分析网页内容。本文将介绍如何使用C#和Fizzler这两个强大的工具,来实现一个简单而高效的Instagram爬虫,从代码到内容,探索Instagram的深处。
|
3月前
|
存储 传感器 监控
工业相机如何实现实时和本地Raw格式图像和Bitmap格式图像的保存和相互转换(C#代码,UI界面版)
工业相机如何实现实时和本地Raw格式图像和Bitmap格式图像的保存和相互转换(C#代码,UI界面版)
31 0
|
3月前
|
存储 C# 容器
掌握 C# 变量:在代码中声明、初始化和使用不同类型的综合指南
变量是用于存储数据值的容器。 在 C# 中,有不同类型的变量(用不同的关键字定义),例如: int - 存储整数(没有小数点的整数),如 123 或 -123 double - 存储浮点数,有小数点,如 19.99 或 -19.99 char - 存储单个字符,如 'a' 或 'B'。Char 值用单引号括起来 string - 存储文本,如 "Hello World"。String 值用双引号括起来 bool - 存储具有两个状态的值:true 或 false
37 2