Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Dynamic/runtime dispatch in Swift, or "the strange way structs behave in one man's opinion"

I'm not horribly new to Swift, nor to Objective-C, but I saw some odd behavior when working with an Error subtype today that led me to dig a little deeper.

When working with an NSString subclass (yes, the below example functions similarly for classes not based on NSObject):

import Foundation

// Class version
class OddString : NSString {
    override var description: String {
        return "No way, José"
    }
}

let odd = OddString()

func printIt(_ string: NSString) {
    print(string.description)
}

print(odd.description)
printIt(odd)

I see what I expect to see:

No way, José
No way, José

However, when I write (what I think is) the equivalent code using a struct (Error) instead:

import Foundation

// Struct version
struct TestError : Error {
    var localizedDescription: String {
        return "I am a TestError"
    }
}

let explosive = TestError()

func printIt(_ error : Error) {
    print(error.localizedDescription)
}

print(explosive.localizedDescription)
printIt(explosive)

I see:

I am a TestError
The operation couldn’t be completed. (SanityChecks.TestError error 1.)

This is really confusing to me. Is it deciding at compile time what method will be invoked on the struct passed in to printIt, regardless of what type it actually is?

Further: Is this difference in runtime behavior between classes and structs documented in the Swift Programming Guide, and can someone reference the section? I haven't found anything on this yet.

like image 271
Craig Otis Avatar asked Feb 05 '23 00:02

Craig Otis


1 Answers

In your first example you're overriding the description property. This implementation is therefore added to OddString's vtable (as it's a class), and can be dynamically dispatched to just fine, regardless of what the instance is statically typed as.

In your second example, you don't have a class – so no vtables. However you are conforming to a protocol. Protocols allow for dynamic dispatch via protocol witness tables (see this great WWDC talk on them), however this only happens for implementations of protocol requirements.

localizedDescription isn't a protocol requirement of the Error protocol, it's merely defined in a protocol extension of Error when you import Foundation (this is documented in SE-0112). Therefore it cannot be dynamically dispatched. Instead, it will be statically dispatched – so the implementation called is dependant on the static type of the instance.

That's the behaviour you're seeing here – when your explosive instance is typed as TestError, your implementation of localizedDescription is called. When typed as Error, the implementation in the Error extension is called (which just does a bridge to NSError and gets its localizedDescription).

If you want to provide a localised description, then you should conform your error type to LocalizedError instead, which defines errorDescription as a protocol requirement – thus allowing for dynamically dispatch. See this Q&A for an example of how to go about this.

like image 116
Hamish Avatar answered Feb 08 '23 15:02

Hamish