热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

Swift泛型教程入门

原文:SwiftGenericsTutorial:GettingStarted作者:GemmaBarlow译者:kmyhy更新说明:本教程由Gemm

原文:Swift Generics Tutorial: Getting Started
作者:Gemma Barlow
译者:kmyhy

更新说明:本教程由 Gemma Barlow 更新为 Swift3。原文作者是 Mikael Konutgan。

泛型编程是一种编写函数和数据类型的方法,同时使对数据类型的预设最小化。Swift 泛型编写的代码不会指定数据的实际类型,从而允许进行更优雅的抽象,使得代码更清晰、Bug 更少——编写“适用于王和女王”的代码。我喜欢将自己的代码描述成皇室成员,你呢?

在 Swift 自身也大量使用了泛型,理解它们就等于彻底掌握了这门语言。在 Swift 中的一个泛型的例子是 Optional 类型。你可以让任意数据类型变成 Optional 的,哪怕是你自定义的类型。Optional 数据类型就是它所能包含的类型的泛型。

在本教程中,我们将在 plaground 中学习下列知识:

  • 泛型是什么
  • 泛型有什么用
  • 如何用泛型编写泛型函数和数据结构
  • 如何使用类型限制
  • 如何扩展泛型

注意: 本教程使用 Xcode8 和 Swift 3。

开始

创建一个新 plaground。在 Xcode 中,打开 File\New\Playground… 菜单,取名为 Generics,platform 选择 macOS。点击 Next 保存位置,点 Create。

作为居住在很远很远的国度中的一个程序员,你被召唤进皇宫,为了帮助女王解决一个大难题。她无法算出她有多少臣民,想让你帮她计算。她需要一个函数,用于加两个整数。在这个新的 playground 中添加一个函数:

func addInts(x: Int, y: Int) -> Int {
return x + y
}

addInts(x:y:) 方法有两个 Int 参数,返回二者之和。你可以这样调用它:

let intSum = addInts(x: 1, y: 2)

这是一个简单的例子,可以说明 Swift 是类型安全的。你可以用两个整数来调用这个函数,但不能使用其他的数据类型。

女王大悦,立马让你写出另外一个函数——这次,要加两个 Double 数。你又写了一个 addDoubles(x:y:) 函数:

func addDoubles(x: Double, y: Double) -> Double {
return x + y
}
let doubleSum = addDoubles(x: 1.0, y: 2.0)

addInts 和 addDoubles 的签名不同,但函数实现上没有任何区别。你有两个函数,但函数中的代码是重复的。泛型可以将两个函数合并成一个,避免代码重复。

但首先,我们来看一下在 Swift 编程中使用泛型的其它常见场景。

Swift 泛型的其它例子

你可能不知道,有一些常用的结构,比如数组、字典和 Optional 也是泛型的吧!

数组

在 playground 中写上这两句:

let numbers = [1, 2, 3]

let firstNumber = numbers[0]

这里,我们创建了一个简单数组,包含了 3 个数,然后访问它的第一个数。

现在分别在 numbers 和 firstNumber 上用 option+左键点击。看到了什么?

https://koenig-media.raywenderlich.com/uploads/2017/02/numbers.png’ https://www.#.com/go/Jzxh" href="https://koenig-media.raywenderlich.com/uploads/2017/02/firstNumber.png" referrerpolicy="no-referrer">https://koenig-media.raywenderlich.com/uploads/2017/02/firstNumber.png’ prettyprint">var numbersAgain: Array = []
numbersAgain.append(1)
numbersAgain.append(2)
numbersAgain.append(3)

let firstNumberAgain = numbersAgain[0]

通过 option+左键,查看 numberAgain 和 firstNumberAgain 的类型;它们的类型将和之前看到的一样。这次,我们用泛型语法显式指定了 numberAgain 的类型,在 Array 后面使用了一堆尖括号。

尝试添加其它东西到数组中,比如 String:

numbersAgain.append("All hail Lord Farquaad")

我们会看到某些错误出现, Cannot convert value of type ‘String’ to expected argument type ‘Int’。编译器告诉你,不能将一个 String 添加到一个整数数组中。append 方法是泛型 Array 中的一个泛型方法。它知道数组元素的数据类型,不允许你添加错误的类型给它。

删除这行代码。看标准库中的另外一个泛型例子。

字典

字典也是泛型,用于构造类型安全的数据结构。

在 playground 最后一行创建一个字典,并查找弗里多尼亚的代码:

let countryCodes = ["Arendelle": "AR", "Genovia": "GN", "Freedonia": "FD"]
let countryCode = countryCodes["Freedonia"]

查看两个字典的类型。你会看到 countryCodes 是一个键为 String 值也为 String 的字典,除此之外,不允许有其它值。这说明 Dictionary 是泛型。

Optional

在上面的代码中,注意到没有?countryCode 的类型是 Stirng?,这是
Optional 的简写形式。

看到熟悉的 <和 > 了吧?泛型无处不在!

这里,编译器会强制你只能通过 String 类型的键来访问字典,你得到的返回值也总是 String。这里的 countryCode 用 Optional 类型表示,因为不是任何 key 都会有相应的值返回。如果你使用 “The Emerald City” 访问字典,countryCode 就可能是 nil 了。因为在字典中根本没有这个 key。

注意:关于 Optional,你可以参考本站的Beginning Swift 3 – Optionals 视频教程。

在 playground 中编写如下代码,以查看显式创建一个可空字符串的语法:

let optiOnalName= Optional<String>.some("Princess Moana")
if let name = optionalName {}

查看 name 的类型,你会看到它是一个 String。

可空绑定,即代码中的 if-let,是一种泛型转换。它会将泛型类型 T? 转换成泛型 T。也就是说,你可以在任意实际类型上使用 if let。

现在你已经掌握了基本的泛型概念,接下来可以学习如何编写自己的泛型数据结构和函数了。

编写泛型数据结构

队列 queue 是一种数据结构,类似于列表或堆栈,但你只能从末尾添加新值(入队操作),从前端取值(出队操作)。这就和我们曾经使用过的用于进行网络请求的 OperationQueue 类似。

女王对你之前的工作非常满意,现在想让你实现一个功能,让她的臣民排队等候召见。

在 playground 中添加下列结构:

struct Queue<Element> {
}

很显然,Queue 是一个泛型类型,我们可以从它实用了泛型参数 Element 就可以看出。另外,Queue 通过 Element 来进行泛型化。例如,Queue 和 Queue 会在运行时决定 Element 所属的真正类型,这样我们就只能对整数或字符串进行入队出队操作。

为 Queue 声明一个属性:

fileprivate var elements: [Element] = []

我们用这个数组保存队列中的元素,属性初始化为一个空数组。注意,你可以把 Element 当成是真正的类型使用,当然它还需要在晚些时候才能知道真正的类型。我们用 fileprivate 进行声明,因为我们不想让调用者直接访问底层存储。我们将强制调用者通过方法来访问底层存储。同时,我们使用 fileprivate 而不是 private,是因为我们想在后面对 Queue 进行扩展。

最后,实现两个方法:

mutating func enqueue(newElement: Element) {
elements.append(newElement)
}

mutating func dequeue() -> Element? {
guard !elements.isEmpty else { return nil }
return elements.remove(at: 0)
}

在结构体(包括方法内部)中,都可以以 Element 的方式访问类型参数。将某个类型泛型化相当于让它的所有方法显式地泛型化相同的类型。我们已经实现了一个类型安全的数据结构,就像标准库中所做的一样。

接下来测试一下我们的新结构,将等待接见的臣民入队,也就是将他们的编号添加到队列中:

var q = Queue()

q.enqueue(newElement: 4)
q.enqueue(newElement: 2)

q.dequeue()
q.dequeue()
q.dequeue()
q.dequeue()

我们故意尝试制造一些泛型错误——比如,将 String 放到队列中。现在你能够看到更多的错误信息,在越大的项目中,这种错误的识别就越容易。

编写泛型函数

女王的事情太多了,她又让你写一个将由键值对组成的字典转换成列表的程序。

在 playground 中编写如下函数:

func pairsValue>(from dictionary: [Key: Value]) -> [(Key, Value)] {
return Array(dictionary)
}

注意函数的声明,参数列表及返回类型。

这个函数是对两个类型进行泛型化,即 Key 和 Value。函数有一个参数,是一个字典,字典的键/值类型分别为 Key 和 Value。返回值是一个元组构成的数组,你猜对了元组的类型就是 (key,Value)。

我们在任何有效的字典上使用这个 pairs(from:) 函数,幸好我们有泛型:

let somePairs = pairs(from: ["minimum": 199, "maximum": 299]) 
// result is [("maximum", 299), ("minimum", 199)]

let morePairs = pairs(from: [1: "Swift", 2: "Generics", 3: "Rule"])
// result is [(2, "Generics"), (3, "Rule"), (1, "Swift")]

当然,因为我们不能控制字典中键值对的顺序,你会发现元组是顺序会变成“Generics”, “Rule”, “Swift” !:]

在运行时,函数声明和函数体中 Key 和 Value 会分别用真正的类型替代。第一句 pairs(from:) 函数调用返回一个 (String,Int) 数组。第二句调用则将两个参数类型进行调换,返回(Int,String) 数组。

我们创建了一个能够根据调用方式的不同而返回不同返回类型的函数。这非常棒。我们可以将逻辑放在同一个地方,并简化我们的代码。不需要两个不同的函数,用一个函数就能够处理两种调用方式。

现在,我们学习了如何创建泛型函数和泛型类型,接下来学习更高级的用法。我们已经明白泛型用于限制类型是非常有用的,但我们还可以添加另外的限制,同时扩展我们的泛型类型,让它更加好用。

泛型的约束

女王希望你编写一个程序,对她的一小群子民的年龄进行分析,先排序,然后找出中间值。在 playground 中编写函数:

func mid(array: [T]) -> T? {
guard !array.isEmpty else { return nil }
return array.sorted()[(array.count - 1) / 2]
}

这里会出现一个错误。原因是,要使用 sorted 函数,数组的元素必须是 Comparable 的。我们需要告诉 Swift,mid 函数可以使用数组作为参数,但数组的元素类型必须实现 Comparable 协议。

将函数定义修改为:

func mid(array: [T]) -> T? {
guard !array.isEmpty else { return nil }
return array.sorted()[(array.count - 1) / 2]
}

这里,我们通过 : 语法,为泛型参数 T 添加了一个类型约束。我们只允许元素类型为 Comparable 的数组上调用 mid 函数,因此才能调用 sorted() 方法!测试一下被我们约束后的泛型函数:

mid(array: [3, 5, 1, 2, 4]) // 3

学完类型约束,我们可以在 playground 开始处创建一个 add 泛型函数——既显得优雅,又能赢得女王的欢心。在 playground 中新增一个扩展:

protocol Summable { static func +(lhs: Self, rhs: Self) -> Self }
extension Int: Summable {}
extension Double: Summable {}

首先我们创建了一个 Summable 协议,声明任何实现了加号运算符的类型都是 Summable 的。然后,指定 Int 和 Double 类型都实现了 Summable。

然后用一个泛型参数 T 和一个类型约束实现这个 add 泛型函数:

func add<T: Summable>(x: T, y: T) -> T {
return x + y
}
``

我们将两个函数(甚至更多,我们可以扩展更多的 Summable 类型)缩减为 1 个函数,从而减少了冗余代码。我们可以在整数和浮点数上使用 add 函数了:

```swift
let addIntSum = add(x: 1, y: 2) // 3
let addDoubleSum = add(x: 1.0, y: 2.0) // 3




class="se-preview-section-delimiter">div>

甚至在字符串上使用它:

extension String: Summable {}
let addString = add(x: "Generics", y: " are Awesome!!! :]")




class="se-preview-section-delimiter">div>

我们可以让其他类型也实现 Summable 协议,这样 add 函数的用途就更广泛了,感谢它的泛型定义!由于您的贡献,女王殿下授予了你王国最高荣誉。

泛型的扩展

有一个宫廷小丑会替女王殿下监视等候接见的臣民们,并在女王正式召见之前知道下一个是谁。它通过挨个查看女王接待室的窗户来偷窥这一切。我们可以用一个扩展来模拟这种行为,使用我们的泛型队列。

扩展 Queue 类型,添加下列方法:

extension Queue {
func peek() -> Element? {
return elements.first
}
}




<div class="se-preview-section-delimiter">div>

peek 方法返回队列中的未出队元素的第一个。要扩展一个泛型类型非常简单!泛型参数只能够在原定义中可见。你可以用这个扩展去偷窥一个队列:

q.enqueue(newElement: 5)
q.enqueue(newElement: 3)
q.peek() // 5




<div class="se-preview-section-delimiter">div>

你可以看到队列中的第一个元素是 5,但我们不需要进行出队操作,队列中的元素个数没有发生改变!

王室挑战:为 Queue 扩展一个函数 isHomogeneous,用它来判断是否所有元素相等。你可以在 Queue 定义中使用类型约束,以确保队列中的元素是否能够进行等于比较。

参考答案

首先让 Queue 中的元素实现 Equatable 协议:

struct Queue<Element: Equatable> {




<div class="se-preview-section-delimiter">div>

然后实现 isHomogeneous() 方法:

extension Queue {
func isHomogeneous() -> Bool {
guard let first = elements.first else { return true }
return !elements.contains { $0 != first }
}
}




<div class="se-preview-section-delimiter">div>

最后,进行测试:

var h = Queue()
h.enqueue(newElement: 4)
h.enqueue(newElement: 4)
h.isHomogeneous() // true
h.enqueue(newElement: 2)
h.isHomogeneous() // false




"se-preview-section-delimiter">

泛型的继承

Swift 允许对泛型类进行继承,在某些情况下这很有用,比如创建某个泛型类的实体子类。

在 playground 中添加下列泛型类。

class Box<T> {
// 一个简单的盒子
}




<div class="se-preview-section-delimiter">div>

这里我们定义了一个 Box 类。Box 可以存放任意对象,因此我们将它定义为泛型类。你可以用两种方法来继承 Box 类:

  1. 我们可以扩展它的功能,保持它是泛型化的,这样仍然可以将任何对象放到盒子里;
  2. 我们也可以继承出一个具体化的子类,我们能够指明它里面放的是什么东西。

Swift 两种方法都允许。在 playground 中编写:

class Gift<T>: Box<T> {
// 默认,礼盒是用白纸进行包装
func wrap() {
print("Wrap with plain white paper.")
}
}

class Rose {

// 童话剧中常用的鲜花
}

class ValentinesBox: Gift<Rose> {
// 送给情人的玫瑰
}

class Shoe {
// 普通的鞋
}

class GlassSlipper: Shoe {
// 公主的水晶鞋
}

class ShoeBox: Box<Shoe> {
// 鞋盒
}




class="se-preview-section-delimiter">div>

我们定义了两个 Box 子类:Gift 和 ShoeBox。Gift 是一种特殊的 Box,有着不同的方法和属性,比如 wrap()。但是,它仍然有一个泛型类型,这样它可以用于存放任何东西。Shoe 和特殊 Shoe GlassSlipper,可以放到 ShoeBox 中以便送出(或者送给某个追求者)。

定义几个上述子类的实例:

let box = Box() // 一个普通的放玫瑰的盒子
let gift = Gift() // 一个放玫瑰的盒子
let shoeBox = ShoeBox()




class="se-preview-section-delimiter">div>

注意,ShoeBox 的初始化没有使用泛型参数,因为在 ShoeBox 的声明中已经指定了类型。

然后,定义一个新的 ValentinesBox 实例 —— 一个盛放玫瑰的盒子,来自于情人节的特殊礼物。

let valentines = ValentinesBox()

普通的盒子用白纸包装,但情人节的礼盒总要漂亮点吧。为 ValentinesBox 添加如下方法:

override func wrap() {
print("Wrap with ♥♥♥ paper.")
}




<div class="se-preview-section-delimiter">div>

最终,比较一下这两种盒子的包装:

gift.wrap() // plain white paper
valentines.wrap() // ♥♥♥ paper




<div class="se-preview-section-delimiter">div>

ValentinesBox,虽然是通过泛型构造的,但仍然可以像正常的子类一样,可以继承、覆盖父类的方法。太贴心了。

枚举和相关值

正常的错误处理要用到一个所谓的“结果枚举”。结果枚举是一种泛型枚举,有两个相关值:一个是真正的结果值,一个是可能的错误。

这种方法允许我们编写“优雅的”错误处理,这是女王要求我们写的另一个除法方法——这是她的最后一个要求了。

在 playground 中添加下列定义:

enum Result<Value> {
case success(Value), failure(Error)
}




<div class="se-preview-section-delimiter">div
>

这个枚举主要用于作为函数返回值,并用标准库中的 Error 类型返回特殊的错误信息,就和通常返回 Optional 的方式一样。在 playground 最后加入:

enum MathError: Error {
case divisionByZero
}

func divide(_ x: Int, by y: Int) -> Result {
guard y != 0 else {
return .failure(MathError.divisionByZero)
}
return .success(x / y)
}




<div class="se-preview-section-delimiter">div>

这里,我们定义了一个错误枚举类型和一个对两个整数进行除法的函数。如果除法成功,我们会返回一个包含有计算结果的枚举,即 .success,否则返回一个数学错误。

然后,用以下代码测试该函数:

let result1 = divide(42, by: 2) // .success(21)
let result2 = divide(42, by: 0) // .failure(MathError.divisionByZero)

第一句返回包含了 21 的枚举值 .success,第二句返回了包含 .divisionByZero 的枚举值 .failure。尽管这个枚举的关联值有两个泛型参数,但通过 case 语句我们指定了只会用其中一个。

结束

可以在这里下载最终的 playground 项目。

Swift 泛型是许多基础语言特性的基础,比如数组和可空。我们学习了如何用泛型编写优雅的、可重用同时 Bug 更少的代码——编写让女王满意的代码。

更多内容,可以参考苹果官方Swift 编程指南的泛型一章和 Generic Parameters and Arguments。其中你可以找到许多关于泛型的详细内容和一些有用的例子。

如果你还意犹未尽,可以阅读这里关于“Swift 4 中关于泛型的可能改变——计划中的未来特性”。

在基于本教程之后的下一个主题,是面向协议编程——请参考 Niv Yahel 写的这本书面向协议编程引述。

Swift 泛型是一个我们每天都会用到的内置特性,通过它我们可以编写强大和类型安全的代码。每当我们在改写那些常用的代码时,我们都要问一下自己:可以将它泛型化吗?

有任何问题,请在下面留言!


推荐阅读
  • 开发笔记:加密&json&StringIO模块&BytesIO模块
    篇首语:本文由编程笔记#小编为大家整理,主要介绍了加密&json&StringIO模块&BytesIO模块相关的知识,希望对你有一定的参考价值。一、加密加密 ... [详细]
  • 重入锁(ReentrantLock)学习及实现原理
    本文介绍了重入锁(ReentrantLock)的学习及实现原理。在学习synchronized的基础上,重入锁提供了更多的灵活性和功能。文章详细介绍了重入锁的特性、使用方法和实现原理,并提供了类图和测试代码供读者参考。重入锁支持重入和公平与非公平两种实现方式,通过对比和分析,读者可以更好地理解和应用重入锁。 ... [详细]
  • 本文讨论了微软的STL容器类是否线程安全。根据MSDN的回答,STL容器类包括vector、deque、list、queue、stack、priority_queue、valarray、map、hash_map、multimap、hash_multimap、set、hash_set、multiset、hash_multiset、basic_string和bitset。对于单个对象来说,多个线程同时读取是安全的。但如果一个线程正在写入一个对象,那么所有的读写操作都需要进行同步。 ... [详细]
  • 如何在php文件中添加图片?
    本文详细解答了如何在php文件中添加图片的问题,包括插入图片的代码、使用PHPword在载入模板中插入图片的方法,以及使用gd库生成不同类型的图像文件的示例。同时还介绍了如何生成一个正方形文件的步骤。希望对大家有所帮助。 ... [详细]
  • 本文介绍了Python高级网络编程及TCP/IP协议簇的OSI七层模型。首先简单介绍了七层模型的各层及其封装解封装过程。然后讨论了程序开发中涉及到的网络通信内容,主要包括TCP协议、UDP协议和IPV4协议。最后还介绍了socket编程、聊天socket实现、远程执行命令、上传文件、socketserver及其源码分析等相关内容。 ... [详细]
  • 本文分享了一个关于在C#中使用异步代码的问题,作者在控制台中运行时代码正常工作,但在Windows窗体中却无法正常工作。作者尝试搜索局域网上的主机,但在窗体中计数器没有减少。文章提供了相关的代码和解决思路。 ... [详细]
  • macOS Big Sur全新设计大版本更新,10+个值得关注的新功能
    本文介绍了Apple发布的新一代操作系统macOS Big Sur,该系统采用全新的界面设计,包括图标、应用界面、程序坞和菜单栏等方面的变化。新系统还增加了通知中心、桌面小组件、强化的Safari浏览器以及隐私保护等多项功能。文章指出,macOS Big Sur的设计与iPadOS越来越接近,结合了去年iPadOS对鼠标的完善等功能。 ... [详细]
  • AFNetwork框架(零)使用NSURLSession进行网络请求
    本文介绍了AFNetwork框架中使用NSURLSession进行网络请求的方法,包括NSURLSession的配置、请求的创建和执行等步骤。同时还介绍了NSURLSessionDelegate和NSURLSessionConfiguration的相关内容。通过本文可以了解到AFNetwork框架中使用NSURLSession进行网络请求的基本流程和注意事项。 ... [详细]
  • 本文分享了一位Android开发者多年来对于Android开发所需掌握的技能的笔记,包括架构师基础、高级UI开源框架、Android Framework开发、性能优化、音视频精编源码解析、Flutter学习进阶、微信小程序开发以及百大框架源码解读等方面的知识。文章强调了技术栈和布局的重要性,鼓励开发者做好学习规划和技术布局,以提升自己的竞争力和市场价值。 ... [详细]
  • 原文链接:Python:获取“3年前的今天”的日期时间Python:getdatetimefor3yearsagotoday在Python中,如何获取3年前的今天的datetime ... [详细]
  • scrcpy通过adb调试的方式来将手机屏幕投到电脑上,并可以通过电脑控制您的Android设备。它可以通过USB连接,也可以通过Wifi连接(类似于隔空投屏),而且不需要任何ro ... [详细]
  • 本文摘自JavaGuide。1、简单易学;2、面向对象(封装,继承,多态);3、平台无关性(Java虚拟机实现平台无关性);4、可靠性;5、安全性;6、支持多线程(C++语言没有内 ... [详细]
  • quartus管脚分配后需要保存吗_嵌入式必须会的一些硬件面试题,要试一试吗?你过来呀!...
    1、下面是一些基本的数字电路知识问题,请简要回答之。(1)什么是Setup和Hold时间?答:SetupHoldTime用于测试芯片对输入 ... [详细]
  • Java大文件HTTP断点续传到服务器该怎么做?
    最近由于笔者所在的研发集团产品需要,需要支持高性能的大文件http上传,并且要求支持http断点续传。这里在简要归纳一下,方便记忆 ... [详细]
  • 当我在doWork方法中运行代码时,通过单击button1,进度条按预期工作.但是,当我从其他方法(即btn2,btn3)将列表传递给doWork方法时,进度条在启动后会跳转到10 ... [详细]
author-avatar
Tags | 热门标签
RankList | 热门文章
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有