Skip to content

Effective C# 改善 C# 程序的 50 种方法

第 26 条:实现泛型接口的同时,还应该实现非泛型接口

  • 这条建议适用于三项内容:1. 要编写的类以及这些类所支持的接口;2. public 属性;3. 打算序列化(serialize)的那些元素。
  • 在绝大多数情况下,如果想给旧版接口提供支持,那么只需要在类里面添加签名正确的方法就可以了。
  • 要考虑实现与泛型接口相对应的旧式接口,在实现这些接口时,应该明确加以限定,以防用户在本打算使用新版接口时无意间调用了旧版接口。
  • 使用 VS 及其他一些工具时,可以通过向导(wizard)功能创建针对这些接口方法的样板代码。

第 27 条:只把必备的契约定义在接口中,把其他功能留给扩展方法其实现

  • 这个方法对应了软件开发原则,对修改关闭,对新增(扩展)开放。
  • 接口的功能需要尽可能的单一。
  • 定义接口的时候,只把必备的功能列出来就行了,而其他一些功能则可以在别的类里面以扩展方法的形式去编写,那些方法能够借助原接口所定义的基本功能来完成自身的任务。
  • 只把那些必备的功能定义到接口里面,以满足应用程序的需求,而不要在接口里面定义附加功能,因为那些功能可以留给扩展方法去实现,对使用者来讲,就产生了好似接口有该功能的错觉,其实是扩展方法的功劳。
  • 注意,如果本类中实现了和扩展方法同名的方法,怎么办?调用有条件与先后顺序,太复杂,我们只讲怎么做。首先保证行为一致,类似于你通过同名方法做了重构(算法优化等等),这样可以保证程序不出大问题。

第 28 条:考虑通过扩展方法增强已构造类型的功能

  • 我之前一直是这么做的,只是我没想到可以为接口添加扩展方法(用过,但未留意),有了这样一个思路,简直是无敌了。
  • 同样,再为接口添加扩展方法的同时,可以直接调用接口所具有的方法,比如可以把一堆复杂的操作合并出来,从而只暴露出一个扩展方法,双倍无敌。
  • 我们应该想一想这些类型目前已有的方法以及将来还有可能需要提供的方法里面有哪些可以改用扩展方法来实现。若能将这些方法实现成针对某个泛型类型或泛型接口的扩展方法,则会令那个以特定参数而构造的泛型类型或接口具备丰富的功能。此外,这样做还可以最大限度地将数据的储存模型(storage model)与使用方式相解耦。

第 29 条:优先考虑提供迭代器方法,而不要返回集合

  • 促使 C#语言升级到 3.0 版的一项动力就是 LINQ。C#语言之所以会引入并实现这些新功能。是因为业界希望该语言能够支持延迟查询(deferred query)机制。
  • 迭代器方法是一种采用 yield return 语法来编写的方法,它会等到调用方请求获取某个元素的时候再去生成序列中的这个元素。
  • 对于较小的序列(集合)来说,这样做的优势并不明显,但对于较大的序列就不一样了。
c#
var allNumbers = Enumerable.Range(0, int.MaxValue);
  • 该方法所生成的对象可以在调用方真正用到某个整数时再去创建该数,这使得调用方不用把那么多数字全都放到某个庞大的集合中,除非确实需要。

  • 这种按需生成(generate-as-needed)的策略还揭示出迭代器方法的另一个重要特点,那就是序列中的元素由该方法创建出来的那个对象生成。只有当调用方真正用到序列中的某个元素时程序才会通过那个对象创建该元素。这使得程序在调用生成器方法(generator method)时只需要执行少量的代码。

  • 有一个缺点是,有些异常是需要调用时(使用函数的返回值)才会触发,而无法在传入错误参数的时候就抛出异常。

  • 问:有没有哪种场合不需要(不适宜)用迭代器方法来生成序列?没有。这个是调用方需要考虑的问题,我们作为 API 的设计者,应当尽可能地使其灵活。具体怎么用更好,调用者会考虑,但我们首先得支持。

  • 请开始提供迭代器方法。

第 30 条:优先考虑通过查询语句来编写代码,而不要使用循环语句

首先你得知道什么是查询语句。(区别于查询方法) 比较两段代码:

c#
private static IEnumerable<Tuple<int, int>> ProduceIndices()
{
   for(var x = 0; x < 100; x++)
       for(var y = 0; y < 100; y++)
           yield return Tuple.Create(x, y);
}
private static IEnumerable<Tuple<int, int>> QueryIndices()
{
   return from x in Enumerable.Range(0, 100)
       from y in Enumerable.Range(0, 100)
       select Tuple.Creat(x, y);
}
  • 第一种为普通的循环结构(区别于查询方法),第二种为查询语句。当你加一些过滤条件(需求)时,两者的变化更是千差万别。查询语句将无比简洁。

  • 命令式的模型很容易过分强调怎样去实现操作,而令阅读代码的人忽视这些操作本身是打算做什么的。

  • 查询语句要比循环结构好,前者可以创建出更容易拼接的 API,用查询命令来编写算法促使开发者把该算法实现成很多小的代码块,小的代码块连续拼接实现大的操作。而,循环结构不能拼接,你必须把中间结构的结果保存起来,或者分别针对小操作创建对应的方法。

  • 再说一遍,命令式的写法必须创建储存空间来保存中间结果。

  • 查询语句天生支持并行化(parallel),当你觉得算法不够快时,可以使用.AsParallel()来执行查询。

  • 总结:编写循环结构时,总是应该想想能不能改用查询语句来实现同样的功能,如果实在不行,再想想能不能改用查询方法,每一种命令式的循环结构几乎都可以通过查询式的写法更为清晰地表达出来。 写代码之前要多动脑子,不要拿来就用,要对自己的每一行代码负责,要本着写了就不再改的目标去下笔(当然这是不可能的,不然不会有《重构》这本书),这是我的理解。但归根结底代码的核心还是要实现需求,但我更希望在此之前,有这样一个原则,如果实在保不住原则,还是要以功能为首位,但总应该有这么一个过程。

第 31 条:把针对序列的 API 设计得更加易于拼接

  • 序列的意思就是集合。
  • 把通用的 IEnumerable<T> 或针对某种类型的 IEnumerable<T> 设计成方法的输入及输出参数是一种比较少见的思路,很多人不愿意这样做,但这种思路确实能带来很多好处。
  • 迭代器方法会等到调用方真正用到某个元素时才去执行相应的代码,而不会提前。这种延迟执行(deferred execution)机制可以降低算法所需的存储空间。并使算法的各个部分之间能够更为灵活地拼接起来。
  • yield return 语句,用这种语句写出来的方法,其输入值与输出值都是迭代器。这种方法属于可以从上次执行到的位置继续往下执行的方法(continuable method)。
  • 改写为连续方法有两个很大的好处,首先,它推迟了每个元素求值的时机,更为重要的是,这种延迟机制使得开发者能够把多个这样的操作拼接起来,从而更灵活地复用它们,反之,若使用 foreach 循环的命令式方法来达成此效果则较为困难。
  • 如果能够把复杂的算法拆解成多个步骤,并把每个步骤都表示成这种小型的迭代器方法,那么就可以将这些方法拼成一条管道,使得程序只需把源序列处理一遍(无需临时保存)即可对其中的元素执行许多种小的变换。

第 32 条:将迭代逻辑与操作、谓词及函数解耦

  • 迭代器方法的代码通常由两部分组成,一部分是用来迭代该序列,另一部分用来对元素执行操作。
  • 写一个类似过滤器的委托,匿名的委托有两种习惯的用法,一种表示函数,另一种表示操作。表示函数是有一个特殊的用法,充当谓词(predicate)。表示操作的委托则称为操作委托(action delegate),用来在集合中的元素上执行某项操作。
  • 好处是可以把迭代序列时所用的逻辑与处理序列中的元素时所用的逻辑分开。我们在实际的项目中大量使用了这种用法。

第 33 条:等真正用到序列中的元素时再去生成

其实还是 yield return 的用法。

c#
static IEnumerable<int> CreateSequence(int numOfElements, int startAt, int stepBy)
{
    for (int i = 0; i < numOfElements; i++)
    {
        yield return startAt + i * stepBy;
    }
}

可以提前终止序列的遍历过程,TakeWhile 中的条件得不到满足,那么程序就不会再来获取元素了。生成的过程会立刻终止,极大地改善了程序的性能。按需生成序列中的元素,而不需要全部生成再从里面去取。创建元素的开销越大,这种写法的效率越明显。

第 34 条:考虑通过函数参数来放松耦合关系

  • 函数参数(function parameter),其实就是委托。
  • 需要注意的是,如果使用委托或其他一些通信机制来放松耦合关系,那么编译器可能就不会执行某些检查工作了,需要自己来设法做检查。
  • 继承是耦合度最高的编程方式。其次是接口。最后是函数方法(委托)。
  • 具体设计时需要定义接口还是委托,需要根据实际场景语境来判断。有些接口更合适(IComparable<T>),有些委托(RemoveAll(IPredicate<T>))更合适。
  • 关注 Zip()方法。
  • 通过函数的参数确实可以把算法与其操作的具体数据有效地分隔开,但在放松耦合关系的同时,你要多做一些工作,比如异常处理等。
  • 在设计组件时,首先应该考虑能否把本组件与客户代码之间的沟通方式约定成接口。如果有一些默认的实现代码需要编写,那么放到抽象基类中,使得调用方无需重新编写这些代码。如果采用委托,那么用起来会更加地灵活,你需要编写更多的代码才能确保这种灵活的设计能够正常运作。

第 35 条:绝对不要重载扩展方法

复习一下,针对接口或类型创建扩展方法有三个好处:

  • 为接口实现默认的行为;
  • 针对封闭的泛型类型实现某些逻辑;
  • 能够创建出易于拼接的接口方法。
  • 可以把接口定义得尽量简单些,然后编写扩展方法,利用接口提供的少数几个方法来组合出更多的常见操作。
  • 作者的意思是,不要通过相同名字不同命名空间的方式来重载扩展方法(我并不认为这是重载),这很明显是对扩展方法的误用。
  • 如果方法本身是类型的一部分,这样才应该去定义扩展方法。(注意,在面向对象的设计中,实际的使用场景很重要,其实就是真实世界的投影。语言逻辑组织不清楚,代码就一定写不清楚。)
  • 扩展方法是可以重载的,去给它们不同的名字或者不同的参数列表。通过不同命名空间的做法,愚蠢至极。

第 36 条:理解查询表达式与方法调用之间的映射关系

  • 完整的查询表达式模式(query expression pattern)包含 11 个方法。
  • .NET 基础类库为该模式提供了两套参考实现(reference implementation)。一个是位于 Syetem.Linq.Enumerable 中,是 IEnumerable<T> 的扩展方法;一个是位于 Syetem.Linq.Queryable 中,提供针对 IQueryable<T>的查询程序。

第 37 条:尽量采用惰性求值的方式来查询,而不要及早求值

  • 定义查询操作时,程序并不会立刻把数据获取过来并填充到序列中,因为你定义的实际上只是一套执行步骤而已,等真正需要遍历查询结果时,才会得以执行。也就是说,对于查询结果做迭代的时候,程序总是会从头开始执行这套步骤,这样做通常是合理的。每迭代一遍都产生一套新的结果,这叫作惰性求值(lazy evaluation),反之,如果像编写普通的代码那样直接查询某一套变量的取值并将其立刻记录下来,那么就称为及早求值(eager evaluation)。
  • 惰性求值其实还是 yield return 的用法,感觉作者在水字数。
  • 惰性求值的优点在于只求一遍。
  • LINQ 查询操作与那些代码不同,它会把代码当成数据来看,用作参数的 lambda 表达式要等到以后再去调用(而不是立刻就得以执行)。此外,如果 provider 使用的是表达式树(expression tree)而不是委托,那么稍后可能还会有新的表达式融入这棵树中。
  • 你需要了解有哪些查询操作会导致程序必须处理整个序列,试着把这些操作放在查询表达式的尾部。
  • 笔者一直在重复惰性求值的好处(优先考虑),在绝大多数情况下这都是最好的办法。在个别情况下,你可能确实需要一份快照,说用 ToList()ToArray() 这两个方法。他们能立刻根据查询结果来生成序列,并保存到容器中。
  • 总之,与及早求值相比惰性求值基本上能减少程序的工作量,而且使用起来也更加灵活。除非确有必要,否则还是应该优先考虑惰性求值。

第 38 条:考虑用 lambda 表达式来代替方法

  • 在涉及查询表达式与 lambda 的地方应该用更为合理的办法去创建可供复用的代码块。
  • 你可以把查询操作分成许多个小方法来写,其中一些方法在其内部用 lambda 表达式处理序列,而另一些方法则可以直接以 lambda 表达式做参数。把这些小方法拼接起来,就可以实现整套的操作。这样写既可以同时支持 IEnumerable<T>IQueryable<T> ,又能够令系统有机会构建出表达式树,以便高效地执行查询。

第 39 条:不要在 Func 与 Action 中抛出异常

如下面的代码,给每位员工加薪 30%。

c#
var allEmployees = FindAllEmloyees().ForEach(e => e.MonthlySalary *= 1.30M);

如果代码在运行的过程中抛出异常,可能只有一部分员工加薪,而另一部分没有加薪,但是你无法确定是哪一部分。

怎样做,才能给每位员工正常加薪 30%?

首先你的做法可以过滤到那些可能加薪失败的员工(比如不在数据库内),但是这并不彻底。其次,你可以先复制一份,等到副本可以全部成功加薪之后,再赋给原对象,但是,开销会变大。同时使算法的拼接性变差。

你可以考虑令这些查询操作返回新的元素,而不是直接在源序列上面修改,以确保该操作在无法完整执行的情况下不会破坏程序的状态。与一般写法相比,用 lambda 表达式来编写 Action 及 Func 会令其中的异常更加难以发觉。因此在返回最终结果之前,必须确定这些操作都没有出现异常,然后才能用处理结果把整个源序列替换掉。

第 40 条:掌握尽早执行与延迟执行之间的区别

  • 声明式代码(declarative code)的重点在于把执行结果定义出来,而命令式代码(imperative code)则重在详细描述实现该结果所需的步骤。
  • 只有当程序确实要用到某个方法的执行结果时,才会去调用这个方法,这是声明式写法与命令式写法之间的重要区别,如果这两种写法混用,程序可能会出现严重的问题。
  • 从外部来看,只要方法不产生副作用(side effect),那么凡是出现该方法的地方都可以用其返回值来替换,反之亦然。
  • 直接传入方法的结果(带()的传入)与传入委托(lambda,或方法名本身)相比,第一种模型是先调用某个方法,然后把执行结果当做参数在传回本方法,第二种模型则是把那个方法当成委托传给本方法,使得本方法可以根据自己的需要随时调用委托,从而实现第一种的效果。第一种更像是面向过程的,第二种更灵活(动态)。
  • 由于 C#引入 lambda 表达式、类型推断机制及 enumerator 等特性,因此,开发者在编写自己的类时,可以更加方便地运用函数式编程(functional programming)中的某些概念。
  • 那么到底是提前计算好还是等到用的时候再去计算呢?你应该首要考虑的问题是程序的运行效果能否保持一致。其次,你应该在计算成本与储存成本之间考虑,还要考虑自己会怎样使用计算出来的结果。
  • 在编写 C#算法时,先要判断用数据(算法的结果)还是用函数(算法本身)当参数,这样会不会导致程序的运行结果有所区别。在难以判断的情况下,不妨优先考虑把算法当成参数来传递,这样做可以令编写函数的人更灵活,因为它既可以惰性求值,也可以及早求值。

第 41 条:不要把开销较大的资源捕获到闭包中

  • 闭包(closure)会创建出含有约束变量(bound variable)的对象,但是这些对象的生存期可能与你想的不一样,而且通常会给程序带来负面效果。
  • 如果算法使用了一些查询表达式,那么编译器在编译这个方法时,就会把同一个作用域内的所有表达式合起来纳入一个闭包中,并创建相应的类来实现该闭包。这个类的实例会返回给方法的调用者。只用当该实例的使用方全都从系统中移除之后,它才有可能得到回收。这就会产生很多问题。
  • 如果程序从方法中返回的是一个用来实现闭包的对象,那么与闭包相关的那些变量就全都会出现在该对象里面。你需要考虑此后程序是否真的需要用到这些变量。如果不需要使用其中的某些变量,那么就应该调整代码,令其在方法返回的时候能够及时得到清理,而不会随着闭包泄漏到方法之外。

第 42 条:注意 IEnumerable 与 IQueryable 形式的数据源之间的区别

IQueryable<T> 内置 LINQ to SQL 机制,IEnumerable<T> 的查询,是把排序放到本地来完成的。 有些功能使用 IQueryable 要比 IEnumerable 快得多。 用 IEnumerable<T> 编写的代码必须在本地运行,无论数据在不在本地,都要首先下载到本地。如果数据在云端,这就涉及到大量的信息传输。如果更看重健壮性,请使用 IEnumerable<T>。 可以使用 AsEnumerable()与 AsQueryable()进行相互转换。IQueryable 更适合远程执行(数据库)。

第 43 条:用 Single()及 First()来明确地验证你对查询结果所做的假设

  • Single()方法只会在有且仅有一个元素合乎要求时把该元素返回给调用方,如果没有或者很多,就会抛出异常。
  • 如果你确定你的查询结果里面有且仅有一个元素,那么就应该使用 Single()来表达这个意思,因为这样做是很清晰的。只要查询结果中的元素数量与自己的预期不符,程序就会立刻抛出异常。
  • 如果你想表达的是要么查不到任何元素,要么只能查到一个元素,那么可以用 SingleOrDefault()来验证。这两个方法都可以保证查询表达式所返回的结果绝对不会超过一个。
  • 有时候,你并不在乎查到的元素是不是有很多,而只是想取出这样的一个元素而已,这种情况下,考虑用 First()或者 FirstOrDefault()方法来表达这个意思。
  • 应该考虑通过更好地写法来寻找那个元素,使得其他开发者与代码维护者能够更为清晰地理解你想找的究竟是什么。

第 44 条:不要修改绑定变量

  • 编译器创建的嵌套类会把 lambda 表达式所访问或修改的每个变量都囊括进来,而且原来访问局部变量的那些地方现在也会改为访问该嵌套类中的字段。这意味着对于同一个局部变量来说,lambda 表达式里面的代码与其外围方法中的代码访问的其实都是嵌套类中与该变量相对应的那个字段。表达式里面的逻辑会编译成嵌套类中的方法。
  • 把延后执行机制与编译器实现闭包的方式等因素考虑进来,你就会发现:如果在定义查询表达式的时候用到了某个局部变量,而在执行之前又修改了它的值那么程序就有可能会出现奇怪的错误,因此,捕获到闭包中的那些变量最好不要去修改。

接下来的一章会讲解怎样通过异常来清晰而准确地表达程序在运行中所发生的错误,而且还会告诉大家怎样管理程序的状态才能令其更容易从错误中恢复。

第 45 条:考虑在方法约定遭到违背时抛出异常

  • 如果方法不能完成其所宣称的操作,那么就应该通过异常来指出这个错误。如果改用错误码(error code)来实现,这些代码很容易被调用方所忽视。反之,如果调用方专门用一些逻辑来检测这些代码,并把它们传播出去,那么后果很严重,这些逻辑还会干扰到程序的核心逻辑。
  • 由于异常本身也是类,所以你可以从中派生出自己的异常类型,以此来表达更为丰富的错误信息。
  • 使用异常的另一个好处是,异常不会轻易为人所忽视。若无适当的 catch 语句处理异常,程序会明确地终止。
  • 方法与调用方的约定无法得到遵守,就应该抛出异常,并不是说遇到调用方不满意就得一定抛出异常。
  • 由于异常并不适合当做控制程序流程的常规手段,所以还应该提供另一套方法,使开发者在执行操作前先做一个判断,判断不通过提前采取相应措施,而不是等到抛出异常时再去处理。(当然这也不是绝对的,检查的方法不可能全面)

第 46 条:利用 using 与 try/finally 来清理资源

  • 如果某个类用到了非托管型的系统资源,那么就需要通过 IDisposable 接口的 Dispose()方法来明确地释放。.NET 环境规定,这种资源并不需要由包含该资源的类型或系统来释放,而是应该由使用此类型的代码释放。
  • 拥有非托管资源的那些类型,都实现了 IDisposable 接口,此外还提供了 finalizer(终结器/终止化器),以防用户忘记释放该资源。
  • using 语句能够确保 Dispose()总是可以得到调用。
  • 如果函数里只用到一个 IDisposable 对象,那么想要确保它总是可以能够适当地得到清理,最简单的办法就是使用 using 语句。
  • 对象的编译期类型必须支持 IDisposable 接口才能够用在 using 语句中,而不是任何一种对象都可以放在 using 里面。
  • 如果你不清楚一个对象是否实现了 IDisposable 接口,那么可以通过 as 子句来安全地处置它。using(null)不会产生任何效果,但是却可以令程序正常运行下去。
  • 凡是实现了 IDisposable 接口的对象都应该放在 using 语句中或者 try 块中去实现,否则就有可能泄露资源。
  • 尽量选用 Dispose(),而不是 Close()。
  • Dispose()方法并不会把对象从内存中移除,只是提供了一次机会,令其能够释放非托管型的资源。如果程序中的其他地方还需要引用该对象,就不要过早地将其释放。

第 47 条:专门针对应用程序创建异常

  • 如果你要给自己所写的 C#应用程序创建专门的异常类,那么必须考虑的特别周到才行。
  • 必须要把那些需要用不同的方式来处理的情况设计成不同的异常类型。但是,只有那些确实需要有必要分开处理的状况才应该表示成不同的异常类,把明明可以合起来处理的情况硬是放在不同的异常类里面只会增加开发者的工作量,不能带来任何好处。
  • Exception e 的 e.TargetSite.Name 可以获取抛出异常的方法的方法名。
  • 如何判定是否应该抛出异常?
  • 如果某种状况必须立刻得到处理或汇报,否则将长期影响应用程序,那么就应该抛出异常。
  • 开发者应该仔细想想,能不能创建一种新的异常类,以促使调用方更为清晰地理解这个错误,从而试着把应用程序恢复到正常的状态。
  • 之所以要创建不同的异常类,原因很简单,就是为了调用 API 的人能够通过不同的 catch 子句去捕获那些状况,从而采取不同的方法加以处理。
  • 一旦决定自己来创建异常类,就必须遵照相应的原则。这些类都要能够追溯到 Exception 才行,你应该从 System.Exception 类或其子类进行继承。你应该创建四个构造函数。
  • 异常转换(exception translation),用来将底层的异常转换成高层的异常,从而提供更贴近与当前情景的错误信息。catch 一种异常但其实 throw 另外一种异常。

第 48 条:优先考虑做出强异常保证

  • 针对异常做出的保证分为三种,基本保证(basic guarantee)、强保证(strong guarantee)、no-throw 保证(不会抛出异常的保证)。no-throw 保证的意思是不能出任何问题,强保证比较折中,允许出问题,但是应该可以恢复或者不影响程序的整体运行,基本保证是,产生异常后,程序的资源不会泄露,所有的对象都处于有效状态。
  • 应用程序中的许多操作,都会在未能完全执行完毕的情况下令程序陷入无效状态,这种情况无法避免,我们要考虑使用强保证来处理这些异常。做法规定:操作抛出异常,应用程序的状态必须和操作之前相同,要么完全成功,要么完全失败。不存在部分成功的情况。可以认为操作根本没生效。可以的做法之一是使用防御式的拷贝(defensive copy),在拷贝出来的数据上进行操作。
  • 能够从错误中恢复要比性能稍稍得到提升更为重要。
  • 一般来说,要想安全地替换引用类型的数据,就得面对有些客户端无法看到最新数据的情况。这没有两全其美的办法。替换数据这一办法只对值类型有效。
  • 异常筛选器(exception filter)的 when 子句里面绝不应该抛出异常。如果抛出,那么新异常会成为当前活动异常,从而无法获取原来那个异常中的信息。
  • 包括事件处理程序在内的各种委托目标都不应该抛出异常。
  • finalizer、Dispose()、when 子句以及委托目标是四个特例,在这些场合绝对不应该令任何异常脱离其范围。如果在拷贝出来的历史数据上面执行完操作之后想用它把原数据替换掉,而原来那个数据又是引用类型,那么要多加小心,可能会引发很多微妙的 bug。

第 49 条:考虑用异常筛选器来改写先捕获异常再重新抛出的逻辑

  • 如果改用异常筛选器来捕获并处理异常,那么以后诊断起来就会容易一些,而不会令应用程序的开销增大。多使用异常筛选器,而不要在 catch 子句里面通过条件语句去分析异常。
  • 异常筛选器是针对 catch 子句所写的表达式,它出现在 catch 右侧那个 when 关键字之后,用来限定该子句所能捕获的异常。
  • 采用异常筛选器会给程序带来正面影响。.NET CLR 对带有 when 关键字的 try/catch 结构做了优化,使得程序在无须进入该结构时其性能尽量不受影响。
  • 如果仅通过异常的类型不足以判断出自己到底能不能处理该异常,那么可以考虑给相关的 catch 子句添加筛选器,使得程序只有在筛选条件得以满足时才会进入这个 catch 块。

第 50 条:合理利用异常筛选器的副作用来实现某些效果

  • 系统在寻找 catch 子句的过程中会执行这些筛选器,而此时,调用栈还没有真正展开。(于是,不妨利用这一特性来实现某种效果)
  • 放在异常筛选器中的那个方法必须总是返回 false,绝对不能返回 true,否则异常将不会继续传播(when 之后的条件为 true 时,将展开 catch 之后的子句)。你在这用情况下可以使用 Expection 基类作为类型,但属特例。一般情况下都应该使用 Exception 的子类。
  • catch (Exception e) when log(e) {},异常可以继续传播,不会干扰到程序的正常运行。log(e)返回 false。
  • 只要程序进程与 debugger 相连,Debugger.IsAttached 属性就返回 true,无论你构建的是 debug 版还是 release 版都是如此。
你觉得这篇文章怎么样?
  • 0
  • 0
  • 0
  • 0
  • 0
  • 0
评论
  • 按正序
  • 按倒序
  • 按热度