原文:Beginner's guide to modern generic programming in Swift - The.Swift.Dev.
💡 了解有关协议、存在性、不透明类型的基础知识,以及它们如何与 Swift 中的泛型编程相关。
协议(与关联类型)
根据 Swift 语言指南,协议可以定义方法、属性和其他要求的蓝图。使用协议预定义属性和方法非常容易,语法非常简单,当我们开始使用关联类型时,问题就开始出现了。我们必须回答的第一个问题是:关联类型到底是什么?
关联类型是特定类型的通用占位符。直到协议被遵守并且在确切的类型由实现指定之前,我们不知道该类型。
protocol MyProtocol {
associatedtype MyType // 协议中的关联类型,只是一个占位符,没有类型!
var myVar: MyType { get }
func test()
}
extension MyProtocol {
func test() {
print("is this a test?")
}
}
struct MyIntStruct: MyProtocol {
typealias MyType = Int // 在遵守协议的类型中,描述了关联类型的 Type 是 Int 类型!
var myVar: Int { 42 }
}
struct MyStringStruct: MyProtocol {
let myVar = "Hello, World!" // 在这里,关联类型的 Type 是 String 类型!
}
let foo = MyIntStruct()
print(foo.myVar)
foo.test()
let bar = MyStringStruct()
print(bar.myVar)
bar.test()
如你所见,当我们实现协议时,关联的 MyType 占位符可以具有不同的类型。在第一种情况 (MyIntStruct) 中,我们通过使用类型别名明确告诉编译器使用 Int 类型,而在第二种情况 (MyStringStruct) 中,Swift 编译器足够聪明,可以根据提供的字符串值进行类型推断。
当然我们可以显式地写成 let myVar: String = "Hello, World!" 或使用计算属性或常规变量,这真的没关系。关键要点是,当我们使用两个结构实现协议时,我们已经定义了 MyType 占位符的类型。 🔑
你可以使用关联类型作为通用占位符对象,这样在需要支持多种不同类型时就不必重复代码。
存在类型 (any)
太棒了,我们的通用协议有一个默认的测试方法实现,我们可以在两个对象上使用,现在是这样,我真的不关心将要实现我的协议的类型,我只想调用这个测试函数并使用协议作为一种类型,我可以这样做吗?好吧,如果你使用的是 Swift 5.6+,那么答案是肯定的,否则...…
//
// ERROR:
//
// Protocol 'MyProtocol' can only be used as a generic constraint
// because it has Self or associated type requirements
//
let myObject: MyProtocol
// even better example, an array of different types using the same protocol
let items: [MyProtocol]
我敢打赌你之前已经看到过这个著名的错误消息。这里到底发生了什么?
答案很简单,编译器无法弄清楚协议实现的底层关联类型,因为它们可以是不同的类型(或者我应该说:运行时是动态的🤔),反正编译时也不是确定的。
最新版本的 Swift 编程语言通过引入一个新的 any 关键字解决了这个问题,这是一个类型擦除助手,它将最终类型装箱到一个可以用作存在类型的包装对象中。听起来很复杂?是的。 🤗
// ...
let myObject: any MyProtocol
let items: [any MyProtocol] = [MyIntStruct(), MyStringStruct()]
for item in items {
item.test()
}
通过使用 any 关键字,系统可以创建一个指向实际实现的不可见 box 类型,box 具有相同的类型,我们可以在其上调用共享接口函数。
- any HiddenMyProtocolBox: MyProtocol --- pointer ---> MyIntStruct
- any HiddenMyProtocolBox: MyProtocol --- pointer ---> MyStringStruct
这种方法允许我们将具有 Self 关联类型要求的不同协议实现放入一个数组中,并在两个对象上调用测试方法。
如果您真的想了解这些东西是如何工作的,我强烈建议您观看 Embrace Swift Generics WWDC22 会议视频。整个视频是一个宝石。 💎
如果您想了解更多关于泛型的知识,那么您绝对应该观看另一场名为 Swift 中的设计协议接口的会议。
从 Swift 5.7 开始,在创建存在类型时必须使用 any 关键字,这是一个重大更改,但它是为了更大的利益。我真的很喜欢 Apple 解决这个问题的方式,any 和 some 关键字都非常有用,但是理解差异可能很困难。 🤓
不透明类型 (some)
不透明类型可以隐藏值的类型信息。默认情况下,编译器可以推断出底层类型,但在协议具有关联类型的情况下,无法解析泛型类型信息,而这正是 some 关键字和不透明类型可以提供帮助的地方。
some 关键字是在 Swift 5.1 中引入的,如果你以前使用过 SwiftUI,你一定不会陌生。首先,它只是一个返回类型特性,但在 Swift 5.7 中,你现在也可以在函数参数中使用 some 关键字。
import SwiftUI
struct ContentView: View {
// the compiler knows that the body is always a Text type
var body: some View {
Text("Hello, World!")
}
}
通过使用 some 关键字,您可以告诉编译器您将处理特定的具体类型而不是协议,这样编译器可以执行额外的优化并查看实际的返回类型。这意味着您将无法为具有某些 “限制” 的变量分配不同的类型。 🧐
var foo: some MyProtocol = MyIntStruct()
// ERROR: Cannot assign value of type 'MyStringStruct' to type 'some MyProtocol'
foo = MyStringStruct()
不透明类型可用于隐藏实际的类型信息,您可以使用链接文章找到更多优秀的代码示例,但由于我的帖子主要关注泛型,因此我想向您展示一件与此主题相关的具体事情。
func example<T: MyProtocol>(_ value: T) {}
func example<T>(_ value: T) where T: MyProtocol {}
func example(_ value: some MyProtocol) {}
信不信由你,但上面的 3 个功能是相同的。
第一个是泛型函数,其中 T 占位符类型遵循 MyProtocol 协议。
第二个描述了完全相同的事情,但我们使用了 where 子句,这允许我们在需要时对关联类型进行进一步限制。例如 Where T:MyProtocol,T.MyType == Int。
第三个使用 some 关键字来隐藏类型,允许我们使用任何东西作为符合协议的函数参数。这是 Swift 5.7 中的一项新功能,它使通用语法更加简单。 🥳
如果你想了解更多关于 some 和 any 关键字之间的区别,你可以阅读 Donny Wals 的这篇文章,它真的很有帮助。
主要关联类型 (Protocol)
要约束不透明的结果类型,您可以使用 where 子句,或者我们可以使用一种或多种主要关联类型 “标记” 协议。这将允许我们在使用某些时对主要关联类型进行进一步的约束。
protocol MyProtocol<MyType> {
associatedtype MyType
var myVar: MyType { get }
func test()
}
//...
func example(_ value: some MyProtocol<Int>) {
print("asdf")
}
如果您想了解有关主要关联类型的更多信息,您也应该阅读 Donny 的文章。 💡
泛型 ()
到目前为止,我们还没有真正讨论 Swift 的标准泛型特性,但我们主要关注协议、关联类型、存在性和不透明类型。幸运的是,你在 Swift 中编写通用代码而不需要涉及所有这些东西。
struct Bag<T> {
var items: [T]
}
let bagOfInt = Bag<Int>(items: [4, 2, 0])
print(bagOfInt.items)
let bagOfString = Bag<String>(items: ["a", "b", "c"])
print(bagOfString.items)
这个 Bag 类型有一个名为 T 的占位符类型,它可以容纳任何类型的相同类型,当我们初始化包时,我们明确地告诉我们要使用哪种类型。在此示例中,我们使用 struct 创建了泛型类型,但你也可以使用枚举、类甚至角色,此外还可以编写更简单的泛型函数。 🧐
func myPrint<T>(_ value: T) {
print(value)
}
myPrint("hello")
myPrint(69)
如果你想了解更多关于泛型的知识,你应该阅读 Paul Hudson 的这篇文章,这是对 Swift 中泛型编程的一个很好的介绍。由于本文更多的是提供介绍,所以我不想深入探讨更高级的内容。泛型真的很难理解,尤其是当我们涉及协议和新关键字时。
我希望这篇文章能帮助您更好地理解这些事情。
谢谢。 🙏