This is my code in my main target(so not the test target):
protocol ProtocolA {
func dontCrash()
}
extension ProtocolA {
func dontCrash() {
fatalError()
}
func tryCrash() {
dontCrash()
}
}
class MyClass: ProtocolA {}
In my test target (so different target), I got this code:
import XCTest
@testable import Project
extension MyClass {
func dontCrash() {
print("I dont crash")
}
}
class ProjectTests: XCTestCase {
func testExample() {
MyClass().tryCrash()
}
}
It crashes. Why it doesn't use the dynamic dispatching mechanism? MyClass
has it's own implementation of dontCrash()
, I expect that one to fire.
Your
Project
module declaresMyClass
's conformance toProtocolA
.Swift implements that conformance using a data structure called a “protocol witness table”. For each method declared by the protocol, the witness table contains a function that calls the actual implementation of the method for the conforming type.
To be concrete, there is a witness table for the conformance of
MyClass
toProtocolA
. That witness table contains a function for thedontCrash
method declared byProtocolA
. That function in the witness table calls theMyClass
dontCrash
method.You can see the function from the protocol witness table in the stack trace when your test case hits
fatalError
:Frame #10 is the call from
tryCrash
to the function in the protocol witness table. Frame #9 is the call from the protocol witness table function to the actual implementation ofdontCrash
.Swift emits the protocol witness table in the module that declares the conformance. So, in your case, the witness table is part of the
Project
module.Your override of
dontCrash
in your test bundle cannot change the contents of the witness table. It's too late for that. The witness table was fully defined when Swift generated theProject
module.Here's why it has to be this way:
Suppose I'm the author of the
Project
module and you're just a user of it. When I wrote theProject
module, I knew callingMyClass().dontCrash()
would callfatalError
, and I relied on this behavior. In many places insideProject
, I calledMyClass().dontCrash()
specifically because I knew it would callfatalError
. You, as a user ofProject
, don't know how muchProject
depends on that behavior.Now you use the
Project
module in your app, but you retroactively changeMyClass().dontCrash()
to not callfatalError
. Now all those places whereProject
callsMyClass().dontCrash()
don't behave in the way that I expected when I wrote theProject
module. You have broken theProject
module, even though you didn't change the source code of theProject
module or any of the modules thatProject
imports.It's critical to the correct operation of the
Project
module that this not happen. So the only way to change whatMyClass().dontCrash()
means (when called from inside theProject
module) is to change the source code of theProject
module itself (or change the source code of something thatProject
imports).