在Swift中使用Do / Catch的好处

I’ve always found throwing functions (try/catch or do/catch) to be a very underrated feature in the Swift community. While many native APIs from iOS rely on it, very few people seem to actually use them in their daily projects. Most people seem to either optionally unwrap them (try?) or use other features like the Result type. In my opinion, this stems from the fact that unless your entire project is built around throwing, the parts that do use it are somewhat annoying to deal with and don't look very nice code-wise:

我一直发现throw函数( try/catchdo/catch )在Swift社区中是一个被低估的功能。 尽管iOS的许多本机API都依赖它,但似乎很少有人在其日常项目中实际使用它们。 大多数人似乎要么选择解开它们( try? ),要么使用其他功能(例如Result类型)。 在我看来,这是由于这样一个事实,除非您的整个项目都是围绕throw构建的,否则使用它的部分在处理时会有些烦人,而且在代码方面看起来也不是很好:

I’ve personally avoided this feature a lot for these reasons in favor of things like promises or the new Result<> type, but even then, I still felt that my way of handling errors wasn't good. In fact, I was often falling into the same pitfalls — just in different ways.

由于这些原因,我亲自避免了此功能,而选择了promises或新的Result<>类型,但是即使那样,我仍然觉得我处理错误的方法不好。 实际上,我经常陷入相同的陷阱–只是以不同的方式。

When I recently refactored one of my open-source CLI tools, I noticed that parts of it would not only look better if they used throwing functions instead, but they also would be considerably easier to unit-test. I decided to try it out by refactoring that tool to use this feature, but to avoid falling into that same pit, I made the entire tool rely on it. The results exceeded all of my expectations. With about 80% code coverage, the tool is now very easy to maintain and evolve thanks to the benefits of throwing functions.

当我最近重构我的一个开源CLI工具时,我注意到它的某些部分不仅可以改用throwing函数看起来更好,而且可以大大简化单元测试。 我决定通过重构该工具以使用此功能来进行尝试,但是为了避免陷入同一困境,我让整个工具都依赖于此。 结果超出了我的所有期望。 由于具有抛出功能的优点,该工具现在具有约80%的代码覆盖率,因此非常易于维护和发展。

In this article, I’ll identify the benefits of this feature that have caught my attention the most and share some of my thoughts on why you should give it a second chance.

在本文中,我将确定此功能的优点引起了我的最大关注,并分享了我对为什么应该给它第二次机会的一些想法。

抛出函数通过让您专注于重要的事情来清理代码 (Throwing Functions Clean Your Code by Allowing You to Focus on What Matters)

When you need the value of something wrapped in the Result type, you must switch its result immediately. This will make your method responsible for handling any errors associated with that result:

当需要使用Result类型包装的值时,必须立即切换其结果。 这将使您的方法负责处理与该结果相关的任何错误:

This method now has two responsibilities that must ideally be covered by unit tests, which might not be your intention.

现在,此方法具有两项职责,理想情况下,单元测试必须涵盖这两项职责,而这并不是您的意图。

When you use try in a method that is itself throws, you can delegate the treatment of errors to the code that is actually interested in it. This allows you to develop methods that only handle their success cases:

当您在本身throws的方法中使用try时,可以将错误的处理委托给对它真正感兴趣的代码。 这使您能够开发仅处理成功案例的方法:

func getUserProfile() throws {
let profileSchema = try database.profileSchema
return Profile(profileSchema)
}

As you can see, I don’t need to worry about the database failing to fetch the user’s profile schema in this specific method because that’s not the point of it — someone else will handle it if it happens. With these changes, this method is now so short that it possibly doesn’t even need to exist anymore. It could be refactored to a simple try Profile(database: database) call (initializers can also be throws!). This benefit is especially visible when your access depends on multiple things that can fail:

如您所见,我不必担心数据库无法以这种特定方法获取用户的概要文件架构,因为这不是问题的重点-如果发生这种情况,其他人将处理它。 经过这些更改,此方法现在太短了,甚至可能不再需要。 可以将其重构为一个简单的尝试Profile(database: database)调用(初始化程序也可以throws !)。 当您的访问取决于可能失败的多种情况时,此好处尤其明显:

Without throwing functions, you would probably have to divide this operation into multiple methods.

如果不抛出函数,则可能必须将此操作分为多种方法。

投掷函数使您可以更好地设计/单元测试致命问题 (Throwing Functions Allow You to Better Design/Unit-Test Fatal Problems)

In CLI tools, it’s common to halt everything or cause a crash when something goes wrong, like failing to open a file:

在CLI工具中,通常会在出现问题(例如无法打开文件)时停止所有操作或导致崩溃:

Not only is this method impossible to unit-test by itself, but other unit tests might also trip these failure conditions and crash your test bundle entirely. While your iOS app probably doesn’t crash in these conditions, I have seen my share of similar conditions (using an optional try?, returning things like nil or an empty string, logging the occurrence and having the methods that rely on this information be able to treat these special cases). This can be made better by using Result, but that can make you fall back to the previous issue: Your methods now do more than they have to.

不仅这种方法本身无法进行单元测试,而且其他单元测试也可能使这些失败情况失效,并使测试包完全崩溃。 虽然您的iOS应用在这种情况下可能不会崩溃,但我看到了我的类似情况(使用可选的try? 、返回nil或空字符串之类的东西,记录事件的发生以及使依赖此信息的方法成为能够治疗这些特殊情况)。 通过使用Result可以使此方法更好,但这可以使您退回到上一个问题:您的方法现在可以做的比他们要做的要多。

Similarly to the previous benefit, you can use throws here and defer the actual crash/failure to someone who is actually interested in it. In the case of my CLI tool, all fatal conditions will throw a special FatalError that only results in a crash if handled by main.swift. In fact, main.swift is the only part of the code that even attempts to handle errors. Everything in the tool is delegated to it, which makes the tool's code considerably cleaner.

与以前的好处类似,您可以在此处使用throws ,然后将实际的崩溃/失败延迟给对它真正感兴趣的人。 就我的CLI工具而言,所有致命条件都将引发特殊的FatalError ,如果由main.swift处理,只会导致崩溃。 实际上, main.swift是代码中甚至唯一尝试处理错误的部分。 该工具中的所有内容都委托给它,这使该工具的代码更加清晰。

func obfuscate(file: File) throws {
let contents = try open(file)
let obfuscatedVersion = try obfuscate(string: contents)
try save(contents, toFile: file)
}

You can now unit-test that this method succeeds if everything is fine and proceed with your life. It’s not necessary to unit-test this method’s specific failure conditions because the errors are not only not coming from it, but it also doesn’t handle them — it just sends them downstream.

现在,如果一切正常,就可以对这种方法成功进行单元测试,并继续您的生活。 不必对这种方法的特定故障条件进行单元测试,因为错误不仅不是由它引起的,而且它也不能处理它们-只是将它们发送到下游。

For reference, here’s an example of a method in my CLI tool that generates a failure condition:

作为参考,这是我的CLI工具中生成失败条件的方法的示例:

Custom errors can be made by creating enums that conform to Error. Personally, I like making my custom error inherit from LocalizedError to make error.localizedDescription return a custom description. This can be done by implementing its errorDescription property (implementing localizedDescription directly doesn't work):

可以通过创建符合Error枚举来进行自定义Error 。 就个人而言,我喜欢使自定义错误继承自LocalizedError以使error.localizedDescription返回自定义描述。 这可以通过实现其errorDescription属性来实现(直接实现localizedDescription不起作用):

XCTestCase对抛出方法有特殊的支持 (XCTestCase Has Special Support for Throwing Methods)

Perhaps my favorite benefit is that XCTestCase can automatically handle failures in throwing functions. Here’s a classic example of how would I unit-test something that used Result:

也许我最喜欢的好处是XCTestCase可以自动处理抛出函数中的故障。 这是一个经典示例,说明如何对使用Result单元测试:

Having to bypass error conditions tests is very annoying. Fortunately, XCTestCase allows you to mark any test method as throws, making it automatically fail the test if it throws. By making the example's getAResult() become a throwing getAString() instead, you can refactor this test to a single line and completely ignore the failure conditions:

必须绕过错误条件测试非常烦人。 幸运的是, XCTestCase允许您将任何测试方法标记为throws ,使其在throws时自动通过测试。 通过使示例的getAResult()变成getAString() ,您可以将此测试重构为一行,并完全忽略失败条件:

func testSomethingUsingTryCatch() throws {
XCTAssertEqual(try getAString(), "aString")
}

If you would like to do the reverse, which is testing if something fails, there’s no need to switch the result — you can use the special XCTAssertThrowsError method. You can also use XCTAssertNoThrow to test whether something succeeds when the result itself isn't what’s being tested.

如果您要进行相反的操作(即测试是否失败),则无需切换结果-您可以使用特殊的XCTAssertThrowsError方法。 您也可以使用XCTAssertNoThrow来测试结果本身不是正在测试的内容时是否成功。

In general, what I like about this is that I don’t need to consider failure cases when the test subject itself isn’t the one throwing the errors. If I want to test that this method is working, all I have to do is test its success cases. If anything fails upstream, the test will throw an error and fail. This makes unit testing considerably easier and faster while still being very durable (if not more durable, in my opinion).

通常,我喜欢的是,当测试对象本身不是抛出错误的对象时,我不需要考虑失败案例。 如果我想测试该方法是否有效,我要做的就是测试其成功案例。 如果上游有任何失败,则测试将引发错误并失败。 这使得单元测试变得相当容易和快捷,同时仍然非常耐用(我认为如果不是耐用的话)。

结果可以从/向投掷函数转换 (Result Can Be Translated From/to Throwing Functions)

Although Result is sometimes seen as the opposite of throwing functions, they are actually somewhat interchangeable. It's possible to build Result types from throwing operations and get throwing operations from existing Result instances, which might be helpful if you'd like to play with throwing functions in a project without fully committing to it.

尽管有时将Result视为与throwing函数相反,但它们实际上可以互换。 可以通过抛出操作构建Result类型,并从现有Result实例中获得抛出操作,如果您想在项目中完全不使用它们的情况下使用抛出函数,这可能会很有帮助。

let result: Result<String, Error> = Result(catching: { try file.read() })
let contents = try result.get()

结论 (Conclusion)

Using throwing functions has been extremely beneficial in my new project, but as I said in the beginning, you might find that most of these benefits only apply if your project uses them to handle all errors. Still, even if you don’t have a full project, you can use Result's special initializers to treat the gaps and benefit from cleaner methods and more durable unit tests.

在我的新项目中,使用throwing函数一直是非常有益的,但是正如我在一开始所说的那样,您可能会发现,只有当您的项目使用它们来处理所有错误时,这些好处中的大多数才适用。 尽管如此,即使您没有完整的项目,也可以使用Result的特殊初始化程序来处理差距,并受益于更干净的方法和更持久的单元测试。

The project in question is SwiftShield. Make sure to check it out (especially the test cases) to see how these benefits are translated to code.

有问题的项目是SwiftShield 。 确保检查出来(尤其是测试用例),以查看这些好处如何转换为代码。

翻译自: https://medium.com/better-programming/benefits-of-using-throwing-functions-try-swifts-most-underrated-feature-d37eb4c8ebaf

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值