Day14 – 作用域和泛型
Swift —— 100 天从新手到大师
它的目标是那些想要学习如何构建真正的 iOS 应用程序的开发者
如果说的是你 —— 准备好了吗
今天,我们解决 Swift 中的作用域和泛型。
作用域
在 Swift 程序中,万物皆有使用范围。这是指他们被其他对象看到的能力。对象可以嵌套在其他对象的内部,构成对象的嵌套层次结构。遵循一个规则:当我们在一个范围内使用对象时,我们仅可访问当前作用域以及嵌套当前作用域的更高层级的作用域。
你甚至不明白具体作用域具体的概念是什么,但是在实践中,你依然正确的使用出来了。
简单来说,Swift 中的作用域从高到低分成三层。
-
模块作用域,比如只要我们
import Foundation
框架时,我们可以在任意地方定义一个 String、Array 等Foundation 框架声明的类型 - 文件作用域 我们将代码写在不同的文件中,一般在文件头部定义一些全文皆可以访问的属性
-
{}
作用域写在文件中的代码根据其嵌套关系,存在不同级别的{}
作用域。
我们常见的是
{}
作用域。
常见的
{}
作用域可以分成以下几个概念,全局作用域、局部作用域、函数作用域和类作用域。
我们首先确定一点,我们这里所说的全局作用域指的是最外层的
{}
,而不是应用级别的全局。
局部作用域是指你所键入代码的位置,即键入位
于 {}
之间的代码块。作用域的窍门就是,跟踪你当前的局部作用域是什么,以及你可以访问哪些变量、类型等。
在 Swift 中,函数位于作用域的最深层,这就是为什么局部作用域通常与函数作用域相同。闭包比函数更深一层,但是闭包是可以
关闭
的,这使得它们有点特殊。
命名空间(Namespaces)
命名空间是程序的命名区域。名称空间内的事物的名称不能被名称空间外的事物访问,除非首先以某种方式通过表示该区域名称的障碍。这是一件好事,因为它允许在不同的地方使用相同的名称,而不会产生冲突。
了解命名空间有助于你理解作用域相关。
Objective-C 一个一直以来令人诟病的地方就是没有命名空间,在应用开发时,所有的代码和引用的静态库最终都会被编译到同一个域和二进制中。这样的后果是一旦我们有重复的类名的话,就 会导致编译时的冲突和失败。为了避免这种事情的发生,我们习惯性的在创建类之前添加其相关前缀。
在 Swift 中,由于可以使用命名空间了,即使是名字相同的类型,只要是来自不同的命名空间的 话,都是可以和平共处的。和 C# 这样的显式在文件中指定命名空间的做法不同,Swift 的命名空 间是基于 module 而不是在代码中显式地指明,每个 module 代表了 Swift 中的一个命名空间。也就是说,同一个 target 里的类型名称还是不能相同的。
你可以通过创建 Cocoa (Touch) Framework 的 target 的方法来新建一个 module,这样我们就可以在两个不同的 target 中添加同样名字的类型 了。
另一种策略是使用类型嵌套的方法来指定访问的范围。
class Product {
var kind:Kind = .thing
enum Kind {
case food
case thing
}
func canEat() -> Bool {
return kind == .food
}
}
class Seller {
class Product {
var kind:Kind = .thing
enum Kind {
case food
case thing
}
}
func canSell(_ product: Product) -> Bool {
return product.kind == .food
}
}
闭包
我们在之前的 Swift 闭包详解 中说明了可关闭闭包和值捕获的概念。
我们常见的闭包操作是作为 completionHandler 之类的尾随闭包。
例如我们在 viewDidLoad
中实现一个图片下载功能,当下载结束以后,更新 imageView
的图片
class DetailViewController: UIViewController {
@IBOutlet weak var imageView:UIImageView?
func viewDidLoad() {
network.downloadImage(url, completionHandler: { image in
imageView?.image = image
})
}
}
闭包与其定义的实体具有相同的作用域,在这种情况下,即
功能范围
。
局部作用域
局部作用域可以理解为一个 {}
内的作用域。在 Swift 中,直接使用大括号的写法是不支持的,因为这和闭包的定义产生了冲突。
在 Objective-C 中还有一个很棒的技巧是使用 GNU C 的声明扩展来在限制局部作用域的时候同时 进行赋值,运用得当的话,可以使代码更加紧凑和整洁。
self.titleLabel = ({
UILabel *label = [[UILabel alloc] initWithFrame:CGRectMake(150, 30, 20, 40)];
label.textColor = [UIColor redColor];
label.text = @"Title";
[view addSubview:label];
label;
});
Swift
中
当然没有这样子的 GNU C 的扩展。
不过在
Swift 闭包详解
一章中,我们详细介绍了关于Swift 中闭包的使用
我们可以实现一个匿名闭包来完成这个操作
let titleLabel: UILabel = {
let label = UILabel(frame: CGRect(x: 150, y: 30, width: 200, height: 40))
label.textColor = .red
label.text = "Title"
return label
}()
总结
作用域是什么?作用域就是当你想在一个 {}
中调用一个方法或变量时,你可以问一下自己,我是否真的可以这么调用。
关于作用域的问题往往可以引申至内存管理。这点我们到时候会详细说明。
泛型
在我们过往的讨论中,我们多次提及了泛型的概念。泛型是 Swift 编程语言最强大的功能之一。
作为一种类型安全的语言,泛型是 Swift 的核心特性——包括它的标准库,也大量使用泛型。在学习之前,你应该对Swift的类型、类和协议有基本的了解。
Swift 中的复杂数据类型
一文中提及的 Array
、 Dictionary
和 Set
,都是使用了泛型。
为什么要使用泛型
自动类型生成是泛型解决的问题!
当我们编写可以应用于许多不同类型的代码时,泛型特别有用。
在
协议与拓展
一节中,我们利用协议来处理商场中不同类型产品的售卖操作。这其实就是一种泛型思想的落实 。
protocol Purchaseable {
// 商品名称
var name: String { get set }
// 折扣
var discount: Double { get }
}
struct Customer {
var shoppingList = Array()
// 购买
mutating func buy(_ product: Purchaseable) {
self.shoppingList.append(product)
}
}
如果我们不遵循
Purchaseable
协议,我们需要为用户添加
struct Customer {
var shoppingBookList = Array()
var shoppingClothesList = Array()
...
// 购买书籍
mutating func buyBook(_ book: Book) {
self.shoppingBookList.append(book)
}
// 购买服装
mutating func buyclothes(_ clothes: Clothes) {
self.shoppingBookList.append(clothes)、
}
...
}
在实际应用中,我们应该遵循一种被称作
Don’t Repeat Yourself (DRY)
的软件开发原则,用来减少代码和应用程序中的重复的代码块。
我们应该尽可能的减少重复的代码块。
让我们开始在我们的代码中使用泛型吧!
编写含有泛型的结构体
让我们定义一个包含任意类型和日期对象的结构体,Container
struct Container {
var value: Value
var date: Date
}
let stringContainer = Container(value: "iOS成长指北", date: Date())
let intContainer = Container(value: 2000, date: Date())
let dateContainer = Container(value: Date(), date: Date())
print("stringContainer.value = ", stringContainer.value)
print("intContainer.value = ", intContainer.value)
print("dateContainer.value = ", dateContainer.value)
//stringContainer.value = iOS成长指北
//intContainer.value = 2000
//dateContainer.value = 2021-01-07 09:49:34 +0000
Container
是一种泛型类型,其泛型自变量子句中具有类型自变量 Value
。另一种说法是, Container
是 Value
类型上的泛型。例如, Container
和 Queue
在运行时将成为它们自己的具体类型。
Value
被称为占位符类型。这告诉 Swift, Value
不是实际类型,而是 Container
中的占位符。
编写含有泛型的函数
在商品的例子中,我们为消费者创建了购物的方法,我们将这个方法改成支持泛型的
struct Customer {
var shoppingList = Array()
// 购买
mutating func buy(_ product: Element) {
self.shoppingList.append(product)
}
}
此时,我们的类型为任意类型,但是我们需要让我们的类型遵循 Purchaseable
协议,我们可以
使用泛型类型约束
如果能对泛型函数或泛型类型中添加特定的
类型约束
,这将在某些情况下非常有用。类型约束指定类型参数必须继承自指定类、遵循特定的协议或协议组合。
struct Customer {
var shoppingList = Array()
// 购买
mutating func buy(_ product: Element) {
self.shoppingList.append(product)
print("product.name", product.name)
}
}
当自定义泛型类型时,你可以定义你自己的类型约束,这些约束将提供更为强大的泛型编程能力。
如果我们在方法中使用类型约束的话,我们可以这么做
struct Customer {
// 购买
mutating func buy(_ product: Element) {
print("product.name", product.name)
}
}
除了自定义的协议,Swift 提供了以下一些基本协议:
-
Equatable
对于那些可以相等或不相等的值
-
Comparable
或可以比较的值,例如a> b
-
Hashable
用于
可哈希
的值,该值是该值的唯一整数表示形式(通常用于字典键)
-
CustomStringConvertible
用于可以表示为字符串的值,这是一种有助于快速将自定义对象转换为可打印字符串的有用协议 -
Numeric
和SignedNumeric
指数字值,例如42
和3.1415
-
Strideable
可以偏移和测量的值
关联类型
Swift 中的关联类型与通常与协议密切相关。
你可以从字面上把它们看作是协议的一种关联类型:从你把它们放在一起的那一刻起,它们就是一体的。
关联的类型可以看作是协议定义中特定类型的替代。换句话说:这是在采用协议并指定确切类型之前要使用的类型的占位符名称。
关联类型通过
associatedtype
关键字来指定。
下面例子定义了一个
Identifiable
协议,该协议定义了一个关联类型
ID
:
protocol Identifiable {
associatedtype ID: Equatable & CustomStringConvertible
var id: ID { get }
}
然后在我们遵循 Identifiable
协议的对象中给他加上不同类型的 id
struct Book: Identifiable {
let id: String
}
struct Clothes: Identifiable {
let id: Int
}
当我们加入到购物车时,我们可以根据
id
是否相同来判断我们加入购物车的商品的数量。并且各个类型的
id
都可以有他们自己的类型,甚至声明出来。
通过简化针对多个场景的通用接口,它们可以防止编写重复的代码。这样,可以将同一逻辑用于多种不同类型,从而只允许编写和测试一次逻辑。
这与泛型的优势很像是不是?
结合泛型、协议和关联类型的使用
定义一个集合协议 Collection
protocol Collection {
associatedtype Item: Equatable
var count: Int { get }
subscript(index: Int) -> Item { get }
mutating func append(_ item: Item)
}
然后为集合协议添加一个新的 CollectionSlice
协议用于获取集合的前 n 个,并保证类型相等。
protocol CollectionSlice: Collection {
associatedtype Slice: CollectionSlice where Slice.Item == Item
func prefix(_ maxLength: Int) -> Slice
}
然后我们定义一个遵循 Collection
协议的结构体 UppercaseStringsCollection
struct UppercaseStringsCollection: Collection {
var container: [String] = []
var count: Int { container.count }
mutating func append(_ item: String) {
guard !container.contains(item) else { return }
container.append(item.uppercased())
}
subscript(index: Int) -> String {
return container[index]
}
}
然后我们为 UppercaseStringsCollection
新增一个遵循 CollectionSlice
协议的拓展
extension UppercaseStringsCollection: CollectionSlice {
func prefix(_ maxLength: Int) -> UppercaseStringsCollection {
var collection = UppercaseStringsCollection()
for index in 0..<min(maxLength, count) {
collection.append(self[index])
}
return collection
}
}
结合泛型、协议和关联类型的使用,在 Swift 中很常见。
总结
总的来说,泛型是一种强大功能,它使我们能够编写更易于重用的代码,同时还支持本地专门化。
算法、数据结构和实用程序通常是泛型的最佳候选者,因为它们通常只需要它们所使用的类型来满足特定的一组需求,而不是绑定到特定的具体类型。
不透明类型
Swift 5.1之后,我们可以使用另一种方法来处理泛型:不透明类型。
some
关键字允许
隐藏
属性或函数的具体返回类型。返回的具体类型可以由实现本身决定,而不是由调用代码决定。这就是为什么不透明类型有时被称为
反向泛型
。
不透明类型和泛型是相关的。
- 对于不透明类型,函数实现将确定具体类型。
使用泛型的占位符,常见的
T
,其类型是由函数的调用者确定占位符
T
的具体类型。
我们依旧按照我们之前商品的例子来说明不透明类型的作用。
我们为商品协议定义一个买家
protocol Purchaseable {
associatedtype Buyer
var buyer: Buyer { get }
// 商品名称
var name: String { get set }
}
然后我们定义书籍和衣服两种产品
struct Book: Purchaseable {
var buyer: String = "iOS 成长指北"
var name: String = "我是一本书"
}
struct Clothes: Purchaseable {
var buyer: Int = 89757
var name: String = "我是一件衣服"
}
定义一个快递员 Courier
,快递员将商品送给买家,但是他不需要知道买家买的具体是什么,他可以是符合商品协议的任何一个。
//快递员
struct Courier {
func delivery() -> some Purchaseable {
return Book()
}
}
此时就是不透明类型的使用场景了。
泛型类型占位符允许函数的调用者具体化泛型函数中使用的类型。协议类型允许我们从函数中返回任何类型,只要它符合协议。
但是这不适用于关联的类型,因为缺少具体的类型信息。因此,我们使用
some
关键字创建不透明类型,反向泛型,并让函数的实现确定返回值的具体类型以及任何关联类型的具体类型。
很绕口是不是?
总结一下几点
- 首先,不透明类型可以将带有关联类型的协议用作返回类型。
-
其次,与协议类型不同是,不透明类型会
保留
类型身份。
- 第三,最后但同样重要的是,不透明类型对于SwiftUI至关重要——我们会在学习 Swift UI 时进行讲解。
消化一下今天的内容。我们下次再说。
本文的提纲来自于 hackingwithswift 的 《100 Days of Swift》,然后根据每一个知识点进行总结
欢迎点赞、转发、评论、在看。
如果有任何问题,欢迎留言交流