欢迎关注微信公众号「Swift 花园」
创建你自己的结构体
Swift 允许你用两种方式创建自己的类型。其中一种最常见的叫做结构体,即 struct
。Struct 可以拥有自己的变量、常量以及函数,而你可以在任意时候创建和使用它们。
让我们以一个简单的例子开始:创建一个 Sport
结构体,它有一个叫 name
的字符串变量。在结构体中,这种变量被称为 属性 。因此,这是一个拥有一个属性的结构体。
1 | struct Sport { |
类型定义完成,现在让我们来创建和使用它的实例:
1 | var tennis = Sport(name: "Tennis") |
name
和 tennis
都是变量,因而我们可以像常规变量那样修改它们:
1 | tennis.name = "Lawn tennis" |
属性可以像常规变量那样拥有默认值,并且依赖 Swift 的类型推断。
计算属性
我们刚刚创建了 Sport
结构体:
1 | struct Sport { |
它有一个叫 name 的属性,存储 String
类型。这种属性叫做 存储 属性,因为 Swift 还有另外一种属性,它叫 计算 属性。这是一种通过运行代码来获得值的属性。
让我们为 Sport
结构体再增加一个存储属性,然后是一个计算属性。代码如下:
1 | struct Sport { |
如你所见, olympicStatus
看起来像一个常规的 String
,但它其实是依据其他的属性返回不同的值。
让我们来创建一个 Sport
实例:
1 | let chessBoxing = Sport(name: "Chessboxing", isOlympicSport: false) |
属性观察者
属性观察者允许我们可以在属性变化前后运行代码。让我们来写一个名叫 Progress
的结构体,这个结构体追踪一个任务以及它完成的百分比:
1 | struct Progress { |
现在,创建这个结构体的实例,随着时间的推移调整它的进度:
1 | var progress = Progress(task: " 加载数据 & quot;, amount: 0) |
我们期望 Swift 在每一次 amount
改变的时候都打印信息,这里可以用到一个叫 didSet
属性观察者。它可以用于每一次 amount
改变之后运行代码:
1 | struct Progress { |
你还可以用到叫 willSet
的属性观察者。它是在属性改变之前作用,相对来说更不常用。
方法
Struct 的内部可以拥有函数,它们在必要时可以使用结构体的属性。这种函数被称为 方法 ,关键字也是 func
。
现在我们通过一个叫 City
的结构体来演示。它有一个 population
属性,用于存储城市里的人口。此外,它还有一个 collectTaxes ()
方法,这个方法返回人口数乘以 1000。 由于方法是属于 City
的,它可以读取当前城市的 population
属性。
代码如下:
1 | struct City { |
基于结构体调用方法的代码如下:
1 | let xiamen = City(population: 4_110_000) |
可变方法
如果一个结构体拥有一个变量属性,但是这个结构体的实例是以常量的方式创建的,那么在实例中,这个变量属性是不能修改的。这是因为结构体本身已经是常量了,所以它的所有属性也是常量。
这里面有一个问题, Swift 无从得知你将以常量还是变量的方式使用结构体。所以安全起见, Swift 的默认策略是:不允许你在方法里修改属性,除非你显式地要求这一点。
当你想要改变属性值时,需要在方法前使用 mutating
关键字,就像这样:
1 | struct Person { |
由于这个方法改变了属性值,所以 Swift 只会允许这个方法在变量型的 Person
实例上调用。
1 | var person = Person(name: "Ed") |
String 的属性和方法
目前为止我们已经大量地使用了字符串。你发现了吗?其实 String
类型是一个结构体类型。它有许多属性和方法,用于查询和维护字符串本身。
首先,我们创建一个测试字符串:
1 | let string = "Do or do not, there is no try." |
你可以用 count
属性来读取这个字符串里的字符数量:
1 | print(string.count) |
字符串有一个 hasPrefix ()
方法,可以用来检测字符串是否以特定字符开头:
1 | print(string.hasPrefix ("Do")) |
你还可以用 uppercased ()
方法把一个字符串转换成全大写的版本:
1 | print(string.uppercased ()) |
你甚至可以让 Swift 将字符串中的字母重新排序成一个数组:
1 | print(string.sorted ()) |
String 类型有大量的属性和方法。 你可以利用 Xcode 的代码补全,用 string.
调取这些选项看一看它们都有些什么能力。
数组的属性和方法
数组同样也是结构体,这意味着它们也有可以用来查询和操作数组的属性和方法。
从一个简单的数组开始:
1 | var toys = ["Woody"] |
你可以用 count
属性来读取数组的元素个数:
1 | print(toys.count) |
如果你需要增加一个元素,可以使用 append ()
方法,就像这样:
1 | toys.append ("Buzz") |
你可以用 firstIndex ()
方法来定位元素在数组里的位置,就像这样:
1 | toys.firstIndex (of: "Buzz") |
上面的代码会返回 1 ,因为数组位置从 0 开始计数。
跟字符串一样,你可以让 Swift 以字母表顺序给数组的元素重新排序。
1 | print(toys.sorted ()) |
最后,如果你想要移除数组里的一个元素,可以使用 remove ()
方法,就像这样:
1 | toys.remove (at: 0) |
数组类型也有大量的属性和方法。 你可以利用 Xcode 的代码补全,用 toys.
调取这些选项看一看它们都有些什么能力。
构造器
构造器是一种可以用来支持不同方式创建结构体的特殊方法。所有的结构体都有一个默认的构造器,这个构造器被称为 逐一成员构造器 ,它要求你为结构体的每一个属性都提供一个值。
让我们来声明一个 User
结构体,它有一个属性:
1 | struct User { |
当我们创建这个结构体的实例时,我们需要提供一个 username
:
1 | var user = User(username: "Paul") |
当然,我们也可以创建自己的构造器用以替换默认的。举个例子,我们可能希望所有的新用户默认都叫 “Anonymous” ,并且会打印一条信息,就像这样:
1 | struct User { |
你并不需要在构造器前面写 func
关键字,但你必须确保构造器结束前所有的属性都被赋值。
现在让我们来用上这个没有参数的构造器,就像这样:
1 | var user = User() |
引用当前实例
方法的内部,有一个特殊的常量叫 self
,它指向当前正在使用的结构体实例。当你在构造器中遇到参数名和属性名相同的情况时,这个 self
会很有用。
举个例子,如果你声明一个 Person
的结构体,它有一个 name
属性,并且你尝试写一个接收名为 name
参数的构造器,那么 self
可以帮助你区分属性和参数。 self.name
指代属性,而 name
指代参数。
代码如下:
1 | struct Person { |
懒加载属性
作为一种性能优化手段, Swift 允许你在用到的时候才真正创建属性。举个例子,这里有一个叫 FamilyTree
的结构体,它做的工作很容易描述。但理论上为一个人创建族谱可能会是一个很耗时的过程:
1 | struct FamilyTree { |
我们可以在 Person
结构体内将 FamilyTree
作为一个属性来使用,就像这样:
1 | struct Person { |
假如某些情况我们并不需要用到某个人的族谱信息呢?我们可以在 familyTree
属性前添加 lazy
关键字, Swift 将会在 familyTree
属性第一次被访问的时候才执行创建代码:
1 | lazy var familyTree = FamilyTree() |
因此,如果你想要看到 “创建族谱!” 这条信息被打印,你至少需要访问 familyTree
属性一次:
1 | ed.familyTree |
静态属性和方法
目前为止我们认识的所有属性和方法都是属于独立的结构体实例,这意味着假如我们有一个叫 Student
的结构体,我们可以创建几个 student 实例,每个实例都有各自的属性和方法:
1 | struct Student { |
你可以要求 Swift 在不同的结构体实例之间共享属性和方法,这些属性和方法被称为静态属性和静态方法,实现的方式是添加 static 声明。
现在,让我们给 Student
结构体添加一个静态属性,用以存放班级学生的总数。每当我们创建一个新的 student 实例时,我们将这个属性的数值加 1 :
1 | struct Student { |
classSize
属性是属于结构体本身而非结构体的实例,因此我们需要用 Student.classSize
来访问它:
1 | print(Student.classSize) |
访问控制
访问控制使得我们可以限制哪些代码能够访问属性和方法。这个机制在你想要保护属性免于被直接读取的时候很有用,举个例子:
我们仍然创建一个 Person
结构体,它有一个 id
属性,用来存放社保 ID:
1 | struct Person { |
一旦 person 实例被创建,我们希望它的 id
是私有的。私有的意思是你不能从结构体外部读取它,像 ed.id
这样的代码会变得不合法。
要做到这一点,你需要用到 private
关键字,像这样:
1 | struct Person { |
这么写之后,只有 Person
内部的方法才能读取 id
属性。举个例子:
1 | struct Person { |
还有一个常见的选项是 public
,它使得所有的其他代码都能够访问到目标属性或者方法。
总结
让我们一起来总结一下。
- 你可以创建自己的结构体类型,它们有自己的属性和方法。
- 你学习了存储属性,以及每一次通过临时计算得到值的计算属性。
- 当你希望在方法中修改属性时,需要把方法标记成
mutating
。 - 构造器是创建结构体时的特殊方法。默认情况下,你会得到一个逐一成员构造器。不过,如果你想要自行实现构造器的话,需要确保所有的属性都被赋值。
- 你可以在方法中用
self
常量来引用当前正在使用的结构体实例。 lazy
关键字告诉 Swift 你希望属性在第一次访问时才被创建。- 可以利用
static
关键字在结构体的所有实例间共享属性和方法。 - 访问控制使得我们可以限制哪些代码能够访问属性和方法。