类型约束和关联类型 2个月前

类型约束

swapTwoValues(_:_:) 函数和 Stack 适用于任意类型。不过,如果能对泛型函数或泛型类型中添加特定的 类型约束 ,这将在某些情况下非常有用。类型约束指定类型参数必须继承自指定类、遵循特定的协议或协议组合。

例如,Swift 的 Dictionary 类型对字典的键的类型做了些限制。在 字典的描述 中,字典键的类型必须是可哈希(hashable)的。也就是说,必须有一种方法能够唯一地表示它。字典键之所以要是可哈希的,是为了便于检查字典中是否已经包含某个特定键的值。若没有这个要求,字典将无法判断是否可以插入或替换某个指定键的值,也不能查找到已经存储在字典中的指定键的值。

这个要求通过 Dictionary 键类型上的类型约束实现,它指明了键必须遵循 Swift 标准库中定义的 Hashable 协议。所有 Swift 的基本类型(例如 StringIntDoubleBool)默认都是可哈希的。如何让自定义类型遵循 Hashable 协议,可以查看文档 遵循 Hashable 协议

当自定义泛型类型时,你可以定义你自己的类型约束,这些约束将提供更为强大的泛型编程能力。像 可哈希(hashable) 这种抽象概念根据它们的概念特征来描述类型,而不是它们的具体类型。

类型约束语法

在一个类型参数名后面放置一个类名或者协议名,并用冒号进行分隔,来定义类型约束。下面将展示泛型函数约束的基本语法(与泛型类型的语法相同):

funcsomeFunction<T:SomeClass, U:SomeProtocol>(someT: T, someU: U) {
// 这里是泛型函数的函数体部分
}

上面这个函数有两个类型参数。第一个类型参数 T 必须是 SomeClass 子类;第二个类型参数 U 必须符合 SomeProtocol 协议。

类型约束实践

这里有个名为 findIndex(ofString:in:) 的非泛型函数,该函数的功能是在一个 String 数组中查找给定 String 值的索引。若查找到匹配的字符串,findIndex(ofString:in:) 函数返回该字符串在数组中的索引值,否则返回 nil

funcfindIndex(ofStringvalueToFind: String, inarray: [String]) ->Int? {
for(index, value)in array.enumerated() {
if value == valueToFind {
return index
        }
    }
returnnil
}

findIndex(ofString:in:) 函数可以用于查找字符串数组中的某个字符串值:

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

如果只能查找字符串在数组中的索引,用处不是很大。不过,你可以用占位类型 T 替换 String 类型来写出具有相同功能的泛型函数 findIndex(_:_:)

下面展示了 findIndex(ofString:in:) 函数的泛型版本 findIndex(of:in:)。请注意这个函数返回值的类型仍然是 Int?,这是因为函数返回的是一个可选的索引数,而不是从数组中得到的一个可选值。需要提醒的是,这个函数无法通过编译,原因将在后面说明:

funcfindIndex<T>(ofvalueToFind: T, inarray:[T]) ->Int? {
for(index, value)in array.enumerated() {
if value == valueToFind {
return index
        }
    }
returnnil
}

上面所写的函数无法通过编译。问题出在相等性检查上,即 "if value == valueToFind"。不是所有的 Swift 类型都可以用等式符(==)进行比较。例如,如果你自定义类或结构体来描述复杂的数据模型,对于这个类或结构体而言,Swift 无法明确知道“相等”意味着什么。正因如此,这部分代码无法保证适用于任意类型 T,当你试图编译这部分代码时就会出现相应的错误。

不过,所有的这些并不会让我们无从下手。Swift 标准库中定义了一个 Equatable 协议,该协议要求任何遵循该协议的类型必须实现等式符(==)及不等符(!=),从而能对该类型的任意两个值进行比较。所有的 Swift 标准类型自动支持 Equatable 协议。

遵循 Equatable 协议的类型都可以安全地用于 findIndex(of:in:) 函数,因为其保证支持等式操作符。为了说明这个事情,当定义一个函数时,你可以定义一个 Equatable 类型约束作为类型参数定义的一部分:

funcfindIndex<T:Equatable>(ofvalueToFind: T, inarray:[T]) ->Int? {
for(index, value)in array.enumerated() {
if value == valueToFind {
return index
        }
    }
returnnil
}

findIndex(of:in:) 类型参数写做 T: Equatable,也就意味着“任何符合 Equatable 协议的类型 T”。

findIndex(of:in:) 函数现在可以成功编译了,并且适用于任何符合 Equatable 的类型,如 DoubleString

let doubleIndex =findIndex(of:9.3, in: [3.14159, 0.1, 0.25])
// doubleIndex 类型为 Int?,其值为 nil,因为 9.3 不在数组中
let stringIndex =findIndex(of:"Andrea", in: ["Mike", "Malcolm", "Andrea"])
// stringIndex 类型为 Int?,其值为 2

关联类型

定义一个协议时,声明一个或多个关联类型作为协议定义的一部分将会非常有用。关联类型为协议中的某个类型提供了一个占位符名称,其代表的实际类型在协议被遵循时才会被指定。关联类型通过 associatedtype 关键字来指定。

关联类型实践

下面例子定义了一个 Container 协议,该协议定义了一个关联类型 Item

protocolContainer {
associatedtype Item
mutatingfuncappend(_item: Item)
var count: Int { get }
subscript(i: Int) -> Item { get }
}

Container 协议定义了三个任何遵循该协议的类型(即容器)必须提供的功能:

  • 必须可以通过 append(_:) 方法添加一个新元素到容器里。
  • 必须可以通过 count 属性获取容器中元素的数量,并返回一个 Int 值。
  • 必须可以通过索引值类型为 Int 的下标检索到容器中的每一个元素。

该协议没有指定容器中元素该如何存储以及元素类型。该协议只指定了任何遵从 Container 协议的类型必须提供的三个功能。遵从协议的类型在满足这三个条件的情况下,也可以提供其他额外的功能。

任何遵从 Container 协议的类型必须能够指定其存储的元素的类型。具体来说,它必须确保添加到容器内的元素以及下标返回的元素类型是正确的。

为了定义这些条件,Container 协议需要在不知道容器中元素的具体类型的情况下引用这种类型。Container 协议需要指定任何通过 append(_:) 方法添加到容器中的元素和容器内的元素是相同类型,并且通过容器下标返回的元素的类型也是这种类型。

为此,Container 协议声明了一个关联类型 Item,写作 associatedtype Item。协议没有定义 Item 是什么,这个信息留给遵从协议的类型来提供。尽管如此,Item 别名提供了一种方式来引用 Container 中元素的类型,并将之用于 append(_:) 方法和下标,从而保证任何 Container 的行为都能如预期。

这是前面非泛型版本 IntStack 类型,使其遵循 Container 协议:

structIntStack:Container {
// IntStack 的原始实现部分
var items: [Int] = []
mutatingfuncpush(_item: Int) {
        items.append(item)
    }
mutatingfuncpop() ->Int {
return items.removeLast()
    }
// Container 协议的实现部分
typealiasItem=Int
mutatingfuncappend(_item: Int) {
        self.push(item)
    }
var count: Int {
return items.count
    }
subscript(i: Int) ->Int {
return items[i]
    }
}

IntStack 结构体实现了 Container 协议的三个要求,其原有功能也不会和这些要求相冲突。

此外,IntStack 在实现 Container 的要求时,指定 ItemInt 类型,即 typealias Item = Int,从而将 Container 协议中抽象的 Item 类型转换为具体的 Int 类型。

由于 Swift 的类型推断,实际上在 IntStack 的定义中不需要声明 ItemInt。因为 IntStack 符合 Container 协议的所有要求,Swift 只需通过 append(_:) 方法的 item 参数类型和下标返回值的类型,就可以推断出 Item 的具体类型。事实上,如果你在上面的代码中删除了 typealias Item = Int 这一行,一切也可正常工作,因为 Swift 清楚地知道 Item 应该是哪种类型。

你也可以让泛型 Stack 结构体遵循 Container 协议:

structStack<Element>:Container {
// Stack<Element> 的原始实现部分
var items: [Element] = []
mutatingfuncpush(_item: Element) {
        items.append(item)
    }
mutatingfuncpop() ->Element {
return items.removeLast()
    }
// Container 协议的实现部分
mutatingfuncappend(_item: Element) {
        self.push(item)
    }
var count: Int {
return items.count
    }
subscript(i: Int) ->Element {
return items[i]
    }
}

这一次,占位类型参数 Element 被用作 append(_:) 方法的 item 参数和下标的返回类型。Swift 可以据此推断出 Element 的类型即是 Item 的类型。

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

在扩展添加协议一致性 中描述了如何利用扩展让一个已存在的类型遵循一个协议,这包括使用了关联类型协议。

Swift 的 Array 类型已经提供 append(_:) 方法,count 属性,以及带有 Int 索引的下标来检索其元素。这三个功能都符合 Container 协议的要求,也就意味着你只需声明 Array 遵循Container 协议,就可以扩展 Array,使其遵从 Container 协议。你可以通过一个空扩展来实现这点,正如通过扩展采纳协议中的描述:

extensionArray:Container {}

Arrayappend(_:) 方法和下标确保了 Swift 可以推断出 Item 具体类型。定义了这个扩展后,你可以将任意 Array 当作 Container 来使用。

给关联类型添加约束

你可以在协议里给关联类型添加约束来要求遵循的类型满足约束。例如,下面的代码定义了 Container 协议, 要求关联类型 Item 必须遵循 Equatable 协议:

protocolContainer {
associatedtype Item:Equatable
mutatingfuncappend(_item: Item)
var count: Int { get }
subscript(i: Int) -> Item { get }
}

要遵守 Container 协议,Item 类型也必须遵守 Equatable 协议。

在关联类型约束里使用协议

协议可以作为它自身的要求出现。例如,有一个协议细化了 Container 协议,添加了一个 suffix(_:) 方法。suffix(_:) 方法返回容器中从后往前给定数量的元素,并把它们存储在一个 Suffix 类型的实例里。

protocolSuffixableContainer:Container {
associatedtype Suffix:SuffixableContainer where Suffix.Item == Item
funcsuffix(_size: Int) -> Suffix
}

在这个协议里,Suffix 是一个关联类型,就像上边例子中 ContainerItem 类型一样。Suffix 拥有两个约束:它必须遵循 SuffixableContainer 协议(就是当前定义的协议),以及它的 Item 类型必须是和容器里的 Item 类型相同。Item 的约束是一个 where 分句,它在下面 具有泛型 Where 子句的扩展 中有讨论。

这是上面 泛型类型Stack 类型的扩展,它遵循了 SuffixableContainer 协议:

extensionStack:SuffixableContainer {
funcsuffix(_size: Int) -> Stack {
var result =Stack()
for index in(count-size)..<count {
            result.append(self[index])
        }
return result
    }
// 推断 suffix 结果是Stack。
}
var stackOfInts = Stack<Int>()
stackOfInts.append(10)
stackOfInts.append(20)
stackOfInts.append(30)
let suffix = stackOfInts.suffix(2)
// suffix 包含 20 和 30

在上面的例子中,SuffixStack 的关联类型,也是 Stack ,所以 Stack 的后缀运算返回另一个 Stack 。另外,遵循 SuffixableContainer 的类型可以拥有一个与它自己不同的 Suffix 类型——也就是说后缀运算可以返回不同的类型。比如说,这里有一个非泛型 IntStack 类型的扩展,它遵循了 SuffixableContainer 协议,使用 Stack<Int> 作为它的后缀类型而不是 IntStack

extensionIntStack:SuffixableContainer {
funcsuffix(_size: Int) -> Stack<Int> {
var result = Stack<Int>()
for index in(count-size)..<count {
            result.append(self[index])
        }
return result
    }
// 推断 suffix 结果是 Stack<Int>。
}
image
EchoEcho官方
无论前方如何,请不要后悔与我相遇。
1377
发布数
439
关注者
2223773
累计阅读

热门教程文档

Golang
23小节
爬虫
6小节
Javascript
24小节
Gin
17小节
Vue
25小节