Swift UI 深入理解泛型
Swift UI 深入理解泛型

Swift UI 深入理解泛型

本篇文章的主要核心是学习并理解Swift UI 的泛型知识内容,也是我近期编写的第二篇类似知识理解的文章,第一篇是学习try

泛型是Swift中用于编写通用代码的强大特性,通过使用泛型可以编写支持多种类型的函数、方法或解构。因此,不需要为每个具体类型编写重复代码,可以让你的代码更具灵活性和复用性。

泛型的定义

在Swift中,我们可以使用泛型类型参数来定义泛型。泛型类型参数通常使用大些字母(如T、U)来表示,它们是占位符,表示一个未知的类型。直到调用这个函数或使用这个类型时,才会指定具体的泛型。

泛型函数

泛型函数的形式是,在函数名后面加上<T>,表示泛型函数。T是这个泛型的类型参数,比如:

// 这是一个泛型函数,可以比较任意类型的两个值
func compare<T: Comparable>(_ a: T, _ b: T) -> Bool {
    return a == b
}
  • <T: Comparable>: 表示这个函数是泛型的,T是一个占位符,表示函数可以接受任何类型,只要这个类型符合Comparable协议。
  • Compare(_:_:):函数可以比较任何可比较的类型,并比较它们是否相等。

可以使用这个函数比较不同类型的数据,比如:

let isEqualInt = compare(5, 5) // true
let isEqualString = compare("Swift", "Swift") // true

在调用时,编译器会自动推断T的具体类型,例如第一次调用时,T被推断为Int,第二次调用时,T被推断为String。

泛型结构

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

Button("泛型") {
    var itemsNum = Stack(items: [1,2,3,4])
    itemsNum.push(5)
    _ = itemsNum.pop()
    _ = itemsNum.pop()
    print("itemsNum:\(itemsNum)")   // itemsNum:Stack<Int>(items: [1, 2, 3])
}

我们的泛型不仅限于函数,还可以用于类、结构和协议。

泛型样例

下面我们将通过一个扩展样例来解释泛型:

// astronauts.json
{
    "grissom": {
        "id": "grissom",
        "name": "Virgil I. \"Gus\" Grissom",
        "description": "Virgil Ivan \"Gus\" Grissom (April 3, 1926 – January 27, 1967) was one of the seven original National Aeronautics and Space Administration's Project Mercury astronauts, and the first of the Mercury Seven to die. He was also a Project Gemini and an Apollo program astronaut. Grissom was the second American to fly in space, and the first member of the NASA Astronaut Corps to fly in space twice.\n\nIn addition, Grissom was a World War II and Korean War veteran, U.S. Air Force test pilot, and a mechanical engineer. He was a recipient of the Distinguished Flying Cross, and the Air Medal with an oak leaf cluster, a two-time recipient of the NASA Distinguished Service Medal, and, posthumously, the Congressional Space Medal of Honor."
    },
    "white": {
        "id": "white",
        "name": "Edward H. White II",
        "description": "Edward Higgins White II (November 14, 1930 – January 27, 1967) (Lt Col, USAF) was an American aeronautical engineer, U.S. Air Force officer, test pilot, and NASA astronaut. On June 3, 1965, he became the first American to walk in space. White died along with astronauts Virgil \"Gus\" Grissom and Roger B. Chaffee during prelaunch testing for the first crewed Apollo mission at Cape Canaveral.\n\nHe was awarded the NASA Distinguished Service Medal for his flight in Gemini 4 and was then awarded the Congressional Space Medal of Honor posthumously."
    }
}
// Astronauts.swift
extension Bundle 
    func decode<T: Decodable>(_ file: String) -> T {
        guard let url = self.url(forResource: file, withExtension: nil) else {
            fatalError("Failed to locate \(file) in bundle.")
        }
        guard let data = try? Data(contentsOf: url) else {
            fatalError("Failed to load \(file) from bundle.")
        }
        let decoder = JSONDecoder()
        guard let loaded = try? decoder.decode(T.self, from: data) else {
            fatalError("Failed to decode \(file) from bundle.")
        }
        return loaded
    }
}

struct Astronaut:Codable, Identifiable {
    let id: String
    let name: String
    let description: String
}

struct Astronauts: View {
    var body: some View {
        Button("对比两个参数") {
            let astronauts: [String: Astronaut] = Bundle.main.decode("astronauts.json")
            print(astronauts)
        }
    }
}

上面为两个文件,一个是我们所读取的文件astronauts.json文件,另一个就是我们的代码部分,在代码中,我们使用了Bundle的扩展,Bundle扩展的decode为泛型函数。这里使用的是泛型T,T必须符合Decodable协议。

君宇大白话

下面使用一个简单的大白话来介绍一下这个代码:

我们为了扩展Bundle,给Bundle新增了一个decode函数。我们让decode函数扩展性增强,使用了泛型,让其能够接收并处理多种类型的json文件。

extension Bundle {
    func decode<T: Decodable>(_ file: String) -> T {

因此,我们在func decode的后面添加了泛型<T: Decodable>,这表示我们的函数使用了泛型T,并且这个泛型T必须符合Decodable协议。

如果这里不理解为什么要添加T的话,我们可以忽略掉<T: Decodable>。

extension Bundle {
    func decode(_ file: String) -> some {

我们再回来看一下这个代码,是否跟平时学习到的代码一样了?所以,我们如果想要让函数能够处理单一的类型,比如我们只处理[String: Astronaut]这种字典格式:

extension Bundle {
func decode(_ file: String) -> [String: Astronaut] {

当我们访问[String: Astronat]格式的JSON文件时,我们可以正常读取它们。

因此,我们的扩展decode函数处理的类型比较单一,但我们处理单一的结构数据时就会报错,比如当我们处理下面这个代码时:

// astronautsSignle.json
{
    "id": "grissom",
    "name": "Virgil I. \"Gus\" Grissom",
    "description": "Virgil Ivan \"Gus\" Grissom (April 3, 1926 – January 27, 1967) was one of the seven original National Aeronautics and Space Administration's Project Mercury astronauts, and the first of the Mercury Seven to die. He was also a Project Gemini and an Apollo program astronaut. Grissom was the second American to fly in space, and the first member of the NASA Astronaut Corps to fly in space twice.\n\nIn addition, Grissom was a World War II and Korean War veteran, U.S. Air Force test pilot, and a mechanical engineer. He was a recipient of the Distinguished Flying Cross, and the Air Medal with an oak leaf cluster, a two-time recipient of the NASA Distinguished Service Medal, and, posthumously, the Congressional Space Medal of Honor."

}

当我们点击按钮时,就会提示:Preview Crashed(预览崩溃),这个报错的原因就是我们的扩展decode函数只能处理[String: Astronaut]这一种字典类型,使用decode读取Astronaut结构的JSON字符串就会报错。

因此,如果我们想处理Astronaut结构的JSON字符串,我们就需要修改扩展decode函数的类型部分。

1)修改返回的函数类型,将[String: Astronaut]改为Astronaut类型。

func decode(_ file: String) -> [String: Astronaut] {

改为:

func decode(_ file: String) -> Astronaut {

2)我们需要修改JSONDecoder解码的类型,将[String: Astronaut]改为Astronaut。

guard let loaded = try? decoder.decode([String: Astronaut].self, from: data) else {

改为:

guard let loaded = try? decoder.decode(Astronaut.self, from: data) else {

修改完上述两个部分,我们再重新点击按钮,就发现能够正常读取了。

那么,当我们的扩展decode函数有更多的代码时,涉及的类型越多,我们修改的部分就越多。这也引出了我们的类型万金油——泛型。

泛型的使用部分也很简单,如果我们需要一个万能的格式类型,我们就可以在我们刚才的扩展decode函数中添加一个<T>:

func decode<T>(_ file: String) -> Astronaut {

为什么要添加这个T呢,这表示要在这个函数中要用到泛型。就像给一个厕所门口放置了一个打扫牌一样。

这表明我们要给这个厕所打扫卫生,会有打扫人员进行这个场所打扫。至于这个场所是由大叔还是阿姨,现在还是稍后打扫,都不清楚。这个提前的放置的标识牌就可以理解为我们的函数占位符。

所以我们要把这个厕所的打扫牌放到扩展decode函数的门口,我们就写成了:

<T>

这种形式,跟牌子一样,里面通常用T或者U等字母。这就可以表示,我们要放进去一个参数,至于这个参数是什么,是由我们手动赋值或者由我们的泛型来判断的。

又因为我们的代码涉及了Bundle查找文件、Data转换以及JSONDecoder解码文件,所以我们需要我们这个泛型支持Decodable解码协议。

func decode<T: Decodable>(_ file: String) -> some {

在这里,我们放置了一个厕所的打扫牌,<T>,同时让其支持Decodable协议。

接着,我们保存代码的话,Xcode会提示:Generic parameter ‘T’ is not used in function signature(函数签名中未使用通用参数“T”),这表示我们没有用到这个泛型T,就像我们在厕所门口只放了一个打扫牌,但没有人进行打扫一样。

Xcode就像巡查人员一样,发现了这个问题并报了这个错误。

那么,我们要如何用到这个泛型<T>呢?我们需要将原本只能处理[String: Astronaut]或Astronaut更改为我们的泛型<T>,我们的扩展decode函数就变成了:

import SwiftUI

extension Bundle {
    func decode<T: Decodable>(_ file: String) -> T {
        guard let url = self.url(forResource: file, withExtension: nil) else {
            fatalError("Failed to locate \(file) in bundle.")
        }

        guard let data = try? Data(contentsOf: url) else {
            fatalError("Failed to load \(file) from bundle.")
        }

        let decoder = JSONDecoder()

        guard let loaded = try? decoder.decode(T.self, from: data) else {
            fatalError("Failed to decode \(file) from bundle.")
        }

        return loaded
    }
}

这里我们只变更了两个格式部分,那就是将[String: Austronaut]或Austronaut改为了T这个占位符。

再回到我们关于厕所打扫牌的故事,原先我们只能指定的李阿姨或者张大爷打扫,但是我们现在使用了一个打扫牌,所以可以让任何一个打扫人员进行打扫。

如果这时我们保存代码,我们就会发现在调用扩展decode函数的部分:

let astronauts = Bundle.main.decode("astronautsSingle.json")

报了下面的错误:

Generic parameter 'T' could not be inferred

翻译为:无法推断通用参数“T”。

为什么我们我们的泛型不能推断通用参数“T”呢,这是因为编译器在涉及到解码JSON数据时,因为解码操作的泛型decode<T: Decodable>可以解码为多种类型,编译器无法自动推断要解码的对象类型,所以必须显式的告诉编译器期望的类型。

未显式告诉编译器期望的类型:

let astronauts = Bundle.main.decode("astronauts.json")

显式告诉编译器期望的类型:

let astronauts: [String: Astronaut] = Bundle.main.decode("astronauts.json")

我们可以看到,区别就在于,定义Bundle.main.decode函数时,需要给赋值的对象添加一个类型:[String: Astronaut]。

通过这种方式,就可以明确告诉编译器你期望的decode方法返回一个[String: Astronaut]类型的值,编译器就知道如何来解码JSON数据,如果不指定返回类型,编译器就不知道你希望从decode方法中得到哪种类型的数据。

总体来看,我们使用泛型的话,每次我们只要在定义Bundle.main.decode函数时,给一个编译器期望的类型即可。

let astronauts: [String: Astronaut] = Bundle.main.decode("astronauts.json")

如果我们不用泛型,那么我们需要根据要解码的类型,来修改对应的类型。

func decode(_ file: String) -> [String: Astronaut] {

相比之下,泛型即使需要显式告诉它类型,我们也只需要在定义函数时给一个编译器期望的类型,而不需要修改多处解码的类型。

因此,使用泛型让我们的代码更简单,就是一个批量替换的操作一样,让我们的泛型T全部变成我们想要指定的类型。

这时有人可能还会有疑问,那就是为什么这篇教程最开始时的compare函数,在调用时不需要指定返回类型?

// 这是一个泛型函数,可以比较任意类型的两个值
func compare<T: Comparable>(_ a: T, _ b: T) -> Bool {
    return a == b
}
let isEqualInt = compare(5, 5) // true
let isEqualString = compare("Swift", "Swift") // true

这是因为compare函数的泛型类型T时可以通过函数参数自动推断出来。

当编译器根据传递的参数来判断T的具体类型,在compare(5,5)这一行,编译器知道5是Int类型,所以T被推断为Int。在compare(“Swift”,”Swift”)中,T被推断为String。因此,不需要显式指定T的类型,编译器可以自动完成推断。

而我们在泛型样例的代码中,扩展decode函数涉及泛型的两个部分,一个是返回的类型,另一个是解码的类型,无法根据输入的json文件来判断解码和返回的类型,因此必须要显式指定。

func decode<T: Decodable>(_ file: String) -> T {
...
guard let loaded = try? decoder.decode(T.self, from: data) else {

最后,我们再反过来看一下我们的代码:

struct Astronauts: View {
    var body: some View {
        Button("对比两个参数") {
            let astronauts:[String: Astronaut] = Bundle.main.decode("astronauts.json")
            print(astronauts)
        }
    }
}

当我们显式指定泛型为[String: Astronaut]格式后,我们扩展的decode函数就会知道我们的泛型T为[String: Astronaut]格式,然后将T都改为[String: Astronaut]格式。

所以我们下面的泛型代码:

extension Bundle {
    func decode<T: Decodable>(_ file: String) -> T {
        guard let url = self.url(forResource: file, withExtension: nil) else {
            fatalError("Failed to locate \(file) in bundle.")
        }

        guard let data = try? Data(contentsOf: url) else {
            fatalError("Failed to load \(file) from bundle.")
        }

        let decoder = JSONDecoder()

        guard let loaded = try? decoder.decode(T.self, from: data) else {
            fatalError("Failed to decode \(file) from bundle.")
        }

        return loaded
    }
}

跟下面的指定[String: Astronaut]类型的代码是相等的:

extension Bundle {
    func decode<T: Decodable>(_ file: String) -> [String: Astronaut] {
        guard let url = self.url(forResource: file, withExtension: nil) else {
            fatalError("Failed to locate \(file) in bundle.")
        }

        guard let data = try? Data(contentsOf: url) else {
            fatalError("Failed to load \(file) from bundle.")
        }

        let decoder = JSONDecoder()

        guard let loaded = try? decoder.decode([String: Astronaut].self, from: data) else {
            fatalError("Failed to decode \(file) from bundle.")
        }

        return loaded
    }
}

因为我们为泛型指定了一个类型,所以跟随的就是T也变成了指定的类型。

在回到我们的比喻上,我们将厕所原本没有打扫牌(占位符)时,我们只能让张大爷或李阿姨去打扫固定的厕所,所以每当有人休班时,都需要将打扫的人员变更为其他人。

当我们有打扫牌(占位符)后,我们只需要放一个打扫牌。然后不管是哪个人都可以进去打扫,因为我们已经放了一个打扫牌,这个打扫牌就对应的这个厕所的打扫人员,而不必指定具体的某一个人来打扫。

所以,这就是我们的泛型,作为一个占位符在代码中,替换所需要的格式类型。

希望读到这里的人,能够彻底理解泛型的意义。

如果您认为这篇文章给您带来了帮助,您可以在此通过支付宝或者微信打赏网站开放者。

发表回复

您的电子邮箱地址不会被公开。 必填项已用 * 标注