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

2021-03-18  本文已影响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 []
    }
}

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

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

        我们终于达到了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 :)方法。 尽管这为我们提供了很大的灵活性,但并不是我们应用程序中的所有类型都可解码。
        这里的问题是: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编译器由于其自动类型推断功能,可以将泛型与类型匹配,从而检查我们代码的正确性。

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

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

上一篇下一篇

猜你喜欢

热点阅读