Swift 范型(Generics)详解

一、泛型解决的问题

首先来看一个实际开发中经常遇到的简单问题,这是一个标准的非泛型函数swapTwoInts(::),它可以交换两个Int值:

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

var someInt = 3
var anotherInt = 107
swapTwoInts(&someInt, &anotherInt)
print("someInt is now \(someInt), and anotherInt is now \(anotherInt)")
// Prints "someInt is now 107, and anotherInt is now 3"

swapTwoInts(_:_:)函数很有用,但它只能与Int值一起使用。如果要交换两个String值或两个Double值,则必须编写更多函数,例如下面的swapTwoStrings(_:_:)swapTwoDoubles(_:_:)函数:

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
}

swapTwoInts(_:_:)swapTwoStrings(_:_:)swapTwoDoubles(_:_:)三个函数体的功能是相同的。唯一区别是它们接收的参数类型分别不同。我们可以用泛型函数来解决这种因参数类型不同,而处理逻辑相同而导致编写大量重复代码的问题。

二、泛型函数

泛型函数可以使用任何类型。这是上面函数swapTwoInts(_:_:)的通用版本,称为swapTwoValues(_:_:)

func swapTwoValues<T>(_ a: inout T, _ b: inout T) {
    let temporaryA = a
    a = b
    b = temporaryA
}

var someInt = 3
var anotherInt = 107
swapTwoValues(&someInt, &anotherInt)
// someInt is now 107, and anotherInt is now 3

var someString = "hello"
var anotherString = "world"
swapTwoValues(&someString, &anotherString)
// someString is now "world", and anotherString is now "hello"

上面的函数使用了一个称为T的占位符类型名,而不是实际的类型名。占位符类型名没有指明T必须是哪种类型,只是指明了参数ab必须是同一类型T。因为T是占位符,所以Swift不会查找T的实际类型。

三、类型参数

  • 类型参数指定并命名占位符类型,将其写在函数名之后的一对尖括号(<>)内。
  • 通过在尖括号(用逗号分隔)中写入多个类型参数名称,可以提供多个类型参数。
  • 在大多数情况下,类型参数具有描述性名称,例如Dictionary中的<Key, Value>Array中的<Element>,这说明了类型参数和它所使用的泛型类型或函数之间的关系。若它们之间没有任何有意义的关系,则通常使用单个字母命名,如T、U和V。

四、泛型类型

除泛型函数,Swift还允许自定义泛型类型,这些可以是自定义类,结构和枚举。
首先来看一个非泛型的堆栈示例:

struct IntStack {
    var items = [Int]()
    mutating func push(_ item: Int) {
        items.append(item)
    }
    mutating func pop() -> Int {
        return items.removeLast()
    }
}

该结构体使用一个名为items的数组属性将值存储在堆栈中。Stack提供了两种方法,push以及pop在堆栈中压入和弹出值。这些方法被标记为mutating,因为它们需要修改(或改变)结构体的items数组。
然而,上面显示的IntStack类型只能与Int值一起使用。定义一个泛型堆栈类会更实用,它可以管理任何类型值的堆栈。
下面再看一个泛型的堆栈示例:

struct Stack<Element> {
    var items = [Element]()
    mutating func push(_ item: Element) {
        items.append(item)
    }
    mutating func pop() -> Element {
        return items.removeLast()
    }
}

var stackOfStrings = Stack<String>()
stackOfStrings.push("uno")
stackOfStrings.push("dos")
stackOfStrings.push("tres")
stackOfStrings.push("cuatro")
// the stack now contains 4 strings

let fromTheTop = stackOfStrings.pop()
// fromTheTop is equal to "cuatro", and the stack now contains 3 strings

五、扩展一个泛型类型

在扩展泛型类型时,不需要提供类型参数列表作为扩展定义的一部分。相反,来自原始类型定义的类型参数列表在扩展的主体中可用,原始类型参数名称用于引用来自原始定义的类型参数。

extension Stack {
    var topItem: Element? {
        return items.isEmpty ? nil : items[items.count - 1]
    }
}

if let topItem = stackOfStrings.topItem {
    print("The top item on the stack is \(topItem).")
}
// Prints "The top item on the stack is tres."

六、类型约束

有时需要对泛型函数和泛型类型中使用的类型添加某些约束,使得只有满足该约束的类型才可以在泛型函数和泛型类型中使用。类型约束指定类型参数必须从特定的类继承,或符合特定的协议或协议组合。

(一)类型约束的语法

下面显示了泛型函数的类型约束的基本语法:

func someFunction<T: SomeClass, U: SomeProtocol>(someT: T, someU: U) {
    // function body goes here
}

上面的假设函数有两个类型参数。第一个类型参数的约束是: T必须是SomeClass的子类。第二个类型参数的约束是,U必须符合协议SomeProtocol

(二)类型约束的应用

首先来看一个示例,非泛型函数findIndex(ofString:in:)的功能是在字符串数组array中查找指定元素的索引值。

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

let strings = ["cat", "dog", "llama", "parakeet", "terrapin"]
if let foundIndex = findIndex(ofString: "llama", in: strings) {
    print("The index of llama is \(foundIndex)")
}
// Prints "The index of llama is 2"

上面的函数只适用于字符串类型。如果要适用于其他类型的数据,我们需要将其改成泛型函数,如下所示:

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

但上面的的函数无法编译。问题出在相等性检测if value == valueToFind。在Swift语言中,不是每种数据类型都可以使用等号操作符(==)进行比较。只用遵守并实现了Equatable协议的类型才可以使用==操作符。Swift中的所有标准类型都自动支持Equatable协议。
因此上面的函数的泛型T需要满足一定的条件,即只有遵守并实现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
}

let doubleIndex = findIndex(of: 9.3, in: [3.14159, 0.1, 0.25])
// doubleIndex is an optional Int with no value, because 9.3 isn't in the array
let stringIndex = findIndex(of: "Andrea", in: ["Mike", "Malcolm", "Andrea"])
// stringIndex is an optional Int containing a value of 2

七、关联类型

在定义协议时,有时将一个或多个关联类型声明为协议定义的一部分是很有用的。关联类型向作为协议一部分使用的类型提供占位符名称。在采用协议之前,不会指定用于关联类型的实际类型。关联类型用associatedtype关键字指定。

(一)关联类型应用

这是一个名为Container的协议示例,它声明了一个名为Item的关联类型:

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

下面的非泛型类IntStack遵守并实现协议Container的示例:

struct IntStack: Container {
    // original IntStack implementation
    var items = [Int]()
    mutating func push(_ item: Int) {
        items.append(item)
    }
    mutating func pop() -> Int {
        return items.removeLast()
    }
    // conformance to the Container protocol
    typealias Item = Int
    mutating func append(_ item: Int) {
        self.push(item)
    }
    var count: Int {
        return items.count
    }
    subscript(i: Int) -> Int {
        return items[i]
    }
}

还可以使泛型Stack类型来遵守并实现Container协议:

struct Stack<Element>: Container {
    // original Stack<Element> implementation
    var items = [Element]()
    mutating func push(_ item: Element) {
        items.append(item)
    }
    mutating func pop() -> Element {
        return items.removeLast()
    }
    // conformance to the Container protocol
    mutating func append(_ item: Element) {
        self.push(item)
    }
    var count: Int {
        return items.count
    }
    subscript(i: Int) -> Element {
        return items[i]
    }
}

这次,类型参数Element用作append(_:)方法的item参数的类型和下标的返回类型。因此,Swift可以推断出ElementItem用作这个特定容器的合适类型。

(二)扩展已有类型
  • 我们不仅可以通过扩展,使现有类型遵守一个协议,还可以使其遵守一个关联类型的协议。
    Swift的数组类型已经提供了一个append(_:)方法、一个count属性和一个带有Int索引的下标来检索其元素。这三个功能符合Container协议的要求。这意味着可以通过声明Array采用协议来扩展Array以符合Container协议。可以使用一个空扩展来完成此操作,正如在声明采用带有扩展的协议时所述:
extension Array: Container {}
(三)向关联类型添加约束
  • 可以向协议中的关联类型添加类型约束,以要求符合这些约束的类型满足这些约束。
    下例中,Container协议要求其中的元素类型要满足Equatable协议:
protocol Container {
    associatedtype Item: Equatable
    mutating func append(_ item: Item)
    var count: Int { get }
    subscript(i: Int) -> Item { get }
}
(四)在关联类型的约束中使用协议

协议可以作为其自身需求的一部分出现。例如,这里有一个改进Container协议的协议,添加了suffix(_:)方法的需求。suffix(_:)方法从容器末端返回给定数量的元素,并将它们存储在Suffix类型的实例中。

protocol SuffixableContainer: Container {
    associatedtype Suffix: SuffixableContainer where Suffix.Item == Item
    func suffix(_ size: Int) -> Suffix
}

在这个协议中,Suffix是一个关联类型,就像上面Container示例中的Item类型一样。Suffix有两个约束条件:

  • 它必须符合SuffixableContainer协议;
  • 它的 Item类型必须与容器的Item类型相同;
    Item的约束是一个泛型的where语句。
    下面是通过扩展,使Stack类遵守并实现SuffixableContainer协议的例子:
extension Stack: SuffixableContainer {
    func suffix(_ size: Int) -> Stack {
        var result = Stack()
        for index in (count-size)..<count {
            result.append(self[index])
        }
        return result
    }
    // Inferred that Suffix is Stack.
}
var stackOfInts = Stack<Int>()
stackOfInts.append(10)
stackOfInts.append(20)
stackOfInts.append(30)
let suffix = stackOfInts.suffix(2)
// suffix contains 20 and 30

在上面示例中,StackSuffix关联类型也是Stack,因此Stack上的suffix操作返回另一个Stack。另外,遵守SuffixableContainer协议的类型可以具有与其本身不同的Suffix关联类型——即方法suffix可以返回不同的类型。
例如,这里有一个非泛型IntStack类型的扩展,它遵守SuffixableContainer协议,使用Stack<Int>作为关联类型,而不是IntStack:

extension IntStack: SuffixableContainer {
    func suffix(_ size: Int) -> Stack<Int> {
        var result = Stack<Int>()
        for index in (count-size)..<count {
            result.append(self[index])
        }
        return result
    }
    // Inferred that Suffix is Stack<Int>.
}

八、泛型where语句

  • 泛型where语句让关联类型必须符合特定协议,或者特定类型参数和关联类型必须相同;
  • 泛型where语句以where关键字开头,其后是关联类型的约束或类型与关联类型之间的相等关系;
  • 泛型where语句写在 一个类型或函数体的左大括号前面;
func allItemsMatch<C1: Container, C2: Container>
    (_ someContainer: C1, _ anotherContainer: C2) -> Bool
    where C1.Item == C2.Item, C1.Item: Equatable {

        // Check that both containers contain the same number of items.
        if someContainer.count != anotherContainer.count {
            return false
        }

        // Check each pair of items to see if they're equivalent.
        for i in 0..<someContainer.count {
            if someContainer[i] != anotherContainer[i] {
                return false
            }
        }

        // All items match, so return true.
        return true
}

var stackOfStrings = Stack<String>()
stackOfStrings.push("uno")
stackOfStrings.push("dos")
stackOfStrings.push("tres")

var arrayOfStrings = ["uno", "dos", "tres"]

if allItemsMatch(stackOfStrings, arrayOfStrings) {
    print("All items match.")
} else {
    print("Not all items match.")
}
// Prints "All items match."

在上例中,要求函数的两个类型参数需要满足以下约束条件:

  • C1必须符合Container协议(即C1: Container)。
  • C2还必须符合Container协议(即C2: Container)。
  • C1的Item必须与C2的Item相同(即 C1.Item == C2.Item)。
  • C1的Item必须符合Equatable协议(即C1.Item: Equatable)。
    而函数的两个参数someContaineranotherContainer则需要满足以下约束:
  • someContainer是类型C1的容器。
  • anotherContainer是C2类型的容器。
  • someContaineranotherContainer中的元素类型相同。
  • someContainer中的项可以使用不等运算符(!=)来检查它们是否不同。

九、泛型where分句的扩展

可以使用泛型where分句作为扩展的一部分。下面的示例扩展了前面示例中的泛型Stack结构体,添加isTop(_:)方法:

extension Stack where Element: Equatable {
    func isTop(_ item: Element) -> Bool {
        guard let topItem = items.last else {
            return false
        }
        return topItem == item
    }
}

if stackOfStrings.isTop("tres") {
    print("Top element is tres.")
} else {
    print("Top element is something else.")
}
// Prints "Top element is tres."

上例通过使用泛型where分句向扩展添加新的约束条件,只有当添加到栈中的item符合Equatable协议才添加isTop(_:)方法。若向isTop(_:)方法中传入的参数不符合Equatable协议,则无法通过编译:

struct NotEquatable { }
var notEquatableStack = Stack<NotEquatable>()
let notEquatableValue = NotEquatable()
notEquatableStack.push(notEquatableValue)
notEquatableStack.isTop(notEquatableValue)  // Error

还可以向协议的扩展使用泛型where分句。下例使用扩展向Container协议添加了一个startsWith(_:)方法:

extension Container where Item: Equatable {
    func startsWith(_ item: Item) -> Bool {
        return count >= 1 && self[0] == item
    }
}

if [9, 9, 9].startsWith(42) {
    print("Starts with 42.")
} else {
    print("Starts with something else.")
}
// Prints "Starts with something else."

上面示例中的泛型where分句要求Item符合一个协议,但是也可以编写一个泛型where分句,要求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)
    }
}
print([1260.0, 1200.0, 98.6, 37.0].average())
// Prints "648.9"

十、泛型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
}

对于从另一个协议继承的协议,通过在协议声明中包含泛型where分句,可以向继承的关联类型添加约束。例如,下面的代码声明了一个 ComparableContainer协议,它要求项目符合Comparable:


protocol ComparableContainer: Container where Item: Comparable { }

十一、泛型下标

下标可以是泛型,也可以包括泛型的where分句。在subscript后的尖括号中写上占位符类型名,然后在下标主体的右花括号前写上泛型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
    }
}

十二、其他专题模块

Swift 4.2 基础专题详解

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

推荐阅读更多精彩内容

  • 136.泛型 泛型代码让你可以写出灵活,可重用的函数和类型,它们可以使用任何类型,受你定义的需求的约束。你可以写出...
    无沣阅读 1,432评论 0 4
  • 本章将会介绍 泛型所解决的问题泛型函数类型参数命名类型参数泛型类型扩展一个泛型类型类型约束关联类型泛型 Where...
    寒桥阅读 612评论 0 2
  • 泛型代码可以确保你写出灵活的,可重用的函数和定义出任何你所确定好的需求的类型。你的可以写出避免重复的代码,并且用一...
    iOS_Developer阅读 783评论 0 0
  • 泛型(Generics) 泛型代码允许你定义适用于任何类型的,符合你设置的要求的,灵活且可重用的 函数和类型。泛型...
    果啤阅读 657评论 0 0
  • "姐姐,到头来,我自己把煎熬送给了自己。"小梦望着只是望着马路的站台,身后的云卷云舒,花开花落似乎与她再无干系。 ...
    SnowC阅读 202评论 0 0