一、序言
泛型是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标准库的常规Array
和Dictionary
数据结构就是很好的例子。 这是泛型在日常代码中清晰可见的另一种情况。
你可以将任何类型的值放入数组和字典中。 一旦确定了集合内容的类型,编译器便可以检查所有值是否都属于同一类型。
既然我们已经了解了泛型的工作原理,那么你可以理解为什么集合会以这种方式工作。 同样,你可以检查它们的声明并查看它们是否使用泛型。
@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编译器由于其自动类型推断功能,可以将泛型与类型匹配,从而检查我们代码的正确性。
但是请注意,与任何其他高级功能一样,泛型会使您的代码更难以理解。
除非有必要,否则不要接使用泛型。 在过早地使泛型,这是过早优化的一种情况,这会使你的代码不必要地变得复杂。 始终从具体的代码开始,并且仅在遇到需要它的具体情况时才对其进行泛化。
网友评论