版本记录
版本号 | 时间 |
---|---|
V1.0 | 2019.07.12 星期五 |
前言
这个专题我们就一起看一下Swfit相关的基础知识。
开始
首先看下主要内容
主要内容:学习编写函数和数据类型,同时做出最小的假设。 Swift泛型允许更少的bug和更加干净清晰的代码。
接着看下写作环境
Swift 5, iOS 13, Xcode 11
Generic programming是一种编写函数和数据类型的方法,同时对所使用的数据类型做出最小的假设。 Swift泛型创建的代码没有特定于底层数据类型,允许优雅的抽象,产生更少的错误更清晰的代码。 它允许您编写一次函数并在不同类型上使用它。
你会发现Swift中使用的泛型,这使得理解它们对完全掌握语言至关重要。 您将在Swift中遇到的通用示例是Optional
类型。 您可以选择任何您想要的数据类型,当然也包括您自己创建的类型。 换句话说,Optional
数据类型在其可能包含的值类型上是通用的。
在本教程中,您将在Swift playground
中进行实验以了解:
What exactly generics are
Why they are useful
How to write generic functions and data structures
How to use type constraints
How to extend generic types
首先要创建一个新的playground
。 在Xcode中,转到File ▸ New ▸ Playground…
选择macOS ▸ Blank
模板。 单击Next
并命名Playground为Generics
。 最后,单击Create
!
作为居住在遥远的王国的为数不多的程序员之一,你被召唤到皇家城堡帮助女王,这是一件非常重要的事情。 她已经忘记了自己拥有多少皇家科目,并且在计算方面需要一些帮助。
她要求编写一个相加两个整数的函数。 将以下内容添加到新创建的playground
:
func addInts(x: Int, y: Int) -> Int {
return x + y
}
addInts(x:y :)
获取两个Int
值并返回它们的总和。 您可以通过将以下代码添加到playground
上来尝试:
let intSum = addInts(x: 1, y: 2)
这是一个简单的例子,展示了Swift的类型安全性。 您可以使用两个整数调用此函数,但不能使用任何其他类型。
女王很高兴,并立即要求另外一个add
函数来计算她的财富 - 这个时候,添加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
的函数签名是不同的,但函数体是相同的。 你不仅有两个函数,而且它们里面的代码是重复的。 泛型Generics
可用于将这两个函数减少为一个并删除冗余代码。
但是,首先,您将了解日常Swift中常见编程的一些常见情况。
Other Examples of Swift Generics
您可能没有意识到,但您使用的一些最常见的结构,例如数组,字典,选项和结果都是泛型类型!
1. Arrays
将以下内容添加到您的playground
:
let numbers = [1, 2, 3]
let firstNumber = numbers[0]
在这里,您创建一个包含三个数字的简单数组,然后从该数组中取出第一个数字。
现在按住Option
键单击,首先是numbers
,然后是firstNumber
。 你看到了什么?
因为Swift
具有类型推断 type inference,所以您不必显式定义常量的类型,但它们都具有确切的类型。 numbers
是一个[Int]
- 也就是一个整数数组 - 而firstNumber
是一个Int
。
Swift Array
类型是泛型类型。 通用类型都至少有一个类型参数,一个尚未指定的其他类型的占位符。 您需要指定其他类型才能专门化泛型类型并实际创建它的实例。
例如,Array
的type参数确定数组中的内容。 您的数组是专用的,因此它只能包含Int
值。 这支持Swift的类型安全性。 当你从那个数组中删除任何东西时,Swift - 更重要的是你 - 知道它必须是一个Int
。
通过向playground
添加稍长版本的相同代码,您可以更好地了解Array
的一般特性:
var numbersAgain: Array<Int> = []
numbersAgain.append(1)
numbersAgain.append(2)
numbersAgain.append(3)
let firstNumberAgain = numbersAgain[0]
通过Option-click
来检查numbersAgain
和firstNumberAgain
的类型;类型将与以前的值完全相同。 在这里,您可以使用显式通用语法指定numbersAgain
的类型,方法是将Int
放在Array
之后的尖括号中。 您已将Int
作为type
参数的显式类型参数提供。
尝试在数组中添加其他内容,如String
:
numbersAgain.append("All hail Lord Farquaad")
您将收到错误 - 类似于:Cannot convert value of type ‘String’ to expected argument type ‘Int’
。 编译器告诉您不能将字符串添加到整数数组。 作为泛型类型Array
的一种方法,append
是一种所谓的泛型方法。 因为此数组实例是专用类型Array <Int>
,所以它的append
方法现在也专门用于append(_ newElement:Int)
。 它不会让你添加一些不正确的类型。
删除导致错误的行。 接下来,您将看到标准库中另一个泛型示例。
2. Dictionaries
字典也是泛型类型,并导致类型安全的数据结构。
在playground
末尾创建以下魔法王国词典,然后查找Freedonia的国家代码:
let countryCodes = ["Arendelle": "AR", "Genovia": "GN", "Freedonia": "FD"]
let countryCode = countryCodes["Freedonia"]
检查两个声明的类型。 您将看到countryCodes
是String
键和String
值的字典 - 此字典中没有任何其他内容。 形式泛型类型是Dictionary
。
3. Optionals
在上面的示例中,请注意countryCode
的类型是String?
。 这实际上只是Optional
的简写。
如果<and>
看起来很熟悉,那是因为甚至Optional
是泛型类型。 泛型到处都是!
这里编译器强制您只能使用字符串键访问字典,并且始终返回返回的字符串值。 可选类型用于表示countryCode
,因为可能没有与该键对应的值。 例如,如果您尝试查找The Emerald City
,则countryCode
的值将为nil
,因为它不存在于您的魔法王国词典中。
将以下内容添加到您的playground
,以查看创建可选字符串的完整显式语法:
let optionalName = Optional<String>.some("Princess Moana")
if let name = optionalName {}
检查name
的类型,您将看到它是String
。
Optional binding 可选绑定,即if-let
构造,是对各种类型的泛型转换。 它需要T?
类型的通用值,并为您提供类型为T
的通用值。这意味着您可以使用if let
到任何具体类型。
4. Results
Result
是Swift 5
中的一个新类型。与Optional
一样,它是一个包含两个case
的通用枚举。 结果要么取而代之,要么success
,要么failure
。 每个case
都有自己的关联泛型类型,success
有一个值,failure
有一个Error
。
考虑这种情况,皇家魔法师招募你施放一些法术。 已知法术会生成一个符号,但未知法术会失败。 该函数看起来像这样:
enum MagicError: Error {
case spellFailure
}
func cast(_ spell: String) -> Result<String, MagicError> {
switch spell {
case "flowers":
return .success("💐")
case "stars":
return .success("✨")
default:
return .failure(.spellFailure)
}
}
Result
允许您编写返回值或错误的函数,而无需使用try
语法。 作为额外的好处,failure case
的通用规范意味着您不需要像使用catch
块那样检查类型。 如果出现错误,您可以确定与.failure case
案例相关联的值中会出现MagicError
。
试试一些法术来看看Result
:
let result1 = cast("flowers") // .success("💐")
let result2 = cast("avada kedavra") // .failure(.spellFailure)
掌握了泛型的基础知识,您可以学习如何编写自己的通用数据结构和函数。
Writing a Generic Data Structure
队列queue
是一种类似于列表或堆栈的数据结构,但是您只能向其添加新值(将它们排入队列)并且只从前面获取值(将它们出列)。 如果您曾经使用过OperationQueue
,那么这个概念可能会很熟悉。
女王,对你在本教程前面的努力感到高兴,现在希望你能够编写函数,以帮助跟踪等待与她交谈的皇室成员。
将以下struct
声明添加到playground
的末尾:
struct Queue<Element> {
}
Queue
是泛型类型,在其泛型参数子句中具有类型参数Element
。 另一种说法是,Queue
在类型Element
上是通用的。 例如,Queue <Int>
和Queue <String>
将在运行时成为它们自己的具体类型,它们只能分别对字符串和整数进行入队和出列。
将以下属性添加到队列中:
private var elements: [Element] = []
您将使用此数组来保存元素,您将其初始化为空数组。 请注意,您可以使用Element
,就好像它是一个真实的类型,即使它稍后会被填充。 您将其标记为private
,因为您不希望Queue
的使用者访问elements
。 您希望强制它们使用方法来访问后备存储。
最后,实现两个主要的队列方法:
mutating func enqueue(newElement: Element) {
elements.append(newElement)
}
mutating func dequeue() -> Element? {
guard !elements.isEmpty else { return nil }
return elements.remove(at: 0)
}
同样,类型参数Element
在struct
中的任何位置都可用,包括内部方法。 使类型通用就像使其每个方法在同一类型上隐式通用。 您已经实现了类型安全的通用数据结构,就像标准库中的那样。
在playground
底部玩一下你的新数据结构,通过将他们的royal id
添加到队列中来排队等待主题:
var q = Queue<Int>()
q.enqueue(newElement: 4)
q.enqueue(newElement: 2)
q.dequeue()
q.dequeue()
q.dequeue()
q.dequeue()
通过故意制造尽可能多的错误来触发与泛型相关的不同错误消息,从而获得一些乐趣 - 例如,在队列中添加一个字符串。 您现在对这些错误了解得越多,就越容易在更复杂的项目中识别和处理它们。
Writing a Generic Function
女王有很多要处理的数据,她要求你编写的下一段代码将采用键和值的字典并将其转换为列表。
将以下函数添加到playground
的底部:
func pairs<Key, Value>(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 [(1, "Swift"), (2, "Generics"), (3, "Rule")]
当然,由于你无法控制字典项进入数组的顺序,你可能会在playground
上看到一个元组值的顺序更像“Generics”, “Rule”, “Swift”
,实际上,它们那样做!
在运行时,每个可能的Key
和Value
将作为一个单独的函数,填充函数声明和正文中的具体类型。对pair(from :)
的第一次调用返回一个(String,Int)
元组数组。第二个调用在元组中使用翻转的类型顺序,并返回(Int,String)
元组的数组。
您创建了一个可以使用不同的调用返回不同类型的函数。您可以看到如何将逻辑保存在一个位置可以简化代码。您需要使用一个函数处理两个调用,而不需要两个不同的函数。
现在您已经了解了创建和使用泛型类型和函数的基础知识,现在是时候继续使用一些更高级的功能了。您已经看到了泛型如何按类型限制事物,但您可以添加其他约束以及扩展泛型类型以使它们更有用。
Constraining a Generic Type
为了分析一小部分她最忠诚的主题的年龄,女王要求一个函数来排序数组并找到中间值。
将以下函数添加到playground
时:
func mid<T>(array: [T]) -> T? {
guard !array.isEmpty else { return nil }
return array.sorted()[(array.count - 1) / 2]
}
你会收到一个错误。 问题是要使sorted()
工作,数组的元素必须是Comparable
。 只要元素类型实现Comparable
,你需要以某种方式告诉Swift mid
可以接受任何数组。
将函数声明更改为以下内容:
func mid<T: Comparable>(array: [T]) -> T? {
guard !array.isEmpty else { return nil }
return array.sorted()[(array.count - 1) / 2]
}
在这里,您使用:
语法将类型约束添加到泛型类型参数T
。您现在只能使用Comparable
元素数组调用该函数,以便sorted()
始终有效! 通过添加以下内容来尝试约束函数:
mid(array: [3, 5, 1, 2, 4]) // 3
您在使用Result
时已经看到过这种情况:Failure
类型被限制为Error
。
1. Cleaning Up the Add Functions
现在您已了解类型约束,您可以从playground
的开头创建add
函数的通用版本 - 这将更加优雅,并且请女王们非常高兴。 将以下协议和扩展添加到您的playground
:
protocol Summable { static func +(lhs: Self, rhs: Self) -> Self }
extension Int: Summable {}
extension Double: Summable {}
首先,您创建一个Summable
协议,该协议表明任何符合的类型都必须具有加法运算符+
。 然后,指定Int
和Double
类型符合它。
现在使用泛型参数T
和类型约束,您可以创建一个通用函数add
:
func add<T: Summable>(x: T, y: T) -> T {
return x + y
}
您已将两个函数(实际上更多,因为您需要更多其他Summable
类型)减少到一个并删除冗余代码。 您可以在整数和双精度上使用新函数:
let addIntSum = add(x: 1, y: 2) // 3
let addDoubleSum = add(x: 1.0, y: 2.0) // 3.0
您还可以在其他类型上使用它,例如字符串
extension String: Summable {}
let addString = add(x: "Generics", y: " are Awesome!!! :]")
通过向Summable
添加其他符合要求的类型,您的add(x:y :)
函数由于其泛型驱动的定义而变得更加广泛有用!
Extending a Generic Type
一个Court Jester
一直在协助女王守护等待的皇家臣民,让女王知道下一个主题,然后才正式问候。 他透过她客厅的窗户偷看。 您可以使用扩展来对其行为进行建模,该扩展应用于本教程前面的通用队列类型。
扩展Queue
类型并在Queue
定义的正下方添加以下方法:
extension Queue {
func peek() -> Element? {
return elements.first
}
}
peek
返回第一个元素而不将其出列。 扩展泛型类型很容易! 泛型类型参数与原始定义的主体一样可见。 您可以使用扩展程序查看队列:
q.enqueue(newElement: 5)
q.enqueue(newElement: 3)
q.peek() // 5
您将看到值5
作为队列中的第一个元素,但没有任何内容出列,并且队列具有与以前相同数量的元素。
Royal Challenge:扩展Queue
类型以实现一个函数isHomogeneous
,它检查队列的所有元素是否相等。 您需要在Queue
声明中添加类型约束,以确保可以检查其元素是否相互相等。
Answer:
首先编辑
Queue
的定义,以便Element
符合Equatable
协议:struct Queue<Element: Equatable> {
然后
compose
是你playground
底部的Homogeneous()
:extension Queue { func isHomogeneous() -> Bool { guard let first = elements.first else { return true } return !elements.contains { $0 != first } } }
最后,测试结果:
var h = Queue<Int>() h.enqueue(newElement: 4) h.enqueue(newElement: 4) h.isHomogeneous() // true h.enqueue(newElement: 2) h.isHomogeneous() // false
Subclassing a Generic Type
Swift
具有子类泛型类的能力。 在某些情况下,这可能很有用,例如创建泛型类的具体子类。
将以下泛型类添加到playground
中:
class Box<T> {
// Just a plain old box.
}
在这里定义一个Box
类。 该box
可以包含任何内容,这就是为什么它是泛型类。 您可以通过两种方式将Box
子类化:
- 1) 你可能想要扩展
box
的功能以及它是如何工作的,但要保持通用性,所以你仍然可以在box
里放任何东西; - 2) 您可能希望拥有一个专门的子类,它始终知道其中的内容。
Swift
允许两者。 将其添加到您的playground
:
class Gift<T>: Box<T> {
// By default, a gift box is wrapped with plain white paper
func wrap() {
print("Wrap with plain white paper.")
}
}
class Rose {
// Flower of choice for fairytale dramas
}
class ValentinesBox: Gift<Rose> {
// A rose for your valentine
}
class Shoe {
// Just regular footwear
}
class GlassSlipper: Shoe {
// A single shoe, destined for a princess
}
class ShoeBox: Box<Shoe> {
// A box that can contain shoes
}
你在这里定义了两个Box
子类:Gift
和ShoeBox
。 Gift
是一种特殊的Box
,它是分开的,因此你可以在其上定义不同的方法和属性,例如wrap()
。 但是,它在类型上仍然具有通用性,这意味着它可以包含任何内容。 Shoe
和GlassSlipper
是一种非常特殊的鞋子,已经被声明,可以放在ShoeBox
的一个实例中进行交付。
在子类声明下声明每个类的实例:
let box = Box<Rose>() // A regular box that can contain a rose
let gift = Gift<Rose>() // A gift box that can contain a rose
let shoeBox = ShoeBox()
请注意,ShoeBox
初始化程序不再需要采用泛型类型参数,因为它已在ShoeBox
的声明中修复。
接下来,声明子类ValentinesBox
的一个新实例 - 一个包含玫瑰的盒子,一个专门用于情人节的神奇礼物。
let valentines = ValentinesBox()
虽然标准盒子用白纸包裹,但你希望你的节日礼物有点发烧友。 将以下方法添加到ValentinesBox
:
override func wrap() {
print("Wrap with ♥♥♥ paper.")
}
最后,通过将以下代码添加到您的playground
来比较包装这两种类型的结果:
gift.wrap() // plain white paper
valentines.wrap() // ♥♥♥ paper
ValentinesBox
虽然使用泛型构造,但它作为标准子类运行,其方法可以从超类继承和覆盖。 多么优雅!
Enumerations With Associated Values
女王对您的工作很满意,并希望为您提供奖励:您选择的通用宝藏或奖章。
将以下声明添加到playground
的末尾:
enum Reward<T> {
case treasureChest(T)
case medal
var message: String {
switch self {
case .treasureChest(let treasure):
return "You got a chest filled with \(treasure)."
case .medal:
return "Stand proud, you earned a medal!"
}
}
}
此语法允许您编写枚举,其中至少有一个case
是通用box
。 使用message
var
,您可以将值恢复。 在上面说明的Result示例中,成功和失败案例都是通用的,具有不同的类型。
要恢复关联值,请使用如下:
let message = Reward.treasureChest("💰").message
print(message)
Swift泛型是许多常见语言功能的核心,例如数组和可选项。 您已经了解了如何使用它们来构建优雅,可重用的代码,从而减少错误。
有关更多信息,请阅读Apple指南Swift编程语言的泛型Generics章节和通用参数和参数语言 Generic Parameters and Arguments参考章节。 您将在Swift中找到有关泛型的更多详细信息,以及一些方便的示例。
Swift
中的泛型是一个不可或缺的功能,您每天都会使用它来编写功能强大且类型安全的抽象。
后记
本篇主要讲述了Swift泛型相关,感兴趣的给个赞或者关注~~~