• 译 文 address: How to use Result in Swift 5
  • By Paul Hudson
  • The Nuggets translation Project
  • Permanent link to this article: github.com/xitu/gold-m…
  • Translator: Bruce – pac
  • Proofreader: iWeslie

Se-0235 introduces a Result type into the standard library, making it easier and clearer to handle errors in complex code, such as asynchronous apis. This is something that people have been asking for since the early days of Swift, so it’s great to see it finally coming!

Swift’s Result type is implemented as an enumeration that has two cases: success and failure. Both are implemented using generics, so they can have associated values of your choice, but failure must conform to Swift’s Error type.

To demonstrate Result, we can write a network request function that calculates how many unread messages are waiting for the user. In this example code, we will have only one possible error, which is that the requested URL string is not a valid URL:

enum NetworkError: Error {
    case badURL
}
Copy the code

The function will take a URL string as its first argument and a Completion closure as its second argument. The Completion closure itself accepts a Result, where success will store an integer, and the failure case will be some kind of NetworkError. We’re not actually going to connect to the server here, but using a Completion closure will at least allow us to emulate asynchronous code.

The code is as follows:

func fetchUnreadCount1(from urlString: String, completionHandler: @escaping (Result<Int, NetworkError>) -> Void)  {
    guard let url = URL(string: urlString) else {
        completionHandler(.failure(.badURL))
        return
    }
    
    // complicated networking code here
    print("Fetching \(url.absoluteString)...")
    completionHandler(.success(5))}Copy the code

To use this code, we need to check the value in our Result to see if our call succeeded or failed, as follows:

fetchUnreadCount1(from: "https://www.hackingwithswift.com") { result in
    switch result {
    case .success(let count) :print("\ [count) unread messages.")
    case .failure(let error):
        print(error.localizedDescription)
    }
}
Copy the code

Even in this simple scenario, Result gives us two benefits. First, the error we return is now strongly typed: it must be some kind of NetworkError. Swift’s normal throw functions do not check for type, so they can throw any type of error. So if you add a switch statement to see what happens to them, you need to add the default case, even if that case is impossible. Using the strongly typed error of Result, we can create an exhaustive switch statement by listing all the cases of the error enumeration.

Second, it is now clear that we will either return success data or an error, and one and only one of them will definitely return. If we override fetchUnreadCount1() to complete the Completion closure using the traditional Objective-C method, you can see the importance of the second benefit:

func fetchUnreadCount2(from urlString: String, completionHandler: @escaping (Int? , NetworkError?) -> Void) {
    guard let url = URL(string: urlString) else {
        completionHandler(nil, .badURL)
        return
    }
    
    print("Fetching \(url.absoluteString)...")
    completionHandler(5.nil)}Copy the code

Here, the Completion closure will receive both an integer and an error, although either of them could be nil. Objective-c uses this approach because it doesn’t have the ability to represent enumerations with associated values, so it has no choice but to send both back and let the user figure it out.

However, this approach means that we have gone from two possible states to four: an integer with no errors, an error with no integers, an error and an integer, and no integers and no errors. The last two states should be impossible, but until Swift introduced Result, there was no easy way to express this.

This happens all the time. The dataTask() method in URLSession uses the same solution, for example: it uses (Data? , URLResponse? , Error?) . This could give us some data, a response and an error, or any combination of the three — a situation Swift Evolution’s proposal calls “awkward”.

You can think of Result as a super-powerful Optional that encapsulates a successful value, but can also encapsulate a second case that represents no value. However, for Result, the second case can also pass extra data because it tells us what went wrong, not just nil.

Why not usethrows?

When you first see Result, you often wonder why it’s useful, especially since Swift 2.0 has had a very good throws keyword to handle errors.

You can do much the same thing by having the Completion closure accept another function, which throws or returns the data in question, as follows:

func fetchUnreadCount3(from urlString: String, completionHandler: @escaping  ((a) throws -> Int) - >Void) {
    guard let url = URL(string: urlString) else {
        completionHandler { throw NetworkError.badURL }
        return
    }
    
    print("Fetching \(url.absoluteString)...")
    completionHandler { return 5}}Copy the code

You can then call fetchUnreadCount3() using a Completion closure that accepts the function you want to run, as follows:

fetchUnreadCount3(from: "https://www.hackingwithswift.com") { resultFunction in
    do {
        let count = try resultFunction()
        print("\ [count) unread messages.")}catch {
        print(error.localizedDescription)
    }
}
Copy the code

That also solves the problem, but it’s a lot more complicated to read. Worse, we don’t actually know what the result() function is called for, so if it does more than just return a value or throw a value, it can cause problems of its own.

Even with simpler code, using throws often forces us to process errors immediately rather than storing them for later processing. With Result, the problem goes away, and the error is stored in a value that we can read when we’re ready.

To deal withResult

We’ve seen how the Switch statement allows us to evaluate the success and failure cases of Result in a clean way, but there are five more things you should know before you start using it.

First, Result has a get() method that returns success if it exists and throws an error otherwise. This allows you to convert Result to a regular throw call, as follows:

fetchUnreadCount1(from: "https://www.hackingwithswift.com") { result in
    if let count = try? result.get() {
        print("\ [count) unread messages.")}}Copy the code

Second, you can use regular if statements to read enumerations if you like, although some people find the syntax a bit odd. Such as:

fetchUnreadCount1(from: "https://www.hackingwithswift.com") { result in
    if case .success(let count) = result {
        print("\ [count) unread messages.")}}Copy the code

Third, Result has an initializer that accepts a closure that might throw an error: if the closure returns a value successfully, that value is used in the success case, otherwise the thrown error will be put into the failure case.

Such as:

let result = Result { try String(contentsOfFile: someFile) }
Copy the code

Fourth, you can also use the general Error protocol instead of using a specific Error enumeration that you create. In fact, the Swift Evolution proposal says, “Expect most uses of Result to use swift.error as an Error type parameter.”

Therefore, instead of Result

, Result

can be used. While this means you lose the security of type throws, you gain the ability to throw a variety of different error enumerations — which one you prefer really depends on your coding style.
,>
,>

Finally, if you already have a custom Result type in your project (anything you define yourself or import from the custom Result type on GitHub), they will automatically replace Swift’s own Result type. This will allow you to upgrade to Swift 5.0 without breaking the code, but ideally over time you will migrate to Swift’s own Result type to avoid incompatibility with other projects.

conversionResult

Result has four other methods that might prove useful :map(), flatMap(), mapError(), and flatMapError(). Each of these methods can convert success or error in some way, and the first two behave similarly to the method of the same name on Optional.

The map() method looks inside Result and converts the success value to a value of another type using the specified closure. However, if it finds a failure, it just uses it directly and ignores your transformation.

To demonstrate this, we’ll write some code that generates a random number between 0 and the maximum, and then compute the factors of that number. If the user requests a random number that is less than zero, or if that random number happens to be prime, that is, it has no factors other than itself and 1, we consider these to be failure cases.

We can start by writing code to model two possible failure cases: the user tries to generate a random number less than 0 and the generated random number is prime:

enum FactorError: Error {
    case belowMinimum
    case isPrime
}
Copy the code

Next, we’ll write a function that takes a maximum value and returns either a random number or an error:

func generateRandomNumber(maximum: Int) -> Result<Int.FactorError> {
    if maximum < 0 {
       // creating a range below 0 will crash, so refuse
            return .failure(.belowMinimum)
        } else {
            let number = Int.random(in: 0. maximum)return .success(number)
        }
    }
Copy the code

When it is called, we return Result as either an integer or an error, so we can use map() to convert it:

let result1 = generateRandomNumber(maximum: 11)
let stringNumber = result1.map { "The random number is: \ [$0)." }
Copy the code

When we pass in a valid maximum value, result1 will be a successful random number. So using map() takes this random number, uses it with string interpolation, and returns another Result type, this time Result< string, FactorError>.

However, if we use generateRandomNumber(maximum: -11), result1 will be set to the failure case of FactorError. BelowMinimum. Therefore, using map() still returns Result

, but it has the same failure condition and the same FactorError. BelowMinimum error.
,>

Now that you’ve seen how map() allows us to convert the success type to another type, let’s move on. We have a random number, so the next step is to calculate its factors. To do this, we’ll write another function that takes a number and calculates its factors. If it finds that the number isPrime, it returns a failure Result with an isPrime error, otherwise it returns the number of factors.

Here’s the code:

func calculateFactors(for number: Int) -> Result<Int.FactorError> {
    let factors = (1. number).filter { number % $0= =0 }
    
    if factors.count= =2 {
        return .failure(.isPrime)
    } else {
        return .success(factors.count)}}Copy the code

If we wanted to use map() to convert the output of generateRandomNumber() to generate random numbers and then calculateFactors(), it would look like this:

let result2 = generateRandomNumber(maximum: 10)
let mapResult = result2.map { calculateFactors(for: $0)}Copy the code

However, this makes mapResult a rather ugly type :Result

, FactorError>. It is a Result inside another Result.

As with the optional values, now is the time for the flatMap() method to come into play. If your transformation closure returns a Result, flatMap() will return the new Result directly, rather than wrapping it inside another Result:

let flatMapResult = result2.flatMap { calculateFactors(for: $0)}Copy the code

So, mapResult is a Result

, FactorError>, FlatMapResult is flattened to Result

– The first original success value (a random number) is converted to a new success value (the number of factors). Just like map(), if one of the results fails, the flatMapResult will also fail.
,>

As for mapError() and flatMapError(), they perform similar operations, except that they convert the error value instead of the success value.

The next?

I’ve written about some other great new features in Swift 5 that you might want to check out:

  • How to use custom string interpolation in Swift 5
  • How to use @dynamicCallable in Swift
  • Swift 5.0 is changing optional try
  • What’s new in Swift 5.0

You may also want to try out my What’s New in Swift 5.0 playground, which allows you to interactively try out the new features of Swift 5.

If you want to learn more about the result type in Swift, you might want to check out the source code for Antitypical /Result on GitHub, which is one of the most popular result implementations.

I also highly recommend reading Matt Gallagher’s Excellent Discussion of Result, which is a few years old but still useful and interesting.

You’re already busy updating your app for Swift 4.2 and iOS 12, so why not let Instabug help you find and fix bugs? Just add two lines of code to your project and you’ll receive a comprehensive report with all the feedback you need to publish a world-class application – click here to learn more!

If you find any mistakes in your translation or other areas that need to be improved, you are welcome to the Nuggets Translation Program to revise and PR your translation, and you can also get the corresponding reward points. The permanent link to this article at the beginning of this article is the MarkDown link to this article on GitHub.


The Nuggets Translation Project is a community that translates quality Internet technical articles from English sharing articles on nuggets. The content covers Android, iOS, front-end, back-end, blockchain, products, design, artificial intelligence and other fields. If you want to see more high-quality translation, please continue to pay attention to the Translation plan of Digging Gold, the official Weibo, Zhihu column.