09、Swift中的class
1、struct 和 class的差异
作为Swift中的另外一种自定义类型,从语法上来说,class和struct有很多相似的地方,它们都可以用来自定义类型、都可以有properties,也都可以有methods。
作为Swift中的引用类型,class表达的是一个具有明确生命周期的对象,我们需要关注这类内容的“生死存亡”,而值类型,我们更多关注的,就真的只是它的值而已。
1.1 引用类型必须明确指定init方法
首先,Swift并不会为class自动生成默认的init方法。如果我们不定义它,Swift编译器会报错。
1.2 引用类型关注的是对象本身
其次,class和struct对“常量”的理解是不同的。我们分别定义一个PointRef和PointValue的常量:
let p1 = PointRef(x: 0, y: 0)
let p2 = PointValue(x: 0, y: 0)
同样是常量,当我们修改p2的属性时,编译器会报错:p2 is a let constant:
p2.x = 10 // Compile time error
但是,我们却可以修改p1:
p1.x = 10 // OK
这是因为,p2作为一个值类型,常量的意义当然是:“它的值不能被改变”。但是p1作为一个引用类型,常量的意义则变成了,它可以修改自身的属性,但不能再引用其他的PointRef对象。如果我们尝试让p1引用另外一个PointRef对象,就会发生下面的错误:
p1 = PointRef(x: 1, y: 1) // Compile time error
以上就是引用类型代表的“对象”和值类型代表的“值本身”在语义上的差别。而这种差别,还体现在了对它们各自进行赋值之后的表现上:
var p3 = p1
var p4 = p2
这之后,当我们使用===比较p1和p3的时候,得到的结果是true:
p1 === p3 // true
并且,当我们修改了p3之后,p1的值,会一并修改:
p3.x = 10
p1.x // 10
但是,当我们修改了一个值类型时,却并不会这样:
p4.x = 10
p2.x // 0
1.3 引用类型默认是可以修改的
由于引用类型关注的是其引用的对象,而不是对象的值。因此,它的方法默认是可以修改对象属性的。例如:
class PointRef {
// ...
func move(to: PointRef) {
self.x = to.x
self.y = to.y
}
}
但是,对于值类型PointValue来说move必须用mutating来修饰:
struct PointValue {
// ...
mutating func move(to: PointValue) {
self.x = to.x
self.y = to.y
}
}
所以,修改一个struct的本意,实际上是你需要一个全新的值。
最后,还有一点要说明的是,在PointValue里,我们可以直接给self赋值:
mutating func move(to: PointValue) {
self = to
}
编译器知道对一个值类型赋值就是简单的内存拷贝,因此,他会自动用to的每一个属性设置self的对应属性。但是,对于一个引用类型来说,你却不能这样:
class PointRef {
// ...
func move(to: PointRef) {
self = to // !! Compile time error !!
}
}
在class的方法里,self自身是一个常量,我们不能直接让它引用其它的对象。
2、理解class类型的各种init方法
2.1 默认init
2.1.1 方式一
给每一个属性都添加默认值
class Point2D {
var x: Double = 0
var y: Double = 0
}
调用
let origin = Point2D()
2.1.2 方式二
通常,我们还是至少会为class添加一个memberwise init方法。哪怕它就是一个逐个属性赋值的方法。
class Point2D {
var x: Double
var y: Double
init(x: Double = 0, y: Double = 0) {
self.x = x
self.y = y
}
}
为了让一个对象可以默认构造,class必须提供一个不需要参数的init方法,并且,这个方法必须初始化class的每一个属性。
在Swift里,这种真正初始化class属性的init方法,叫designated init,它们必须定义在class内部,而不能定义在extension里,否则会导致编译错误。
另外,除了designated init方法之外,还有一类不真正初始化class属性的方法。
2.2 Convenience init
extension Point2D {
convenience init(at:(Double,Double)) {
self.init(x:at.0,y:at.1)
}
}
let point = Point2D(at: (2.0,2.0))
这时,我们就需要把作为参数的(2.0, 2.0)拆开成Point2D的每一个属性,然后调用designated init。对于完成这类任务的init方法,就叫做convenience init。
可以看到,对于convenience init来说,它有两个要素:
- 使用convienience关键字修饰;
- 必须最终调用designated init完成对象的初始化;如果我们直接在convenience init中设置self.x或self.y,会导致编译错误;
2.3 Failable init
例如,我们希望用一个String tuple初始化Point2D:
let point44 = Point2D(at: ("4.0", "4.0"))
参考之前的convenience init,我们可以如法炮制一个:
class Point2D {
// ...
convenience init?(at: (String, String)) {
guard let x = Double(at.0),
let y