Swift 5.1 (21) - 泛型

级别: ★☆☆☆☆
标签:「iOS」「Swift 5.1 」「泛型」
作者: 沐灵洛
审校: QiShare团队


使用泛型能让我们写出灵活的,可复用的函数和类型,这些函数和类型会根据我们定义的要求与任何类型一起使用。使用泛型我们不仅可以避免重复的代码而且能以更加清晰抽象的方式表达代码意图。

泛型是Swift最强大的特征之一,并且许多Swift的标准库都是使用泛型的代码编译的。

泛型解决的问题

func swapTwoInts(_ a: inout Int, _ b: inout Int) {
    let temporaryA = a
    a = b
    b = temporaryA
}
func swapTwoStrings(_ a: inout String, _ b: inout String) {
    let temporaryA = a
    a = b
    b = temporaryA
}
func swapTwoDoubles(_ a: inout Double, _ b: inout Double) {
    let temporaryA = a
    a = b
    b = temporaryA
}

上述三个函数,分别在交换对应类型的两个值。但回归函数的本质,其实这三个函数的主体是一样的,只不过他们分别接受IntDoubleString三种类型作为函数的入参。
基于此编写一个函数,能交换任何类型的两个值,会更有用,也更灵活。

泛型函数

//MARK:编写泛型函数
func swapTwoValues<T>(_ a : inout T, _  b : inout T) {
    let temp = a
    a = b
    b = temp
}
//调用
var a = "Qishare"
var b = "Come On"
swapTwoValues(&a, &b)
print(a,b)//!< Come On Qishare

泛型函数中会使用占位符类型代替实际的类型名称,如IntStringDouble等。本例中此占位类型的名称为T,此占位符类型名称并未指定T到底是什么,而是表示无论T代表什么类型,ab都必须具备相同的T类型。每次调用swapTwoValues(_:_:)函数时,Swift都需要进行类型推断,确定代替T使用的实际类型。

泛型函数与非泛型函数的区别在于,编写泛型函数时,函数的名称后需要使用尖括号< >,并在其中指定占位符类型的名称:<T><>用以告诉Swift函数T是此函数定义的占位符类型名称。因为T是一个占位符类型,所以Swift不会查找T的实际类型。

类型参数

类型参数 :泛型函数调用时,可以被函数实际类型代替的参数。在泛型函数名称后尖括号中指定并命名后,意味着指定了类型参数,我们便可以使用此类型参数来定义函数的参数,函数的返回值类型。当然也可以采用<T,Q,...>的形式定义多个类型参数。

命名类型参数

在大多数情况下,类型参数命名是具有描述性的,例如Dictionary <Key,Value>中的KeyValue以及Array <Element>中的Element,它向我们展示了类型参数与我们所使用的泛型类型或泛型函数之间的关系。但是,当它们之间没有有意义的关系时,通常会使用单个字母(例如TUV)来命名它们。

注意:请始终为类型参数提供驼峰式的大写名称(例如TMyTypeParameter),以表明它们是类型的占位符,而不是值。

泛型类型

除了泛型函数,Swift还允许我们能定义自己的泛型类型,涵盖类,结构体,枚举类型,并可以与任何类型一起使用。和字典或数组相似。

接下来我们将定义一个栈的结构体类型,命名为Stack,定义Stack类型之前,我们需要知道栈结构的特点是:先入后出,后入先出。
1.定义只能存储特定类型的栈

struct Stack {
    var items = [Int]()
    mutating func push(_ item:Int){
        items.append(item)
    }
    mutating func pop(_ item:Int) -> Int {
       return items.removeLast()
    }
}
//调用
var stack_int = Stack()
stack_int.push(7)
stack_int.push(3)
stack_int.push(2)
print(stack_int)//Stack(items: [7, 3, 2])

如若我需要存储其他类型呢?
2.定义泛型类型Stack

struct Stack<Element> {
    var items = [Element]()
    mutating func push(_ item:Element){
        items.append(item)
    }
    mutating func pop(_ item:Element) -> Element {
        return items.removeLast()
    }
}
//调用
var stack_int = Stack<Int>()
stack_int.push(7)
stack_int.push(3)
stack_int.push(2)
print(stack_int)
var stack_string = Stack<String>()
stack_string.push("QISHARE")
print(stack_string)

注意:泛型类型Stack具有一个称为Element的类型参数,而不是Int的实际类型。Element是为此泛型类型定义的占位符类型,在结构体定义中的任何位置都可以使用Element来引用未来调用时的实际类型。

泛型类型的扩展

当扩展一个泛型类型的时候,我们不需要提供类型参数的列表作为此扩展定义的一部分。因为,定义泛型类型时定义好的类型参数在其扩展中依旧时可用的。

extension Stack {
    var topItem : Element? {
        items.last
    }
}
//调用
var stack_string = Stack<String>()
stack_string.push("QISHARE")
if let topItem = stack_string.topItem {
   print(topItem)//!< QISHARE
}

类型约束

泛型函数和泛型类型虽然可以与任何类型一起使用,但是有时我们需要强制限制可以一起使用的类型,这个时候就需要使用类型约束。比如:Swift中DictionaryKey便被约束为必须遵守hashable协议。
类型约束:指定类型参数必须继承自特定的类、遵守某个协议或协议组合。

类型约束的语法

语法:参数类型定义时,参数名称后放置单独的类或协议约束,约束与 参数名称之间使用冒号:隔开。
注意:类型的约束条件只能为类或协议。

func someFunction<T: SomeClass, U: SomeProtocol>(someT: T, someU: U) {
    // `T`约束为继承自`SomeClass`的类型 `U`约束为遵守`SomeProtocol` 协议的类型
}

类型约束的使用

func findIndex<T>(of valueToFind: T, in array:[T]) -> Int? {
    for (index, value) in array.enumerated() {
        if value == valueToFind {
            return index
        }
    }
    return nil
}

上述代码编译时会出现出错:Binary operator '==' cannot be applied to two 'T' operands(操作数)。因为==操作符在Swift中不是所有类型都支持。比如,我们自定义的类型,只有只有实现了Swift标准库定义的Equatable协议,才能运用==!=来比较该类型的任意的两个值。因此正确的写法是需要添加类型约束的:

func findIndex<T : Equatable>(of valueToFind: T, in array:[T]) -> Int? {
    for (index, value) in array.enumerated() {
        if value == valueToFind {
            return index
        }
    }
    return nil
}

类型关联

当我们定义协议时,有时声明一个或多个关联类型作为协议定义的一部分是很有用的。
关联类型的作用,主要提供某个类型的占位名称,然后作为协议的一部分去使用。关联类型的实际使用类型直到协议被实现时才会指定。关联类型使用关键字associatedtype指定。

类型关联的使用

//定义协议使用类型关联
protocol Container {
    associatedtype Item
    mutating func append(_ item : Item)
    var count : Int{get}
    subscript(i:Int)->Item{get}
    
}
//定义整型Stack类型
struct IntStack : Container {
    var items = [Int]()
    mutating func push(_ item:Int){
        items.append(item)
    }
    mutating func pop(_ item:Int) -> Int {
        return items.removeLast()
    }
    //实现协议时,需要明确关联类型的实际类型
    typealias Item = Int //!< ①

    mutating func append(_ item: Item) {//!< ①若不存在,此处可直接 Int
        push(item)
    }
    
    var count: Int {
        items.count
    }
    subscript(i: Int) -> Int {
        items[i]
    }
}

Typealias Item = Int是针对Container协议的实现,将Item的抽象类型转换为Int的具体类型。基于Swift的类型推断,通过append(_ :)方法便可以推断出Item的类型以及下标返回值的类型。
采用关联类型作为协议定义的一部分时,此协议也可以被泛型类型实现。

struct Stack<Element> : Container {
    var items = [Element]()
    mutating func push(_ item:Element){
        items.append(item)
    }
    mutating func pop(_ item:Element) -> Element {
        return items.removeLast()
    }
    //实现协议
    typealias Item = Element
    //自动提示为`Element`
    mutating func append(_ item: Element) {
        push(item)
    }
    var count: Int {
        items.count
    }
    subscript(i: Int) -> Element {
        items[i]
    }
}

扩展现有类型以指定关联类型

上篇协议中我们知道,当特定类型已经实现了协议的要求,但尚未声明该类型遵守协议。可以通过扩展声明该类型遵守此协议。当协议中定义了关联类型同样也是可以的。

比如:SwiftArray类型已经提供了Container协议中方法,属性要求的实现,完全匹配Container协议要求。这意味着我们通过Array的扩展声明Array遵守Container协议,并且Array内部对于协议要求的实现可以推断出协议关联类型Item的实际类型。

extension Array : Container{}
//扩展现有类型以指定关联类型?是否成功。
extension Array : Container{
    func associateTypeOne(_:Item){}
    func associateTypeTwo(_:Element){}
    func associateTypeThree(_ : Self){}//实现协议时,Self都会与协议实现类型进行关联
}

值得注意的是:在我们定义这个扩展之后,we can use any Array as a Container ? 实际上此处知识点还需自己探索一番。
若我们有一个具体未使用关联类型的协议Int_Container

protocol Int_Container {
    mutating func append(_ item : Int)
    var count : Int{get}
    subscript(i:Int)->Int{get}
}
  1. 定义函数,参数为协议类型。
func testProtocolWithAssociateTypeOne(_ parameter : Container) {
/*报错:Protocol 'Container' can only be used as a generic 
constraint because it has Self or associated type requirements*/
}
func testProtocolNoAssociatetype(_ parameter : Int_Container){
    //编译成功
}

2.使用isas判断某个类型是否遵守特定协议

let array : [Any] = [1,"ddd",3]
if array is Container {
 /*报错:Protocol 'Container' can only be used as a generic 
constraint because it has Self or associated type requirements*/
    print("遵守此协议")
} else {
    print("不遵守此协议")
}
if array is Int_Container {
    print("遵守此协议")
} else {
    print("不遵守此协议")
}

上述1,2的示例中,带有关联类型的协议,不管是作为函数的参数类型或对象的属性类型,还是单独判断某个类型是否遵守此协议,都会报错:Protocol 'Container' can only be used as a generic constraint because it has Self or associated type requirements。编译器告诉我们Container协议有Self或关联类型的要求,因此它只能被用来作为泛型的约束。
关于Self的提示:系统库为协议提供了Self关联类型,默认指向了实现此协议的类型。

//系统的`Equatable `协议
public protocol Equatable {
    static func == (lhs: Self, rhs: Self) -> Bool
}
//实际实现
class Person : Equatable {
    //默认关联`Self`到`Person`
    static func == (lhs: Person, rhs: Person) -> Bool {
        return lhs.name == rhs.name
    }
    var name : String?
    var age : String?
}

若我们的Int_Container协议定义中使用了关联类型Self,编译器依旧会报此错误。

protocol Int_Container {
    mutating func append(_ item : Int)
    var count : Int{get}
    subscript(i:Int)->Int{get}
    static func testCompare(l:Self,r:Self)->Bool
}

对比泛型和协议的关联类型:

  • 泛型:使用占位符类型完成泛型类型方法的实现,泛型的实际类型由使用此泛型类型者指定。即:使用时指定实际类型。
  • 关联类型:使用占位符类型完成协议方法的定义,关联类型的实际类型由实现此协议者指定,即:实现时指定实际类型。

关联类型的协议用作泛型的约束举例:

//①
struct TempStruct<T:Container> {
    let title : String = "关联类型的协议用作泛型类型的约束:代替`T`的实际类型必须遵守`Container`协议"
    func showYourMagic(_ para : T) -> Void {
        print(para)
    }
}
//②
func showYourMagic<T>(_ para : T) -> Void {
    print(para)
}
showYourMagic("展示魔法")

总结:带有关联类型的协议只能用作泛型的约束。

添加约束到关联类型

可以为协议中的关联类型添加类型约束,以要求符合条件的类型满足这些约束。

protocol Container {
    associatedtype Item : Equatable
    mutating func append(_ item : Item)
    var count : Int{get}
    subscript(i:Int)->Item{get}
}

在关联类型的约束中使用协议

在关联类型的约束中使用协议,协议可以作为协议要求的一部分出现。(当前协议可作为关联类型的协议要求出现)。
Container协议举例,定义协议SuffixableContainer继承自Container,实现功能:实现此协议类型的实例,需要截取它后缀一定长度,组成新的实例。

//协议定义
//定义继承协议
protocol SuffixableContainer : Container {
    
    /*新构建的关联类型`suffix`约束条件有两个:
     1.实现此协议时指定的`suffix`的类型必须是实现`SuffixableContainer`协议的类型
     2.此`suffix`占位的容器类型的存储项类型`Item`必须与当前实现此协议的存储项保持一致。
     */
    associatedtype suffix : SuffixableContainer where suffix.Item == Item
    
    /*`item`关联类型的实际类型由泛型类型的占位类型决定。
     此方法必须确保`String`类型的容器,截取的后缀,重组后的容器仍然是`String`类型的*/
    func suffix(_ size : Int) -> suffix
    
}
//实现
extension Stack : SuffixableContainer {
    func suffix(_ size: Int) -> Stack {
        var result = Stack()
        for index in (count-size)..<count {
            result.append(self[index])
        }
        return result
    }
}
//调用
var stack_int = Stack<Int>()
stack_int.push(7)
stack_int.push(3)
stack_int.push(2)
stack_int.append(4)
let suffix_int = stack_int.suffix(3)
print(stack_int,suffix_int)//3 2 4

上述示例,SuffixableContainer协议的关联类型suffix使用了SuffixableContainer协议进行约束。
基于suffix(_ : ) -> suffix此方法必须确保String类型的特定容器,截取的后缀,重组后的容器仍然是String类型的此容器。解释一下关于关联类型suffix的约束:

  • 实现此协议时指定的suffix的类型必须是实现SuffixableContainer协议的类型。
  • suffix占位的容器类型的存储项类型Item必须与当前实现此协议类型(调用类型)的存储项保持一致。item关联类型的实际类型由泛型类型的占位类型决定。

泛型的where闭包

where闭包能要求关联类型必须遵守某个特定的协议,或特定的类型参数与关联类型必须相等。where闭包以where关键字开始,后跟关联类型的约束或类型参数与关联类型之间的相等关系。我们可以在类型或函数主体的大括号前写一个通用的where子句来设置我们的约束。
以匹配两个容器是否相等的功能举例来阐述。

func twoContainerIsEqual<C1:Container,C2:Container>(_ someContainer : C1 , _ anotherContainer : C2) -> Bool where C1.Item == C2.Item , C2.Item : Equatable {
    /*where闭包对于关联类型的约束:1.容器元素类型一致,
      2.元素的类型遵守`Equatable`协议*/
    if someContainer.count != anotherContainer.count {
        return false
    }
    for i in 0..<someContainer.count {
        if someContainer[i] != anotherContainer[i] {
            return false
        }
    }
    return true
}

使用where闭包扩展泛型

1.where闭包可以作为泛型扩展的一部分。

extension Stack where Element: Equatable {
    func isTop(_ item: Element) -> Bool {
        guard let topItem = items.last else {
            return false
        }
        return topItem == item
    }
}
//调用
struct NotEquatable {
}
var notEquatableStack = Stack<NotEquatable>()
let notEquatableValue = NotEquatable()
notEquatableStack.push(notEquatableValue)

/* Error:Argument type 'NotEquatable' 
 does not conform to expected type 'Equatable'*/
notEquatableStack.isTop(notEquatableValue)

2.where闭包可以作为协议扩展的一部分。

/*
协议通过扩展可以为遵守协议的类型提供方法,初始化,下
标和计算属性的实现。这一点允许我们为协议本身定义行
为,而不是基于遵守协议的每个类型
*/
extension Container where Item: Equatable {
//若`startsWith`函数名不与`container`中要求重名,则`startsWith`便是为遵守此协议的类型增加了新的方法。
    func startsWith(_ item: Item) -> Bool {
        return count >= 1 && self[0] == item
    }
}

3.where闭包,可以要求Container协议Item为特定类型。

extension Container where Item == Double {
    func average() -> Double {
        var sum = 0.0
        for index in 0..<count {
            sum += self[index]
        }
        return sum / Double(count)
    }
}

关联类型使用泛型 where闭包。

关联类型上使用泛型where子句。
例如,为Container 协议增加迭代器的功能。

protocol Container {
    associatedtype Item
    mutating func append(_ item: Item)
    var count: Int { get }
    subscript(i: Int) -> Item { get }
    
    associatedtype Iterator : IteratorProtocol where Iterator.Element == Item
    func makeIterator() -> Iterator
}
//构建迭代器
struct Iterator<T> : IteratorProtocol{
    
    var stack : Stack<T>
    var count = 0
    
    init(_ stack : Stack<T>) {
        self.stack = stack
    }
    
    typealias Element = T

    mutating func next() -> T? {
        let next = stack.count - 1 - count
        guard next >= 0 else {
            return nil
        }
        count += 1
        return stack[next]
    }
}
//我们的泛型`Stack`需要实现`Sequence`协议
struct Stack<Element> : Container,Sequence {
    
    //container只能用作泛型约束。
    var items = [Element]()
    mutating func push(_ item:Element){
        items.append(item)
    }
    mutating func pop(_ item:Element) -> Element {
        return items.removeLast()
    }
    //实现协议
    typealias Item = Element
    //自动提示为`Element`
    mutating func append(_ item: Element) {
        push(item)
    }
    var count: Int {
        items.count
    }
    subscript(i: Int) -> Element {
        items[i]
    }
    //迭代器的实现
    typealias IteratorType = Iterator<Element>
    func makeIterator() -> IteratorType {
        return Iterator.init(self)
    }
}
//调用
var stack_int = Stack<Int>()
stack_int.push(7)
stack_int.push(3)
stack_int.push(2)
stack_int.append(4)
for item in stack_int {
    print(item)
}
//输出:
4
2
3
7

Iterator : IteratorProtocol where Iterator.Element == Item要求Iterator必须遍历与容器的元素具有相同类型的元素,而不管迭代器的类型。

泛型下标

下标可以泛型,也可以包括泛型where子句,下标后的尖括号内写占位符类型名称,并在下标正文的左花括号前写泛型where子句。

extension Container {
    subscript<Indices: Sequence>(indices: Indices) -> [Item]
        where Indices.Iterator.Element == Int {
            var result = [Item]()
            for index in indices {
                result.append(self[index])
            }
            return result
    }
}

Indices.Iterator.Element == Int保证了序列中的索引与用于容器的索引具有相同的类型。即:意味着为索引参数传递的值是整数序列。

参考资料:
swift 5.1官方编程指南


推荐文章:
WWDC2020(0623) 开发者大会观看记录
Swift 5.1 (20) - 协议
Swift 5.1 (19) - 扩展
Swift 5.1 (18) - 嵌套类型
Swift 5.1 (17) - 类型转换与模式匹配
浅谈编译过程
深入理解HTTPS
浅谈 GPU 及 “App渲染流程”
iOS 查看及导出项目运行日志
Flutter Platform Channel 使用与源码分析
开发没切图怎么办?矢量图标(iconFont)上手指南
DarkMode、WKWebView、苹果登录是否必须适配?

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,013评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,205评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,370评论 0 342
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,168评论 1 278
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,153评论 5 371
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,954评论 1 283
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,271评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,916评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,382评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,877评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,989评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,624评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,209评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,199评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,418评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,401评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,700评论 2 345