Optionals vs Throwing functions

前端 未结 3 574
星月不相逢
星月不相逢 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<T:Equatable>(key:T , dictionary:[T:T]) -> T?  {
        for (k,v) in dictionary where k == key {
            return v
        }
        return nil
    }
    

    and

    func lookUpThrows<T:Equatable>(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.

    0 讨论(0)
  • 2021-02-04 02:27

    If you are using Swift 2.0 you could use both versions. The second version uses try/catch (introduced with 2.0) and is thus not backwards compatible which might be a disadvantage to consider.

    If there is any performance difference then it will be neglectable.

    My personal favorite is the first one as it is straight forward and well readable. If I had to do maintenance of the second version I would ask myself why the author took a try/catch approach for such a simple case. So I would rather be confused...

    If you had many complex conditions with many exit points (throws) then I would go for the second one. But as I said, this is not the case here.

    0 讨论(0)
  • 2021-02-04 02:30

    @AirspeedVelocity already has a great answer, but I think it's worth going a bit further into why to use optionals vs errors.

    There are basically four ways for something to go wrong:

    • Simple error: it fails in only one way, so you don't need to care about why something went wrong. The problem may come either from programmer logic or user data, so you need to be able to handle it at run time and design around it when coding.

      This is the case for things like initializing an Int from a String (either the string is parseable as an integer or it's not) or dictionary-style lookups (either there's a value for the key or there's not). Optionals work really well for this in Swift.

    • Logic error: this is the kind of error that (in theory) comes up only during development, as a result of Doing It Wrong — for example, indexing beyond the bounds of an array.

      In ObjC, NSException covers these kinds of cases. In Swift, we have functions like fatalError. I'd assume that part of why NSException isn't surfaced in Swift is that once your program encounters a logic error, it's not really safe to assume anything about its further operation. Logic errors should either be caught during development or cause a (nicely debuggable) crash rather than letting the program continue in an undefined (and thus unsafe) state.

    • Universal error: there are loads of ways to fail, but they aren't very connected to programmer logic or user action. You might run out of memory to allocate, get a low-level interrupt, or (wait for it...) overflow the stack, but those can happen with almost anything you do and not really because of any specific thing you do.

      You see universal errors getting surfaced as exceptions in some other languages, but that means that you have to code around the possibility of any and every call you make being able to fail. And at that point you're writing more error handling than you are actual code.

    • Recoverable error: This is for when there are lots of ways to go wrong, but not in ways that preclude further operation, and what a program does upon encountering an error might change depending on what kind of error it is. Filesystems and networking are the common examples here: if you can't load a file, it might be because the user got the name wrong (so you should tell the user that) or because the wifi momentarily dropped and will be back shortly (so you might forego the alert and just try again).

      In Cocoa, historically, this is what NSError parameters are for. Swift's error handling makes this pattern part of the language.

    So, when you're writing new API (for yourself or someone else to call) in Swift, or using new ObjC annotations to make an existing API easier to use from Swift, think about what kind of errors you're dealing with.

    • Is there only one clear way to fail that isn't a result of API misuse? Use an Optional return type.

    • Can something fail only if a client doesn't follow your API contract — say, if you're writing a container class that has a subscript and a count, or requiring that some specific sequence of calls be made? Don't burden every bit of code that uses your API with error handling or optional unwrapping — just fatalError or assert (or throw NSException if your Swift API is a front to ObjC code) and document what the right way is for people to use your API.

    • Okay, so your ObjC init method returns nil iff [super init] returns nil. So should you mark your initializer as failable for Swift or add an error out-parpameter? Think about when that really happens — if -[NSObject init] is returning nil, it's because you chained it off of an alloc call that returned nil. If alloc fails, it's already The End Times for your process, so it's not worth handling that case.

    • Do you have multiple failure cases, some or all of which might be worth reporting to a user? Or that a client calling your API might want to ignore some but not all of? Write a Swift function that throws and a corresponding set of ErrorType values, or an ObjC method that returns an NSError out-parameter.

    0 讨论(0)
提交回复
热议问题