起因
今天在写Swift的时候,遇到了一个奇怪的bug,代码大致是这样的:
protocol A {
}
protocol B: A {}
class C: B {}
protocol Test {
associatedType AProtocol: A
var a: AProtocol { get }
}
class TestImpl<T: A>: Test {
var a: T
init(a: T) {
self.a = a
}
}
let c: B = C()
let test = TestImpl(a: c)
上面的代码想要做的事情很简单,即将TestImpl只依赖ProtocolA而不依赖于它的具体实现
但Swift compiler却抛出一个错误:Type 'any B' cannot conform to 'A'
这我就纳闷了,B不是继承了A的吗?
于是我将代码改成了:
let c: A = C()
let test = TestImpl(a: c)
结果 Swift compiler 继续抛出Type 'any A' cannot conform to 'A'
惊了,A 居然连它自己都不conform?接下来就让我们一探究竟
Swift中的类型机制
写过Swift的人都或多或少的知道Protocol这个概念,它定义了一组行为,其中包括了构建器(constructor)、静态属性和方法。
他最主要的用处是,在声明一个值的类型时,可以把它声明为这个Protocol的类型
但和其他语言不同的是,它是不能被当作一个具体类型来使用的
什么是使用呢?在Swift中,除了声明的时候,其他任何时候用到的类型可以都认为是使用这个类型,比如:
- 当传递参数的时候
foo(a: aImpl) // aImpl 的 类型是被<使用>了 - 当声明泛型的类型时
foo<A>() // 类型A被<使用>了
注意,被使用的类型必须是一个具体的类型,也就是说,这个类型不能是一个Protocol:
protocol A {}
func foo(a: A) {}
let aImpl: A = ... // 一个A的具体实现
foo(a: aImpl) // X, 使用A是不被允许的
Swift为什么要这样设计?
感兴趣的朋友可以看一下这个Protocol doesn't conform to itself?
我认为这里最能说服我的一点即:
- Protocol能声明构造函数,如果它能够被当作具体类型来使用的话,那么当我调用它的构造函数时,Swift怎么知道我要构建哪一个类型的instance呢?
protocol A {
init()
}
class B: A {
init()
}
class C: A {
init()
}
class Test<T:A> {
func foo() -> T {
return T() // 如果swift只知道T是A的话,那么我怎么知道要调用B的init还是C的init?
}
}
PS: 这里我其实有点疑惑,为什么Swift要允许Protocol能加入init和静态属性方法的声明,这导致这个Protocol使用时的限制大的离谱,到现在我还没有找到一个合适优雅的方式进行mock
解决办法
了解了Swift的类型机制后,那么我们就可以解答上面遇到的问题了。
let c: B = C()
let test = TestImpl(a: c)
上面的错误是因为我们把c声明成了一个Protocol类型,然后将它作为参数传给了TestImpl的构造函数,这致使我们使用了Protocol类型,是不被允许的
既然这是语言上的限制,最好的办法就是遵循它,只使用具体类型,即
let c = C() // 编译器自动识别 c 为 C
let test = TestImpl(a: C)
当然,也有绕开它的方式,但是不推荐,具体方法可以参考Protocol doesn't conform to itself?中的回答