Swift协议的进化之路:深入理解不透明类型与装箱类型

1,170 阅读4分钟

相信大家在使用Swift协议的关联类型时,遇到过下面的编译错误。

    protocol Parser {
        var name: String { get }
        associatedtype File
        func parse(with file: File)
    }
    func print(parser: Parser) {}

此时示例的print会有编译报错。Use of protocol 'Parser' as a type must be written 'any Parser'

解决方案1:

    func printV1(parser: any Parser) {}

这里编译给出的修改建议是让我们加上关键字any,表明这是一个盒子类型Boxed Type,编译时不确定参数的实际类型,运行时才需要确定参数的实际类型。这里编译器让我们添加关键字any的意图是告诉我们这里会有性能问题,因为需要运行时判断具体类型。注意在Swift6中,对于任何用作参数的协议类型,编译会报错,并提示可能需要使用any

解决方案2

    func printV1<T: Parser>(parser: T) {}

这里使用了泛型,无论你的参数是什么类型,编译时总能确定类型,相较于方案1,性能较好。

解决方案3

    func printV1(parser: some Parser) {}

这里使用了some关键字,表明这是一个不透明类型Opaque Type,编译时能确定类型,相较于方案1,性能较好。而且它和Boxed Type一样,能够对客户隐藏具体类型,但是编译器知道具体类型,很好的保护了信息泄露。

接下来我们在深入探讨下不透明类型装箱类型

不透明类型

我们使用关键字some来表达不透明类型。望文生义的话,我们猜测到这种类型可以隐藏信息,而事实的确如此。那么它产生的背景是什么,用来解决什么样的问题?请思考以下例子。

 protocol Shape {
    func area() -> Double
}
struct Circle: Shape {
    var radius: Double
    func area() -> Double {
        return Double.pi * radius * radius
    }
}
func makeShape(flag: Bool) -> some Shape {
    return flag ? Circle(radius: 5) : Circle(radius: 50)
}

使用some关键字使得makeShape函数返回值隐藏了具体类型,客户端只能知道这是一个符合Shape协议的类型,客户端不需要知道具体类型,从而减少了使用API的复杂性。但是如果返回值使用条件表达式返回不同的Shape类型,那么编译器会报错 - 类型不匹配

    //error: Result values in '? :' expression have mismatching types 'Circle' and 'Rectangle'
    func makeShape(flag: Bool) -> some Shape {
        return flag ? Circle(radius: 5) : Rectangle(width: 4, height: 6)
    }

这是因为使用some关键字时,需要在编译时就需要确定函数返回值具体类型,而示例中返回值不是同一类型,所以编译报错。

装箱类型

我们使用any关键字来表示装箱类型,装箱类型也隐藏了具体类型类型信息,和不透明类型不同的是,需要等到运行时才能确定具体类型信息。我们可以使用any关键字改造makeShape函数

    func makeShape(flag: Bool) -> any Shape {
        return flag ? Circle(radius: 5) : Rectangle(width: 4, height: 6)
    }

此时,编译器不需要确定函数的具体类型,只需要类型遵循Shape协议,所以编译通过。

总结

在Swift中,someany分别代表了不透明类型和装箱类型,它们各自的作用和使用场景有所不同。some关键字引入了不透明类型,在编译时确定类型但对外隐藏了具体类型的实现细节。它适合用于简化API的复杂性,同时提升了性能,因为编译器在编译期就能确定实际的类型。

另一方面,any关键字用于表示装箱类型,这种类型在运行时动态决定具体类型。这种灵活性带来了一些性能开销,因为需要在运行时进行类型判断。装箱类型适用于在编译时无法确定具体类型或者需要在运行时处理多种类型的场景。

通过这两种不同的类型系统,Swift为开发者提供了更大的灵活性,允许我们在性能和抽象之间做出权衡。在实际开发中,选择some还是any,取决于具体场景的需求——如果可以在编译时确定类型且需要较好的性能,优先考虑使用some;而在需要更高的灵活性或者必须在运行时确定类型时,any则是更好的选择。

了解这两者的区别和使用场景,对于编写高效、简洁的Swift代码至关重要。