Ree*_*sey 12
CLR中的异常非常丰富,并提供了大量细节.Rico Mariani发表了一篇关于CLR 中例外成本的(旧的,但仍然相关的)帖子,详细介绍了其中的一些内容.
因此,在CLR中抛出异常的相对成本高于其他一些环境,包括OCaml.
在这种情况下,适当修改代码的最佳方法是什么?
如果您希望在正常的非特殊情况下引发异常,则可以以完全避免异常的方式重新考虑您的算法和API.例如,尝试提供一个替代API,您可以在引发异常之前测试环境.
为什么,因为CLR,如果F#比在OCaml中抛出异常更昂贵?
OCaml针对使用异常作为控制流进行了大量优化.相比之下,.NET中的异常根本没有得到优化.
请注意,性能差异很大.OCaml的例外情况比F#快600倍左右.根据我的基准测试,即使C++在这方面也比OCaml快6倍左右.
尽管.NET异常据称提供了更多(OCaml提供了堆栈跟踪,你还想要什么?)我想不出有什么理由为什么它们应该像它们一样慢.
在这种情况下,适当修改代码的最佳方法是什么?
在F#中,您应该编写"total"函数.这意味着您的函数应返回一个联合类型的值,表示结果的类型,例如正常或异常.
特别是,调用find应该被调用替换为tryFind返回值为value的option类型的Some值,或者None如果关键元素不存在于集合中.
Reed已经解释了为什么.NET异常的行为与OCaml异常不同.通常,.NET异常仅适用于特殊情况,并且是为此目的而设计的.OCaml具有更轻量级的模型,因此它们也用于实现一些控制流模式.
举一个具体的例子,在OCaml中你可以使用异常来实现循环的中断.例如,假设您有一个函数test可以测试数字是否是我们想要的数字.以下迭代数字从1到100并返回第一个匹配的数字:
// Simple exception used to return the result
exception Returned of int
try
  // Iterate over numbers and throw if we find matching number
  for n in 0 .. 100 do
    printfn "Testing: %d" n
    if test n then raise (Returned n)
  -1                 // Return -1 if not found
with Returned r -> r // Return the result here
要实现这一点,没有例外,您有两个选择.您可以编写具有相同行为的递归函数 - 如果您调用find 0(并且它被编译为与在C#中使用return n内部for循环基本相同的IL代码):
let rec find n = 
  printfn "Testing: %d" n
  if n > 100 then -1  // Return -1 if not found
  elif test n then n  // Return the first found result 
  else find (n + 1)   // Continue iterating
使用递归函数的编码可能有点长,但您也可以使用F#库提供的标准函数.这通常是重写将使用OCaml异常用于控制流的代码的最佳方法.在这种情况下,您可以写:
// Find the first value matching the 'test' predicate
let res = seq { 0 .. 100 } |> Seq.tryFind test
// This returns an option type which is 'None' if the value 
// was not found and 'Some(x)' if the value was found.
// You can use pattern matching to return '-1' in the default case:
match res with
| None -> -1
| Some n -> n
如果您不熟悉选项类型,请查看一些介绍性材料.F#wikibook有一个很好的教程,MSDN文档也有很多有用的例子.
使用Seq模块中的适当函数通常会使代码缩短很多,因此它更受欢迎.它可能比直接使用递归效率稍低,但在大多数情况下,您不必担心这一点.
编辑:我对实际表现感到好奇.Seq.tryFind如果输入是延迟生成的序列seq { 1 .. 100 }而不是列表[ 1 .. 100 ](由于列表分配的成本),则使用的版本更有效.通过这些更改以及test返回第25个元素的函数,在我的机器上运行代码100000次所需的时间是:
exceptions   2.400sec  
recursion    0.013sec  
Seq.tryFind  0.240sec
这是非常简单的样本,所以我认为使用的解决方案Seq通常不会比使用递归编写的等效代码慢10倍.减速可能是由于额外数据结构的分配(表示序列,闭包的对象......)以及由于额外的间接(代码需要大量虚拟方法调用,而不仅仅是简单的数字操作和跳转).但是,异常甚至更昂贵,并且不会以任何方式使代码更短或更具可读性......