Optionals vs Throwing functions

前端 未结 3 573
星月不相逢
星月不相逢 2021-02-04 01:56

Consider the following lookup function that I wrote, which is using optionals and optional binding, reports a message if key is not found in the dictionary

func          


        
3条回答
  •  被撕碎了的回忆
    2021-02-04 02:15

    Performance

    The two approaches should have comparable performance. Under the hood they are both doing very similar things: returning a value with a flag that is checked, and only if the flag shows the result is valid, proceeding. With optionals, that flag is the enum (.None vs .Some), with throws that flag is an implicit one that triggers the jump to the catch block.

    It's worth noting your two functions don't do the same thing (one returns nil if no key matches, the other throws if the first key doesn’t match).

    If performance is critical, then you can write this to run much faster by eliminating the unnecessary key-subscript lookup like so:

    func lookUp(key:T , dictionary:[T:T]) -> T?  {
        for (k,v) in dictionary where k == key {
            return v
        }
        return nil
    }
    

    and

    func lookUpThrows(key:T , dictionary:[T:T]) throws -> T  {
        for (k,v) in dic where k == key {
            return v
        }
        throw lookUpErrors.noSuchKeyInDictionary
    }
    

    If you benchmark both of these with valid values in a tight loop, they perform identically. If you benchmark them with invalid values, the optional version performs about twice the speed, so presumably actually throwing has a small bit of overhead. But probably not anything noticeable unless you really are calling this function in a very tight loop and anticipating a lot of failures.

    Which is safer?

    They're both identically safe. In neither case can you call the function and then accidentally use an invalid result. The compiler forces you to either unwrap the optional, or catch the error.

    In both cases, you can bypass the safety checks:

    // force-unwrap the optional
    let name = lookUp( "JO", dictionary: dict)!
    // force-ignore the throw
    let name = try! lookUpThrows("JO" , dic:dict)
    

    It really comes down to which style of forcing the caller to handle possible failure is preferable.

    Which function is recommended?

    While this is more subjective, I think the answer’s pretty clear. You should use the optional one and not the throwing one.

    For language style guidance, we need only look at the standard library. Dictionary already has a key-based lookup (which this function duplicates), and it returns an optional.

    The big reason optional is a better choice is that in this function, there is only one thing that can go wrong. When nil is returned, it is for one reason only and that is that the key is not present in the dictionary. There is no circumstance where the function needs to indicate which reason of several that it threw, and the reason nil is returned should be completely obvious to the caller.

    If on the other hand there were multiple reasons, and maybe the function needs to return an explanation (for example, a function that does a network call, that might fail because of network failure, or because of corrupt data), then an error classifying the failure and maybe including some error text would be a better choice.

    The other reason optional is better in this case is that failure might even be expected/common. Errors are more for unusual/unexpected failures. The benefit of returning an optional is it’s very easy to use other optional features to handle it - for example optional chaining (lookUp("JO", dic:dict)?.uppercaseString) or defaulting using nil-coalescing (lookUp("JO", dic:dict) ?? "Team not found"). By contrast, try/catch is a bit of a pain to set up and use, unless the caller really wants "exceptional" error handling i.e. is going to do a bunch of stuff, some of which can fail, but wants to collect that failure handling down at the bottom.

提交回复
热议问题