关于泛型

「泛型」(Generic Code)也许是Swift相对于OC的最大特性之一吧!基于Swift的「泛型」特性,你能够写出扩展性更强、复用性更强的方法、类型,它可以让你尽可能避免重复代码,用一种清晰和抽象的方式来表达代码的意图。

许多的Swift标准库是基于「泛型」够构建的。譬如,Swift中的集合类型都是泛型集,你可以创建一个Int数组,也可以创建一个String数组,甚至任意其他任何类型的数组,这其实就是「泛型」。

泛型所解决的问题

泛型解决什么样的问题呢?如下是一个常见的、非泛型的函数swapTwoInts,用来交换两个Int值:

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

这个函数使用inout参数来交换a和b的值:

var a: Int =
var b: Int =
println("a = \(a), b = \(b)")
// prints "a = 4, b = 2"
swapTwoInts(&a, &b)
println("a = \(a), b = \(b)")
// prints "a = 2, b = 4"

这个函数非常有用,但可惜的是它只能用来交换两个Int值,如果你想交换两个String或Double,就不得不写更多的函数,如swapTwoStrings和swapTwoDoubles函数,如下:

// 交换两个String实例的值
func swapTwoStrings(inout a: String, inout b: String) {
let temp = a
a = b
b = temp
} // 交换两个Double实例的值
func swapTwoDoubles(inout a: Double, inout b: Double) {
let temp = a
a = b
b = temp
}

很容易注意到swapTwoInts、swapTwoStrings和swapTwoDoubles函数功能都是相同的,唯一不同之处在于传入的变量类型不同,分别是Int、String和Double。但实际应用中通常需要一个用处更大并且尽可能的考虑到更多的灵活性函数,可以用来交换两个任意类型值,「泛型」正好可以用来解决这种问题。

泛型函数

泛型函数可以工作于任何类型,如下是上述swapTwoInts函数的一个泛型版本,也用于交换两个值:

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

swapTwoValues函数body和swapTwoInts函数body完全一样,只是函数声明行不太一样:

func swapTwoInts(inout a: Int, inout b: Int)
func swapTwoValues<T>(inout a: T, inout b: T)

可以看到,「泛型」版本swapTwoValues使用「类型名占位符」TT不是必须的,可以使用其他字符代替)代替了真正的类型。在Swift中和其他语言一样,使用尖括号<>来包含函数中会使用到的「类型名占位符」,这样编译器在编译时就不会去审查T的合法性了。「类型名占位符」没有提示T必须是什么类型,但是它提示 了形参a和b必须是同一种类型,而不管T表示什么类型。只有swapTwoValues函数在每次被调用时所传入实际类型才能确定T所代表的类型。

「泛型」函数swapTwoValues的使用示例如下:

var intA =
var intB =
println("a = \(intA), b = \(intB)")
// prints "a = 4, b = 2"
swapTwoValues(&intA, &intB)
println("a = \(intA), b = \(intB)")
// prints "a = 2, b = 4" var stringA = ""
var stringB = ""
println("a = \(intA), b = \(intB)")
// prints "a = 4, b = 2"
swapTwoValues(&intA, &intB)
println("a = \(intA), b = \(intB)")
// prints "a = 2, b = 4"

泛型类型

除了「泛型函数」,Swift还允许你定义自己的「泛型类型」。这些「泛型类型」可以适应任意类型,就像Array/Set/Dictionary那样。

这一部分会展示如何写一个泛型集类型 — Stack(栈)。一个栈是一系列值域的集合,和Array类似,但是比Swift的Array有更多的限制。一个数组允许向其中任何位置执行插入/删除操作,而Stack,只允许在集合的末端添加新的元素(即所谓的push操作),也只能从末端移除元素(即所谓的pop操作)。

栈的概念已被UINavigationController类使用来模拟控制器的导航结构。你可以通过调用UINavigationController的pushViewController:animation:方法来为导航控制器添加新的视图控制器;而通过popViewControllerAnimation:方法来从导航栈中pop某个视图控制器。每当你需要一个严格的后进先出方式来管理集合时,栈都是最实用的模型。

下图展示了栈的push和pop行为:

如图,有5个过程:

  1. 有三个元素在栈中;
  2. 第四个元素正在被push到栈的顶部;
  3. 有四个元素在栈中;
  4. 栈中的最顶部元素正在被移除;
  5. 有三个元素在栈中;

先来写一个非泛型版本的栈,这个栈只盛装Int型元素:

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

上面所展示的IntStack类型只能用于存储Int元素,不过,参考它,来定义一个泛型版本的:

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

其实也蛮简单的,不多说了!

扩展一个泛型类型

和其他类型一样,我们也可以对一个泛型类型进行扩展,语法也差不多,如下对上文的Stack扩展了一个方法topItem。注意,在「泛型类型」的扩展中,可能也会使用到「类型名占位符」,如下:

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

值得注意的是,扩展中不再需要定义参数表。很显然感觉到,我们可能是没办法对一个我们看不到源码的「泛型类型」进行扩展的,因为我们不知道人家的「类型名占位符」。

类型约束

上文中的泛型函数swapTwoValues函数和泛型类型Stack都可以作用于任何类型,不过有时候,我们需要对泛型函数和泛型类型中的泛类型做些限制,譬如定义某个函数,传入的参数必须是可迭代类型,或者参数必须遵循某个协议。

Swift的泛型类型Dictionary对作用域key的类型做了些限制,要求key类型必须是可哈希的。因此Dictionary要求key类型必须遵循Hashable协议(Swift标准库中定义的一个协议)。
P.S:Swift的基本类型,如Int、String、Bool等都是可哈希的。

当你创建自定义「泛型类型」时,你可以定义你自己的「类型约束」。「类型约束」加强了泛型编程的威力。

类型约束语法

Swift规定在「类型名占位符」后面添加「类型约束」,通过冒号分隔,如下:

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

类型约束实战

如下定义了一个名为findStringIndex的非泛型函数,该函数的功能是去查找包含一个给定String值的数组。若找到匹配的字符串,该函数返回该字符串在数组中的index,反之返回nil:

func findStringIndex(array: [String], valueToFind: String) -> Int? {
for (index, value) in enumerate(array) {
if value == valueToFind {
return index
}
}
return nil
} let strings = ["cat", "dog", "llama", "parakeet", "terrapin"]
if let foundIndex = findStringIndex(strings, "llama") {
println("The index of llama is \(foundIndex)")
}
// prints "The index of llama is 2"

如果只是针对字符串而言查找在数组中的某个值的索引,用处不是很大,不过,你可以写出相同功能的泛型函数findIndex,用某个类型T值替换掉提到的字符串。

OK,参考这个findStringIndex函数写一个泛型函数findIndex,该函数的作用类似,只不过适用于更多类型(而不仅仅是[String]String)。

某人可能这样编码:

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

但是很快就会发现Xcode报错:Binary operator ‘==’ cannot be applied to two T operands.

简单来说,编译器无法知道「类型名占位符」T所表示的类型是否支持==操作符。

找到问题就好办了,添加一个「类型约束」就可以解决这问题了。Swift标准库中有一个名为Equatable的协议,遵循该协议的类型都得支持==操作。所以上述泛型函数findIndex定义如下:

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

关联类型

在《Swift协议》中总结了Swift协议的相关知识点。

之前阐述的「协议」里的所有类型都是确切的;但强大的Swift定义的「协议」不限于此,换句话说,你可以将泛型思想融入到「协议」中。

简单来说,Swift允许你在protocol中使用类似于「泛型函数」「泛型类型」的泛类型,这被Swift文档称为「关联类型」(Associated Types)。

关联类型实战

如下定义一个名为Container的协议,理论上Swift中的内置collection应该遵循该协议,定义如下:

protocol Container {
typealias ItemType
mutating func append(item: ItemType)
var count: Int { get }
subscript(i: Int) -> ItemType { get }
}

似乎有些奇怪,我还以为Container应该这么定义:

protocol Container<T> {
mutating func append(item: T)
var count: Int { get }
subscript(i: Int) -> T { get }
}

暂时先不理会哪种定义方式更好,总之,Container协议定义了三个任何容器必须支持的兼容要求:

  • 必须定义append方法用来添加新元素;
  • 必须定义count属性用来获取元素数量,返回类型是Int;
  • 必须定义下标,支持通过Int索引值检索到某个元素;

这个Container协议没有指定容器中的元素是如何存储的,也没有指定容器可以存储的元素类型,但是限制了「append方法的形参类型必须和subscript返回值类型一致。这种限制构成所谓的「关联类型」。

OK,我们让上文的非泛型类型IntStack遵循Container协议,如下:

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

更好的方式是写成下面这样:

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

IntStack类型实现了Container协议的所有三个要求,在IntStack类型的每个包含部分的功能都满足这些要求。此外,IntStack指定了Container的实现,定义typealias ItemType = Int,将抽象的ItemType类型转换为具体的Int类型。

对于泛型类型Stack,可以更简洁一点:

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

扩展已存在类型为关联类型

Swift的Array已经提供「append方法」,「count属性」以及「通过下标来查找一个自己的元素」。这三个功能都达到Container协议的要求,也就意味着你可以扩展Array去遵循Container协议,只要通过简单声明Array遵循该协议而已。声明一个已有类型遵循某个协议非常简单:

extension Array: Container {}

Where语句

「类型约束」使得Swift的泛型更加强大,但是还不够灵活。想象一个应用场景。某个函数接受两个参数,这两个参数都要求遵循Container协议,除此之外,还都要求这两个参数(集合类型)的元素类型相同,这该怎么弄?单纯的「类型约束」是办不到的,好在Swift为我们带来了where语句。

where语句的目的很直接,增强了「泛型」的威力。根据我的理解,where应该属于那种「约束少」「灵活大」的语言特性,关于它的使用想必非常繁杂。

下面举个栗子引出where的应用场景。定义一个名为allItemsMatch的泛型函数,顾名思义,该函数用来检查两个Container是否包含相同顺序的相同元素,如果所有元素顺序相同且值相同,则返回true,否则返回false,如下:

func allItemsMatch<
C1: Container, C2: Container
where C1.ItemType == C2.ItemType, C1.ItemType: Equatable>
(someContainer: C1, anotherContainer: C2) -> Bool { // 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 are equivalent
for i in ..<someContainer.count {
if someContainer[i] != anotherContainer[i] {
return false
}
} // all items match, so return true
return true
}

泛型函数allItemsMatch头部信息告诉我们:

  • 该函数定义了两个参数,这两个参数的类型都遵循Container协议;
  • 该函数的两个参数(容器)中元素的类型一致;
  • 该函数的两个参数中的元素遵循Equatable协议;

OK,我们来演示了allItemsMatch函数运算的使用:

var stackOfStrings = Stack<String>()
stackOfStrings.push("张不坏")
stackOfStrings.push("张无忌")
stackOfStrings.push("张全蛋") var arrayOfStrings = ["张不坏", "张无忌", "张全蛋"] if allItemsMatch(stackOfStrings, arrayOfStrings) {
println("All items match.")
} else {
println("Not all items match.")
}
// prints "All items match."

上面的例子创建一个Stack单例来存储String,然后push三个字符串进栈。然后创建了一个Array实例,并初始化三个同样顺序同样值的字符串。这样,即便栈和数组属于不同的类型,但他们都遵循Container协议,而且它们都包含同样的类型值,完全满足allItemsMatch对参数的要求。

Swift范性的更多相关文章

  1. iOS代码规范(OC和Swift)

    下面说下iOS的代码规范问题,如果大家觉得还不错,可以直接用到项目中,有不同意见 可以在下面讨论下. 相信很多人工作中最烦的就是代码不规范,命名不规范,曾经见过一个VC里有3个按钮被命名为button ...

  2. Swift与C#的基础语法比较

    背景: 这两天不小心看了一下Swift的基础语法,感觉既然看了,还是写一下笔记,留个痕迹~ 总体而言,感觉Swift是一种前后端多种语言混合的产物~~~ 做为一名.NET阵营人士,少少多多总喜欢通过对 ...

  3. iOS开发系列--Swift语言

    概述 Swift是苹果2014年推出的全新的编程语言,它继承了C语言.ObjC的特性,且克服了C语言的兼容性问题.Swift发展过程中不仅保留了ObjC很多语法特性,它也借鉴了多种现代化语言的特点,在 ...

  4. 算法与数据结构(十七) 基数排序(Swift 3.0版)

    前面几篇博客我们已经陆陆续续的为大家介绍了7种排序方式,今天博客的主题依然与排序算法相关.今天这篇博客就来聊聊基数排序,基数排序算法是不稳定的排序算法,在排序数字较小的情况下,基数排序算法的效率还是比 ...

  5. 算法与数据结构(十五) 归并排序(Swift 3.0版)

    上篇博客我们主要聊了堆排序的相关内容,本篇博客,我们就来聊一下归并排序的相关内容.归并排序主要用了分治法的思想,在归并排序中,将我们需要排序的数组进行拆分,将其拆分的足够小.当拆分的数组中只有一个元素 ...

  6. Swift enum(枚举)使用范例

    //: Playground - noun: a place where people can play import UIKit var str = "Hello, playground& ...

  7. swift开发新项目总结

    新项目用swift3.0开发,现在基本一个月,来总结一下遇到的问题及解决方案   1,在确定新项目用swift后,第一个考虑的问题是用纯swift呢?还是用swift跟OC混编      考虑到新项目 ...

  8. swift 中关于open ,public ,fileprivate,private ,internal,修饰的说明

    关于 swift 中的open ,public ,fileprivate,private, internal的区别 以下按照修饰关键字的访问约束范围 从约束的限定范围大到小的排序进行说明 open,p ...

  9. 【swift】BlockOperation和GCD实用代码块

    //BlockOperation // // ViewController.swift import UIKit class ViewController: UIViewController { @I ...

随机推荐

  1. Source Insight 4.0 破解和使用

    参考出处: https://blog.csdn.net/u011604775/article/details/81698062 https://blog.csdn.net/user11223344ab ...

  2. 时钟展频技术能有效降低EMI,深入讲解展频发生器!

    原文地址:https://baijiahao.baidu.com/s?id=1608649367453023659&wfr=spider&for=pc 相关文章: 1.http://b ...

  3. MP4文件格式的解析,以及MP4文件的分割算法

    http://www.cnblogs.com/haibindev/archive/2011/10/17/2214518.html http://blog.csdn.net/pirateleo/arti ...

  4. mybatis的两种分页方式:RowBounds和PageHelper

    原理:拦截器. 使用方法: RowBounds:在mapper.java中的方法中传入RowBounds对象. RowBounds rowBounds = new RowBounds(offset, ...

  5. 世界更清晰,搜狐新闻客户端集成HUAWEI HiAI 亮相荣耀Play发布会!

    ​​6月6日,搭载有“很吓人”技术的荣耀Play正式发布,来自各个领域的大咖纷纷为新机搭载的惊艳技术站台打call,其中,搜狐公司董事局主席兼首席执行官张朝阳揭秘:华为和搜狐新闻客户端在硬件AI方面做 ...

  6. Net中的代码规范工具及使用

    Net中的代码规范工具及使用 https://www.cnblogs.com/selimsong/p/9209254.html 上一篇文章介绍了编码标准中一些常用的工具,本篇就具体来介绍如何使用它们来 ...

  7. Java过滤特殊字符

    Java正则表达式过滤 1.Java过滤特殊字符的正则表达式----转载 java过滤特殊字符的正则表达式[转载] 2010-08-05 11:06 Java过滤特殊字符的正则表达式   关键字: j ...

  8. jvmtop 监控

    1 jar包 <!-- -JVMTOP监控- --> <dependency> <groupId>joda-time</groupId> <art ...

  9. TCP交换数据流——Nagle算法简单记录

    Nagle算法: 该算法提出的目的是想解决网络中大量的小的TCP数据包造成网络拥塞的问题,举个例子,当客户端要发送一个字节的TCP数据包到服务器时,我们实际上产生了41字节长的分组:包括20字节的IP ...

  10. 用CMakeLists.txt组织工程

    1 一个工程会有多个CMakeLists.txt,如何组织这些CMakeLists.txt来构建一个工程? 1.1  最外层一个CMakeLists.txt,是总的CMakeList.txt,在这个里 ...