swift 带有关联类型的协议不可以做类型?

114 阅读7分钟

image.png 这里的错误提示其实跟关联类型没有什么关系,这是any的事儿 swift 中的some 和any和Any

一旦协议里出现了 associated type,它就不能再做普通类型,只能当作约束使用,也就是说不能当作变量/参数/返回值类型直接出现。”这种说法到底正确吗?我们尝试一下就知道了。

protocol aProtocol {
    associatedtype Item
    
    func test()
    func process(_ itemItem) -> Item
    func process2() -> Item
}
struct Astruct: aProtocol {
    typealias Item = Int
    func test() {
        print("test")
    }
    func process(_ itemItem) -> Item {
        item * 2
    }
    
    func process2() -> Item {
        8
    }
}
var p : any aProtocol = Astruct() //这个声明不会报错
p.test() //甚至可以调用方法
p.process(10//这句会报错,Member 'process' cannot be used on value of type 'any aProtocol'; consider using a generic constraint instead
let _result = p.process2() //可以调用

所以说"有关联类型的协议不能作为类型"是不准确的

这涉及到swift工具链版本,在不同的版本中表现不同。

例如在swift 5.6.3版本,不启用-enable-experimental-universal-existentials,就不允许有关联类型/Self 的 protocol 作为类型使用。

我当前使用的是swift6.0.3版本,在 Swift 5.7+ 中:

  1. -enable-explicit-existential-types成为默认行为

  2. -enable-experimental-universal-existentials不再需要单独设置(功能已集成)

  3. 主要关联类型(protocol)成为标准功能

如果你对swift版本感兴趣,可以关注我,后期会做一个总结

我目前使用6.0.3版本,此种环境就这个问题更准确的说法为:“有关联类型的协议可以作为类型使用,但涉及关联类型的方法/属性可能无法直接调用”,但是还是不够严谨,关联类型作为返回值似乎是ok的?要想说清楚这点,就不得不先了解swift编译器的类型检查机制。

其他的很关于这个的讲解文章会涉及协变和逆变,因此你也可以拓展看下这个 协变(Covariant)和逆变(Contravariant)

说回方法调用时的类型检查,其流程是这样的:

process()方法:

1、检查方法是否存在

这里涉及到类型擦除,编译器只需要查看aProtocol有没有这个方法。编译器知道 aProtocol 有 process 方法,因此方法存在。

2、检查参数类型是否匹配

  • 参数类型检查必须在调用时进行,这是静态类型系统的本质要求,防止运行时错误, 同时也防止了副作用和资源消耗等运行时问题

  • 编译器看到参数类型是 Item(关联类型),但编译器不知道 Item 的具体类型

  • 无法验证传入的 10 是否符合 Item 类型:如果 Item 是 String,那么传入 Int 就是错误的

  • 编译器无法在编译时确定这一点,因此拒绝调用

process2()方法

1、检查方法是否存在

编译器知道 aProtocol 有 process2 方法

2、检查参数类型是否匹配

没有参数,不需要检查

3、处理返回值

  • 返回值类型是 Item,但调用者不需要立即使用具体类型,因此返回值可以延迟类型检查(直到实际使用时)

  • 返回值可以返回一个"存在类型"(Existential Type)

  • 编译器可以接受这个返回值,即使不知道具体类型,因此可以调用

接下来我们再来从几个问题入手了解关联类型,他们大多都有重叠的地方。


聊一聊swift 关联类型 中提到关联类型也会有类似范型一样特化的部分,为什么不用特化以后的Item跟方法传入的参数做比较呢?

// swift源码: test/Generics/associated_types.swift:1-11

protocol Fooable {
  associatedtype AssocType
  func foo(_ x : AssocType)
}

struct X : Fooable {
  func foo(_ xFloat) {}
  // ↑ 编译器推断出:AssocType = Float
}

var xa : X.AssocType = Float()  // ✅ 可以使用 X.AssocType,它就是 Float

编译器会从方法签名推断关联类型,这是编译时确定的,不是运行时推断。

但是,关键是你是否有一些涉及到存在类型容器的内容

当使用 any aProtocol 声明变量时,Swift 会创建一个"存在类型容器":

//大致如下
struct ExistentialContainer {
    // 存储具体类型的实例
    let instance: Any
    // 存储方法表(vtable)
    let witnessTable: ProtocolWitnessTable
}

存在容器抹去了AssocType,因此也就无法使用它来做类型检查。

针对Fooable的例子

image.png 这也能解释原来的例子,对于 process2() -> Item, 调用时,编译器不知道 Item 的具体类型,但可以创建一个"类型擦除的返回值", 这个返回值被包装在存在类型中, 调用者可以接收它,但无法直接使用(除非知道具体类型)

image.png

对于 process(_ item: Item),调用时,编译器需要验证参数类型,但无法确定 Item 是什么类型,无法验证 10 是否符合 Item 类型。


参数类型检查必须在调用时进行?为什么不能使用存在容器包装?

因为需要类型信息来验证,使用存在容器包装Item做参数,或者说对Item做类型擦除也就无从谈起。

  structIntProcessor: Processor {
       funcprocess(_item: Item) -> Item {
           return item *2  // 需要知道 item 是 Int
       }
   }
  • 方法内部无法使用:item * 2 无法执行(Any 没有 * 运算符)

  • 需要运行时类型转换:if let int = item as? Int { ... }

  • 违反了类型契约:方法签名要求 Item,不是 Any

  • 失去了类型安全:可能导致运行时错误


你可能会问:既然 p 是 Astruct(),为什么不能推断出 Item = Int?

这也是因为类型擦除的特性:

var p: any aProtocol = Astruct()
  ↓
p 的类型是 "any aProtocol"(存在类型)
  ↓
编译器只知道 p 符合协议,不知道具体实现
  ↓
Item 的具体类型信息被"擦除"

即使运行时 p 确实是 Astruct,编译时编译器只能看到协议类型。

这是 Swift 的类型系统设计:编译时类型和运行时类型是分离的。

换句话说,类型擦除,就是蒙上编译器的眼睛,让他不去进行具类型检查,而是只检查protocol类型,甚至如果是Any,编译器几乎不需要做任何类型检查


为什么类型擦除一定要擦除关联类型item,为什么它不能像protocol的方法一样成为“盲人摸象”的一部分?类型擦除是为了抽象出统一特性,而Item,明显是这个整体中最不能抽象的,不同的实现,Item的类型各不相同,无法统一。


1、无关联类型的any protocol,该存在类型可以做参数

2、带有关联类型的protocol,其方法的参数的类型检查必须在调用时进行且不能使用存在容器包装

以上两个情况有没有涉及冲突的概念?

  protocol Drawable {
      func draw()
  }

  func drawShape(_shapeany Drawable) {  // ✅ 可以
      shape.draw()
  }
//draw() 方法不涉及关联类型
//可以统一检查:所有实现都有 draw() 方法
//可以调用 

这两种情况也是可抽象和不可抽象的区别。关联类型是不可以抽象的,这里是没有冲突的概念的。


你还可能会问:泛型函数为什么可以通过类型检查?

func processItem<T: aProtocol>(_ p: T, item: T.Item) -> T.Item {

return p.process(item)

}

T 是泛型参数,编译器会为每个具体类型创建特化版本, T.Item 是关联类型,但在泛型上下文中特化后,编译器也是知道 T 的具体类型,调用时:processItem(Astruct(), item: 10),特化的T = Astruct, 特化的T.Item = Int,编译器可以验证 10 是 Int,符合 T.Item,因而类型检查通过


var p: any MyProtocol where p.Item = Int = IntImpl()这种写法为什么不允许?这样的设计会导致:

1、失去多态性:p只能存储Item = Int的实现

2、与泛型功能重复:为什么不直接用泛型?

3、类型系统复杂化:需要额外的约束语法