Swift中不可恢复的错误的处理

前言


本文是一个示例,说明了我们如何研究Swift标准库的功能行为,不仅在库文档上而且还在其源代码上建立了我们的知识。


不可恢复的错误


程序员称为“错误”的所有事件都可以分为两种类型。


  • 由外部因素(例如网络连接故障)引起的事件。
  • 由程序员的错误引起的事件,例如到达开关操作员的情况,这是不可达的。

第一类事件在常规控制流中处理。 例如,我们通过向用户显示一条消息并设置一个应用程序以等待网络连接恢复来对网络故障做出反应。


我们尝试在代码投入生产之前尽早发现并消除第二种类型的事件。 这里的方法之一是运行一些运行时检查 ,以可调试状态终止程序执行 ,并打印一条消息,指出错误在代码中的何处发生。


例如,如果未提供但调用了所需的初始化器,则程序员可以终止执行。 在第一次测试运行期间,将始终注意到并修复该问题。


required init?(coder aDecoder: NSCoder) { fatalError("init(coder:) has not been implemented") } 

另一个例子是索引之间的切换(假设由于某种原因您不能使用枚举)。


 switch index { case 0: // something is done here case 1: // other thing is done here case 2: // and other thing is done here default: assertionFailure("Impossible index") } 

同样,程序员在这里调试时将导致崩溃,以便不可避免地注意到索引编制中的错误。


Swift标准库中有五个终止函数(与Swift 4.2一样)。


 func precondition(_ condition: @autoclosure () -> Bool, _ message: @autoclosure () -> String = default, file: StaticString = #file, line: UInt = #line) 

 func preconditionFailure(_ message: @autoclosure () -> String = default, file: StaticString = #file, line: UInt = #line) -> Never 

 func assert(_ condition: @autoclosure () -> Bool, _ message: @autoclosure () -> String = default, file: StaticString = #file, line: UInt = #line) 

 func assertionFailure(_ message: @autoclosure () -> String = default, file: StaticString = #file, line: UInt = #line) 

 func fatalError(_ message: @autoclosure () -> String = default, file: StaticString = #file, line: UInt = #line) -> Never 

我们应该选择五个终止功能中的哪个?


源代码与文档


让我们看一下源代码 。 我们可以立即看到以下内容:


  1. 这五个功能中的每个功能要么终止程序执行,要么不执行任何操作。
  2. 可能的终止有两种发生方式。
    • 通过调用_assertionFailure(_:_:file:line:flags:)打印方便的调试消息。
    • 仅通过调用Builtin.condfail(error._value)Builtin.int_trap()获得调试消息。
  3. 五个终止功能之间的差异在于发生以上所有情况的条件。
  4. fatalError(_:file:line)无条件调用_assertionFailure(_:_:file:line:flags:)
  5. 其他四个终止函数通过调用以下配置评估函数来评估条件。 (它们以下划线开头,这意味着它们是内部的,不应由使用Swift标准库的程序员直接调用)。
    • _isReleaseAssertConfiguration()
    • _isDebugAssertConfiguration()
    • _isFastAssertConfiguration()

现在让我们看一下文档 。 我们可以立即看到以下内容。


  1. fatalError(_:file:line) 无条件打印给定消息并停止执行
  2. 其他四个终止函数的效果根据所使用的构建标志而有所不同: -Onone-O-Ounchecked 。 例如,查看preconditionFailure(_:file:line:) 文档
  3. 我们可以通过SWIFT_OPTIMIZATION_LEVEL编译器构建设置在Xcode中设置这些构建标志。
  4. 从Xcode 10 文档中我们还知道引入了另一个优化标志-Osize
  5. 因此,我们要考虑四个优化构建标记。
    • -Onone (不优化)
    • -O (针对速度进行优化)
    • -Osize (针对大小进行优化)
    • -Ounchecked (关闭许多编译器检查)

我们可以得出结论,在四个终止函数中评估的配置是由这些构建标志设置的。


运行配置评估功能


尽管配置评估功能是为内部使用而设计的,但其中一些功能是出于测试目的而公开的 ,我们可以通过CLI在Bash中提供以下命令来进行尝试。


 $ echo 'print(_isFastAssertConfiguration())' >conf.swift $ swift conf.swift false $ swift -Onone conf.swift false $ swift -O conf.swift false $ swift -Osize conf.swift false $ swift -Ounchecked conf.swift true 

 $ echo 'print(_isDebugAssertConfiguration())' >conf.swift $ swift conf.swift true $ swift -Onone conf.swift true $ swift -O conf.swift false $ swift -Osize conf.swift false $ swift -Ounchecked conf.swift false 

这些测试和源代码检查使我们得出以下粗略结论。


有三种互斥的配置。


  • 通过提供-O-Osize构建标志来设置发布配置。
  • 通过提供-Onone构建标志或完全不提供优化标志来设置调试配置。
  • 如果设置了-Ounchecked构建标志,则_isFastAssertConfiguration()评估为true 。 尽管此函数的名称中包含“ fast”一词,但与优化速度-O build标志无关。

注意:这些结论并不是调试版本或发布版本何时发生的严格定义。 这是一个更复杂的问题。 但是这些结论对于终止函数用法的上下文是正确的。


简化图片


-Ounchecked


让我们不要看-Ounchecked标志 的作用(在这里无关紧要),而是看它在终止函数使用的上下文中的作用。


  • precondition(_:_:file:line:)assert(_:_:file:line:)说:“在-Ounchecked构建中,不评估条件,但是优化器可能会假定它始终评估为true。无法满足该假设是严重的编程错误。”
  • preconditionFailure(_:file:line)assertionFailure(_:file:line:)说:“在-Ounchecked构建中,优化器可能认为此函数从未调用过。未能满足该假设是严重的编程错误。 ”
  • 源代码中可以看出, 不应_isFastAssertConfiguration()评估为true 。 (如果确实发生,则会调用奇怪的_conditionallyUnreachable() 。请参见第136和 _conditionallyUnreachable() 。)

更直接地说,在为程序设置了-Ounchecked构建标志的情况下, 一定不能允许以下四个终止函数的可达性


  • precondition(_:_:file:line:)
  • preconditionFailure(_:file:line)
  • assert(_:_:file:line:)
  • assertionFailure(_:file:line:)

在应用-Ounchecked同时仅使用fatalError(_:file:line)同时允许使用fatalError(_:file:line)指令的程序点可以到达。


条件检查的作用


两个终止功能使我们可以检查条件。 通过源代码检查,我们可以看到,如果条件失败,则函数行为与其各自表亲的行为相同:


  • precondition(_:_:file:line:)变为preconditionFailure(_:file:line)
  • assert(_:_:file:line:)变为assertionFailure(_:file:line:)

该知识使进一步分析变得容易。


发布与调试配置


最终,进一步的文档和源代码检查使我们能够制定下表。


终止功能


现在很清楚,对于程序员而言,最重要的选择是如果运行时检查显示错误,则程序发行时应表现为什么样的行为。


这里的关键要点是assert(_:_:file:line:)assertionFailure(_:file:line:)使得程序失败的影响不那么严重。 例如,iOS应用可能已损坏UI(因为某些重要的运行时检查失败),但不会崩溃。


但是这种情况可能不是您想要的。 您可以选择。


Never返回类型


Never用作无条件引发错误,陷阱或无法正常终止的函数的返回类型。 这些功能实际上不会返回,它们永远不会返回。


在这五个终止函数中,只有preconditionFailure(_:file:line)fatalError(_:file:line)返回Never因为只有这两个函数无条件地停止了程序执行,因此永不返回。


这是利用Never输入命令行应用程序的一个很好的例子。 (尽管此示例不使用Swift标准库终止函数,而是使用标准C exit()函数)。


 func printUsagePromptAndExit() -> Never { print("Usage: command directory") exit(1) } guard CommandLine.argc == 2 else { printUsagePromptAndExit() } // ... 

如果printUsagePromptAndExit()返回Void而不是Never ,则会出现构建时错误,并显示以下消息:“ '守卫'主体不能掉落,请考虑使用'return'或'throw'退出范围 ”。 通过使用“ Never您可以事先声明您永远不会退出作用域,因此编译器不会给您提供构建时错误。 否则,您应该在保护代码块的末尾添加return ,这看起来不太好。


外卖


  • 如果您确定所有运行时检查仅与Debug配置相关,则使用哪个终止函数都没有关系。
  • 在应用-Ounchecked同时仅使用fatalError(_:file:line)同时允许使用fatalError(_:file:line)指令的程序点可以到达。
  • 如果您担心运行时检查可能会因发行而失败,请使用assert(_:_:file:line:)assertionFailure(_:file:line:) 。 至少您的应用程序不会崩溃。
  • 使用“ Never使您的代码看起来很整洁。


Source: https://habr.com/ru/post/zh-CN440884/


All Articles