Swift基础知识相关(一) —— 泛型(一)

版本记录

版本号 时间
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)

addIntsaddDoubles的函数签名是不同的,但函数体是相同的。 你不仅有两个函数,而且它们里面的代码是重复的。 泛型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来检查numbersAgainfirstNumberAgain的类型;类型将与以前的值完全相同。 在这里,您可以使用显式通用语法指定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"]

检查两个声明的类型。 您将看到countryCodesString键和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到任何具体类型。

It’s T time!

4. Results

ResultSwift 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)
}

同样,类型参数Elementstruct中的任何位置都可用,包括内部方法。 使类型通用就像使其每个方法在同一类型上隐式通用。 您已经实现了类型安全的通用数据结构,就像标准库中的那样。

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)
}

仔细看看函数声明,参数列表和返回类型。

该函数对于您已命名为KeyValue的两种类型是通用的。 唯一的参数是具有KeyValue类型的键值对的字典。 返回值是表单元组的数组 - 你猜对了 - (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”,实际上,它们那样做!

在运行时,每个可能的KeyValue将作为一个单独的函数,填充函数声明和正文中的具体类型。对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协议,该协议表明任何符合的类型都必须具有加法运算符+。 然后,指定IntDouble类型符合它。

现在使用泛型参数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子类:GiftShoeBoxGift是一种特殊的Box,它是分开的,因此你可以在其上定义不同的方法和属性,例如wrap()。 但是,它在类型上仍然具有通用性,这意味着它可以包含任何内容。 ShoeGlassSlipper是一种非常特殊的鞋子,已经被声明,可以放在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泛型相关,感兴趣的给个赞或者关注~~~

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,968评论 6 482
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,601评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 153,220评论 0 344
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,416评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,425评论 5 374
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,144评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,432评论 3 401
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,088评论 0 261
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,586评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,028评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,137评论 1 334
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,783评论 4 324
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,343评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,333评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,559评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,595评论 2 355
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,901评论 2 345

推荐阅读更多精彩内容