美文网首页
掌握Swift泛型:一个实际的代码重用示例

掌握Swift泛型:一个实际的代码重用示例

作者: 行知路 | 来源:发表于2021-03-18 16:52 被阅读0次

    一、序言

            泛型是Swift的一项强大功能,可让您以其他方式无法实现的方式泛化和重用代码。泛型具有许多强大的特型,这些特型也成为了许多开发人员的障碍。iOS的SDK广泛的使用了泛型,特别是在SwiftUI中。在这篇文章中,我讲解释为什么泛型存在并且如何在你的App里使用它。

    二、直到遇到真实的泛型使用场景之后你才能真正理解泛型

            Swift的标准库与其他的苹果的库中都大量使用了泛型。
            幸运的是,Swift编译器执行的类型推断通常将泛型代码隐藏在熟悉的代码后面。 你可能在构建iOS应用程序时走得很远,而无需了解泛型或不知道它们的存在。
            但是,在某些情况下,你与泛型终究是会相遇。
            正如我再这篇文章里想你展示的那样——你无法重用你的代码,除非你掌握了Swift的泛型。不但如此,泛型是面向协议编程的基础,特别是在为你的App构建坚固的网络层时。
            根据我的经验,我的学生经常很难理解Swift泛型。 问题在于,在没有了解因缺乏泛型而导致的代码局限之前,很难理解泛型为何有用。
            你通常需要一些曾经作为开发人员的经验才能理解到这点。 最后,如果初学者甚至中级iOS开发人员接受代码中的某些重复内容,都可以在不使用泛型的情况下开发应用。
            我看过许多文章,这些文章直接解释了什么泛型,并向你展示了如何从一开始就使用它们。 我在这种方法中发现的问题是,它仅教你泛型如何在该语言中工作。
            你没有学习到的是——如何识别那种场景(在这种场景中使用泛型可以获取好处)。
            所以,在这里,我讲采用相反的方法。我将从简单的、特殊的代码开始,直到需要泛型的时候再把泛型引入到代码里。

    三、大部分你写的代码都隐式的使用泛型,从而是你的生活简单一些

            举例来说,让我们为一个社交网络应用(类似Facebook或Meetup)构建几个界面,人们可以在其中加入活动。查看源码请点我

    界面示例
            正如你看到的,这两个界面很相似。它们展示不同的信息,但是它们的结构是相同的。
            你创建的有意义的应用程序都将具有要出于不同目的重用的代码。
            有时候,这很明显,就像上面的两个应用程序界面一样。 但这可能出现在代码的任何部分。
            让我们开始定义一些可用于填充应用界面的数据。 此类数据通常来自网络的JSON数据,因此将其存储在Xcode项目中的.json文件中以进行测试是一种很好的做法。
    // Events.json
    [
        {
            "title": "Book club",
            "date": 1591454840,
            "participants": 12
        },
        ...
    ]
     
    // Participants.json
    [
        {
            "name": "Quinten Kortum",
            "friends": 4,
            "joined": 1578250800
        },
        ...
    ]
    

            为简洁起见,上面我仅列出每个文件一项。你可以在Xcode项目中找到完整的数据。
            现在,我们需要两种模型类型来表示应用程序中的事件和人物,并使用Codable协议解码JSON数据。

    struct Event: Decodable {
        let title: String
        let date: Date
        let participants: Int
    }
     
    struct Person: Decodable {
        let name: String
        let friends: Int
        let joined: Date
    }
    

            这是隐式地使用Swift泛型代码的第一个示例。Decodable如何使用泛型是比较复杂的,如果你想要深入了解,可以再阅读文章之后,自己去发掘。
            在这里我只是想指出的它们是泛型的,即使你没有发现他。Codable通过泛型使事情很复杂,例如编码和解码,但是使你的代码很简单。

    四、泛化返回值

            现在,我们要解码两个JSON文件,并准备好数据来测试我们将要构建的接口。这很简单。 我们要做的就是读取每个文件中的数据,并将其提供给JSONDecoder对象。

    struct TestData {
        static let events: [Event] = loadEvents()
        static let participants: [Person] = loadParticipants()
        
        static func loadEvents() -> [Event] {
            let url = Bundle.main.url(forResource: "Events", withExtension: "json")!
            let data = try! Data(contentsOf: url)
            let decoder = JSONDecoder()
            decoder.dateDecodingStrategy = .secondsSince1970
            return try! decoder.decode([Event].self, from: data)
        }
        
        static func loadParticipants() -> [Person] {
            let url = Bundle.main.url(forResource: "Participants", withExtension: "json")!
            let data = try! Data(contentsOf: url)
            let decoder = JSONDecoder()
            decoder.dateDecodingStrategy = .secondsSince1970
            return try! decoder.decode([Person].self, from: data)
        }
    }
    

            这里有一大堆的重复代码,所以我想尽可能的重用、泛化这些代码。
            泛化这两种方法的第一行很容易。 我们需要的是每个文件名称的String参数。 以下几行是相同的,因此我们在那里没有任何问题。
            但是最后一行是不容易泛化的。在那里,我们指明那种模型作为返回值。
            但是,如何才能泛化方法的返回值?
            Swift提供了Any类型,可以表示任何类型。 我们可以尝试使用这种方法来加载事件和参与者的单一方法,但这并不是一个很好的解决方案。

    struct TestData {
        static let events: [Event] = readFile(named: "Events") as! [Event]
        static let participants: [Person] = readFile(named: "Participants") as! [Person]
        
        static func readFile(named name: String) -> [Any]  {
            let url = Bundle.main.url(forResource: name, withExtension: "json")!
            let data = try! Data(contentsOf: url)
            let decoder = JSONDecoder()
            decoder.dateDecodingStrategy = .secondsSince1970
            if let events = try? decoder.decode([Event].self, from: data) {
                return events
            } else if let participants = try? decoder.decode([Person].self, from: data) {
                return participants
            }
            return []
        }
    }
    

            在这里,我们尝试使用两种模型类型对文件进行解码。 如果我们得到一些数据,那么它是正确的类型。 否则,我们尝试下一个。
            上面的代码能够工作,但是有一些问题。

    • 每当我们向应用程序添加新的可解码模型类型时,我们都必须扩展条件语句。 (如果你很好奇,则违反了开放式封闭原则)。
    • 使用Any作为返回类型将擦除类型信息。 每次调用readFile(named :)时,都必须使用as将其结果转换为所需的类型
    • 编译器无法帮助我们。 我们可以将readFile(named :)的结果转换为任何类型。 如果我们犯了一个错误,我们只会在我们的应用崩溃时发现它(可能是在用户手中)。

    五、使用泛型来参数化函数中的类型

            我们终于达到了OOP方法的极限。
            在readFile(named:)函数中,我们不但需要参数来指定函数的要打开的文件,同时需要一个类型来制定函数的返回值。
            我们希望能够说出readFile(named :)返回的类型是[Event]还是[Person],就像我们对常规参数所做的那样。
            显然有一种方法可以做到这一点。 我们已经将类型参数传递给JSONDecoder的encode(_:from :)方法。
            实际上,这就是泛型的。 同样,即使你不知道泛型,我们也会使用泛型。 但是现在,我们看到了泛型是什么:它们使我们也可以将类型用作参数,而不仅仅是值。
            在Swift中,您可以使用尖括号在函数名称后立即声明泛型。

    struct TestData {
        static let events: [Event] = readFile(named: "Events")
        static let participants: [Person] = readFile(named: "Participants")
        
        static func readFile<ModelType>(named name: String) -> [ModelType]  {
            let url = Bundle.main.url(forResource: name, withExtension: "json")!
            let data = try! Data(contentsOf: url)
            let decoder = JSONDecoder()
            decoder.dateDecodingStrategy = .secondsSince1970
            return try! decoder.decode([ModelType].self, from: data)
        }
    }
    

    注意
    这里的代码依然有问题,后续我们将讲到如何解决。

            一些开发人员使用单个字母(例如T,U等)来命名其泛型,但我觉得这太难读了。尽可能尝试使用有意义的名称,这也是Swift标准库的做法。 在这种情况下,此方法处理模型类型,因此ModelType是比M更好的名称。
            在方法中声明泛型后,可以将其用作:

    • 返回类型(我们的情况);
    • 任何参数的类型;
    • 任何局部常量/变量的类型;
    • 另一个通用函数的参数。
              我们的ModelType泛型充当了我们不知道的类型的占位符。 仅当使用方法时才确定该类型。
              你可以在对readFile(named :)的两次调用中看到这一点。 在那里,我们不再需要强制类型转换。 TestData结构的事件和参与者静态属性具有显式类型。 由于类型推断,Swift编译器可以确定在每次调用中使用哪种类型。
              这样,编译器就可以进行所有必要的检查,并在类型不匹配时向您发出警告。 如果使用Any,则不会发生某些事情。
              正如我提到的,此代码尚不能编译。 编译器现在在抱怨我们的代码中的一些问题。

    六、限制泛型的选项,并使用类型约束确保正确性

            我们的方法还不能编译,原因是ModelType太“泛型”了。
            目前,我们可以使用任何类型的readFile(named :)方法。 尽管这为我们提供了很大的灵活性,但并不是我们应用程序中的所有类型都可解码。
            这里的问题是:JSONDecoder的encode(_:from :)方法只需要符合Decodable的类型。 否则,解码器无法将JSON数据映射到类型的属性。为了约束可与泛型一起使用的类型,我们使用类型约束。 这些仅将泛型限制为源自特定类或符合一个特定协议的类型。在我们的情况下,我们希望ModelType泛型符合Decodable。

    struct TestData {
        static let events: [Event] = readFile(named: "Events")
        static let participants: [Person] = readFile(named: "Participants")
        
        static func readFile<ModelType: Decodable>(named name: String) -> [ModelType]  {
            let url = Bundle.main.url(forResource: name, withExtension: "json")!
            let data = try! Data(contentsOf: url)
            let decoder = JSONDecoder()
            decoder.dateDecodingStrategy = .secondsSince1970
            return try! decoder.decode([ModelType].self, from: data)
        }
    }
    

            现在我们的代码可以工作了。 编译器知道我们将用于泛型的任何类型都符合Decodable。 如果我们尝试使用不使用的类型,则编译器将阻止我们犯错误。
            使用Any时也不会发生这种情况。 在这种情况下,编译器没有有关基础类型的信息。
            现在我们可以看到编译器阻止了我们,因为decode(_:from :)方法也是一个泛型函数。 当你在Foundation框架的头文件中查看其声明时,这一点很明显。

    open class JSONDecoder {
        
        // ...
        
        open func decode<T>(_ type: T.Type, from data: Data) throws -> T where T : Decodable
    }
    
    

            在这里,你可以看到在泛型上声明类型约束的另一种方法。 T的可分解约束位于方法声明后面的where子句中,而不是在泛型的声明中。

    七、Swift标准库中的泛型

            在我们的TestData结构中,只有readFile(named :)方法是通用的。 但是泛型也可以用于整个类型,而不仅仅是功能。
            Swift标准库的常规ArrayDictionary数据结构就是很好的例子。 这是泛型在日常代码中清晰可见的另一种情况。
            你可以将任何类型的值放入数组和字典中。 一旦确定了集合内容的类型,编译器便可以检查所有值是否都属于同一类型。
            既然我们已经了解了泛型的工作原理,那么你可以理解为什么集合会以这种方式工作。 同样,你可以检查它们的声明并查看它们是否使用泛型。

    @frozen public struct Array<Element> {
        // ...
    }
     
    @frozen public struct Dictionary<Key, Value> where Key : Hashable {
        // ...
    }
    

            Array和Dictionary的Element和Value泛型没有任何类型约束。 因此,你可以将任何东西放入集合中。
            Dictionary类型还具有带有Hashable类型约束的Key泛型。 这是因为字典是一个哈希表,使用哈希函数存储其内容。
            Swift中还有另一种泛型类型——可选类型。
            Swift出色地完成了将可选内容隐藏在大量语法糖后面的工作。 您使用?声明了可选参数。 运算符,使用nil表示不存在的值,并使用?,??和!展开可选项。 您也可以在条件语句中使用条件绑定,即let或guard let。
            但是,在幕后,可选选项只不过是包含两种情况的泛型枚举。

    @frozen public enum Optional<Wrapped> : ExpressibleByNilLiteral {
        case none
        case some(Wrapped)
        // ...
    }
    

            none情况表示一个nil值,而some情况包含一个值(如果存在)。 由于可选对象还需要使用任何类型,因此可选枚举使用了无类型约束的Wrapped泛型。
            这意味着,通过可选,你可以使用任何与枚举一起使用的Swift构造。

    八、先写具体的代码,发现需要之后,再改写为泛型

            数组,词典和可选变量是你在许多文章中发现的典型的泛型类型示例。 同样,它们向你展示了泛型是如何工作的,但是它们并不能帮助你了解如何在代码中使用它们。
            集合是必要且可理解的编程概念。 但是泛型在其他不太明显的类型中也很有用。
            我们的应用程序用户界面将提供一个很好的例子。从设计中我们已经知道事件列表和参与者列表具有相同的结构。 尽管很明显他们需要共享代码,但最好还是先开始编写特定的代码。
            仅在明显需要将哪些内容归纳之后,才对代码进行归纳。 直接从通用代码开始通常会导致过度优化。虽然通常在性能上下文中使用该概念,但通常可以将其扩展到代码编写的有效性。在达到限制之前,你将不知道需要对代码的哪些部分进行泛化。
            通常,开发人员不必要地编写通用代码。 他们认为,在某些时候,他们的代码将需要与几种类型一起使用。 但是实际上,大多数代码仅在一个特定实例中使用。
            因此,不要仅仅因为某些将来可能永远不会发生的假设用例,而使你的代码难以阅读。 仅在需要通用代码时才通用代码。
            我们可以从“联接”按钮为表行创建表,这是一个仅需基本参数的简单视图。

    struct RowButton: View {
        let title: String
        let color: Color
        
        var body: some View {
            Text(title)
                .font(.subheadline)
                .bold()
                .foregroundColor(.white)
                .padding(EdgeInsets(top: 8.0, leading: 16.0, bottom: 8.0, trailing: 16.0))
                .background(color)
                .cornerRadius(20)
        }
    }
     
    struct EventsView_Previews: PreviewProvider {
        static var previews: some View {
            VStack(spacing: 8.0) {
                RowButton(title: "Join", color: .orange)
                RowButton(title: "Message", color: .blue)
            }
            .padding()
            .previewLayout(.sizeThatFits)
        }
    }
    
    image.png

            这样,我们可以为表中的事件行创建一个视图,并使用该视图创建事件的完整列表。

    struct EventsView: View {
        let events: [Event]
        
        var body: some View {
            NavigationView {
                List(events) { event in
                    EventRow(event: event)
                }
                .navigationBarTitle("Events")
            }
        }
    }
     
    struct EventRow: View {
        let event: Event
        
        var body: some View {
            HStack(spacing: 16.0) {
                Image(event.title)
                    .resizable()
                    .frame(width: 70.0, height: 70.0)
                    .cornerRadius(10.0)
                VStack(alignment: .leading, spacing: 4.0) {
                    Text(event.title)
                        .font(.headline)
                    Group {
                        Text(event.date.formatted(.full))
                        Text("\(event.participants) people going")
                    }
                    .font(.subheadline)
                    .foregroundColor(.secondary)
                }
                Spacer()
                RowButton(title: "Join", color: .orange)
            }
            .padding(.vertical, 16.0)
        }
    }
     
    struct EventsView_Previews: PreviewProvider {
        static var previews: some View {
            Group {
                EventsView(events: TestData.events)
                VStack(spacing: 8.0) {
                    RowButton(title: "Join", color: .orange)
                    RowButton(title: "Message", color: .blue)
                }
                .padding()
                .previewLayout(.sizeThatFits)
            }
        }
    }
    
    image.png

    九、自定义协议来约束泛型类型

            现在我们有了一些可以分析的实际代码,我们可以将其概括化以与Event和Person类型一起使用。
            这也是我们需要参数化类型的情况。 我们知道我们需要用通用类来代替Event类型,但是仅那条信息并不能使我们走得太远。
            问题出在代码的某些部分中,这些部分使用了Event类型的特定属性。

    struct EventRow: View {
        let event: Event
        
        var body: some View {
            HStack(spacing: 16.0) {
                Image(event.title)
                    .resizable()
                    .frame(width: 70.0, height: 70.0)
                    .cornerRadius(10.0)
                VStack(alignment: .leading, spacing: 4.0) {
                    Text(event.title)
                        .font(.headline)
                    Group {
                        Text(event.date.formatted(.full))
                        Text("\(event.participants) people going")
                    }
                    .font(.subheadline)
                    .foregroundColor(.secondary)
                }
                Spacer()
                RowButton(title: "Join", color: .orange)
            }
            .padding(.vertical, 16.0)
        }
    }
    

            人员类型没有标题,日期和参与者属性。在我们的特定示例中,我们可以重命名Person的属性以匹配这些名称,但这仍然无济于事。
            我们添加到视图中的任何泛型都将独立于Event和Person类型。 它们具有共同的属性并不重要。 使用泛型,无论如何我们都无法访问它们中的任何一个。
            此外,这还是一个不好的做法,因为一个人没有头衔或参与者。 在其他情况下,你可能会有无法匹配的类型。
            每次我们需要对泛型进行假设时,都需要使用类型约束。 不过,在这种情况下,我们没有可以使用的协议。
            解决方案是创建一个自定义的。我们在表格行中显示的任何类型都必须具有标题,两个子标题,图像等。

    protocol TableItem {
        static var navigationTitle: String { get }
        static var actionName: String { get }
        static var buttonColor: Color { get }
     
        var headline: String { get }
        var imageName: String { get }
        var subheadline1: String { get }
        var subheadline2: String { get }
    }
    

            请注意,我在TableItem协议中同时使用了常规属性和静态属性。 这是因为headline,imageName,subheadline1和subheadline2是随每个值更改的属性,但是对特定类型的所有值,navigationTitle,actionName和buttonColor保持不变。

    十、使用类型约束使泛型具体化

            现在我们有了定义行要求的协议,我们可以将其用作泛型的类型约束。
            一旦泛型受到约束,你就可以将其视为该协议的实例,因为您知道编译器将强制执行其要求。

    struct Row<Item: TableItem>: View {
        let item: Item
        
        var body: some View {
            HStack(spacing: 16.0) {
                Image(item.imageName)
                    .resizable()
                    .frame(width: 70.0, height: 70.0)
                    .cornerRadius(10.0)
                VStack(alignment: .leading, spacing: 4.0) {
                    Text(item.headline)
                        .font(.headline)
                    Group {
                        Text(item.subheadline1)
                        Text(item.subheadline2)
                    }
                    .font(.subheadline)
                    .foregroundColor(.secondary)
                }
                Spacer()
                RowButton(title: Item.actionName, color: Item.buttonColor)
            }
            .padding(.vertical, 16.0)
        }
    }
    

            EventsView类型包含EventRow视图,我们将其更改为Row <Item>泛型类型。 包含通用类型的任何类型都必须为通用类型指定一种类型,或者也必须公开该通用类型。 在这里,我们需要第二种选择。

    struct TableView<Item: TableItem & Identifiable>: View {
        let items: [Item]
        
        var body: some View {
            NavigationView {
                List(items) { item in
                    Row(item: item)
                }
                .navigationBarTitle(Item.navigationTitle)
            }
        }
    }
    

            TableView类型的Item泛型也必须符合Identifiable协议,因为List视图需要这样做。 在Swift中,你可以使用&运算符在类型声明中编写协议。
            最后一步是使我们的模型类型同时符合TableItem和Identifiable协议。

    struct Event: Decodable, Identifiable {
        let title: String
        let date: Date
        let participants: Int
        
        var id: String { title }
    }
     
    extension Event: TableItem {
        static var navigationTitle: String { "Events" }
        static var actionName: String { "Join" }
        static var buttonColor: Color { .orange }
        
        var headline: String { title }
        var imageName: String { title }
        var subheadline1: String { date.formatted(.full) }
        var subheadline2: String { "\(participants) people going" }
    }
     
    struct Person: Decodable, Identifiable {
        let name: String
        let friends: Int
        let joined: Date
        
        var id: String { name }
    }
     
    extension Person: TableItem {
        static var navigationTitle: String { "Participants" }
        static var actionName: String { "Message" }
        static var buttonColor: Color { .blue }
        
        var headline: String { name }
        var imageName: String { name }
        var subheadline1: String { "\(friends) friends" }
        var subheadline2: String { "Joined \(joined.formatted(.long))" }
    }
    

            使用Swift扩展使我们能够满足TableItem的要求,而不必以任何方式更改Event和Person类型。多亏了此添加,编译器现在允许我们在TableView泛型结构中使用这两种类型。

    struct TableView_Previews: PreviewProvider {
        static var previews: some View {
            Group {
                TableView(items: TestData.events)
                TableView(items: TestData.participants)
            }
        }
    }
    
    image.png

    十一、总结

            Swift泛型是一种强大的语言功能,它使我们能够以其他方式无法实现的方式来抽象代码。

            多亏了泛型,我们不仅可以将值用作参数,而且可以将类型用作类型。 此外,Swift编译器由于其自动类型推断功能,可以将泛型与类型匹配,从而检查我们代码的正确性。

            但是请注意,与任何其他高级功能一样,泛型会使您的代码更难以理解。

            除非有必要,否则不要接使用泛型。 在过早地使泛型,这是过早优化的一种情况,这会使你的代码不必要地变得复杂。 始终从具体的代码开始,并且仅在遇到需要它的具体情况时才对其进行泛化。

    相关文章

      网友评论

          本文标题:掌握Swift泛型:一个实际的代码重用示例

          本文链接:https://www.haomeiwen.com/subject/mhrscltx.html