12-Swift构造过程(Initialization)
构造过程是使用类、结构体或枚举类型一个实例的准备过程。在新实例可用前必须执行这个过程,具体操作包括设置实例中每个存储型属性的初始值和执行其他必须的设置或初始化工作。
通过定义构造器来实现构造过程,构造器即是创建特定类型实例的特殊方法。与OC构造器不同,swift的构造器无需返回值,它们的主要任务是保证新实例在第一次使用前完成正确的初始化。
一、存储属性的初始赋值
类和结构体在创建实例时,必须为所有存储类型属性设置合适的初始值。存储类型属性的值不能处于未知状态。
注意:当为存储类型属性设置默认值或在构造器中为其赋值时,它们的值是被直接设置的,不会触发任何属性观察者。
- 构造器。构造器在创建某个特定类型的新实例时调用,最简形式类似于一个不带参数的实例方法,以关键字
init
命名:
class Cat {
var name:String;
var color:String;
// 如果缺少如下构造过程,代码是会报错的,因为name和color没有初始值
init() { // 在此执行构造过程
print("开始构造");
name = "cat"; // 即设置默认值
color = "black"; // 即设置默认值
}
}
// 默认就是调用init()构造器
var myCat = Cat();
print("\(myCat.name) - \(myCat.color)");
输出结果:
开始构造
cat - black
- 默认属性值。上述代码中,是在构造器中为存储属性设置初始值,但也可以在属性声明时为直接设置默认值:
class Cat {
// 声明时,直接设置默认值
var name:String = "cat";
var color:String = "red";
}
二、自定义构造过程
可以通过传入参数和可选属性类型来自定义构造过程,也可以在构造过程中修改常量属性。
- 构造参数。自定义构造过程时,可以在定义中提供构造参数,指定所需值的类型和名字。而构造参数的功能与语法跟函数、方法的参数相同:
// 自定义构造过程
class Cat {
var name:String?;
var color:String?;
// 自定义构造器1
// 构造参数: 外部参数名fromName,内部参数名name
init(fromName name:String) {
print("初始化名字");
self.name = name;
}
// 自定义构造器2
init(fromColor color:String) {
print("初始化颜色");
self.color = color;
}
}
// 实例化
let myCat1 = Cat(fromName:"笨笨");
print(myCat1.name!);
let mycat2 = Cat(fromColor:"白色");
print(mycat2.color!);
输出结果:
初始化名字
笨笨
初始化颜色
白色
- 构造器内部参数和外部参数。与函数和方法参数相同,但不同的地方放在于,如果构造器中没有提供外部参数名,swift会自动为每个构造器自动生成一个与内部参数名相同的外部参数名:
struct CustomColor {
// 颜色的三原色,都是Double类型
let red, green, blue: Double;
// 构造器
init(red:Double, green:Double, blue:Double) {
self.red = red;
self.green = green;
self.blue = blue;
}
}
// 必须要有外部名称,这里系统会报错
//let testColor = CustomColor(1.0, 0.0, 1.0);
// 注意: 没有外部参数,系统会自动生成与内部参数名一样的
let magenta = CustomColor(red: 1.0, green: 0.0, blue: 1.0);
- 不带外部参数名的构造器。如果不想提供,某个外部参数名,那么可以使用下划线
_
来显示描述外部参数名:
struct CustomColor {
// 颜色的三原色,都是Double类型
let red, green, blue: Double;
// 使用下划线`_`来显示描述外部参数名
init(_ red:Double,_ green:Double,_ blue:Double) {
self.red = red;
self.green = green;
self.blue = blue;
}
}
// 不带外部参数名的构造器,调用时候跟简洁
let magenta = CustomColor(1.0, 0.0, 1.0);
- 构造器中修改常量属性。可以在构造过程的任意时间点修改常量属性值,但常量属性被赋值以后,之后都是不能再修改。另外,对于类的实例来说它的常量属性只能在定义它的类的构造器中修改,而不能在子类中修改:
// 基类: 汽车类
class Car {
// 速度
var currentSpeed = 0.0;
// 车型别名
let alias:String;
// 车型颜色
let color:String;
// 构造器
init(_ alias:String, _ color:String) {
self.alias = alias;
self.color = color;
}
}
var myLamborghini = Car("Lamborghini","白色");
print("\(myLamborghini.alias) - \(myLamborghini.color)");
// 兰博基尼Lamborghini
// 该类继承Car
class Lamborghini:Car {
// 以下构造器是错误的,不能在子类中修改!!!
init(_ alias:String) {
self.alias = alias;
}
}
三、默认构造器
如果结构体和类所有属性都有默认值,而且也没有自定义构造器,那么swift会自动给结构体和类创建一个默认的构造器。
- 类的默认构造器。默认构造器将简单的创建一个所有属性值都设置为默认值的实例:
class Cat {
var name:String?;
// 没有自定义构造器,系统将自动生成一个为所有属性设置默认值的默认构造器
// 而属性name,是可选的,系统将自动设置默认为`nil`
}
let myCat = Cat();
- 结构体的成员逐一构造器。如果结构体对所有存储属性提供了默认值并且自身没有提供定制的构造器,系统将自动添加一个成员逐一构造器。成员逐一构造器是初始化结构体新实例成员属性的快捷方法,通过与成员属性名相同的参数名进行传值即可完成对成员属性的初始化赋值:
struct Size {
// 注意: 是提供了`默认值` 并且 `没有提供定制的构造器`,系统将自动添加一个成员逐一构造器
var width = 0.0, height = 0.0;
}
// 参数名与成员属性名相同
let test = Size(width:30, height: 30);
四、值类型的构造器代理
构造器代理即是构造器通过调用其他构造器来完成实例的部分构造过程,这样能减少多个构造器间的代码重复。
构造器代理的实现规则与形式在值类型和类类型不同。值类型(结构体和枚举类型)是不支持继承的,它们只能代理给本身提供的其他构造器。类则不同,它可以继承自其他类(参考继承),这意味该类有必要保证其所有继承的存储类型属性在构造时也能正确初始化。
值类型,可以使用self.init
在自定义构造器中引用其他的属于相同值类型的构造器,但只能在构造器内部调用self.init
。
如果在值类型中自定义了构造器,那么将无法访问默认构造器(如果是结构体,即无法访问成员逐一构造器,参考本章节中三的内容)。
备注:如果你有iOS基础,下面其实就是设置视图大小以及位置相关的几个结构体。
// 矩形大小结构体
struct Size {
// 可以访问成员逐一构造器
var width = 0.0, height = 0.0;
}
// 矩形中心点位置
struct Point {
// 可以访问成员逐一构造器
var x = 0.0, y = 0.0;
}
// 矩形的大小以及位置
struct Rect {
// 位置
var origin = Point();
// 大小
var size = Size();
// 构造器1
init(origin:Point, size:Size) {
self.origin = origin;
self.size = size;
}
// 构造器2
init(center:Point, size:Size) {
let originX = center.x - (size.width / 2);
let originY = center.y - (size.height / 2);
let tempOrigin = Point(x:originX, y: originY);
// 使用`self.init`调用其他的构造器
self.init(origin:tempOrigin, size: size);
}
}
/**
矩形大小(100,100),位置(50,50)
其矩形的中心点为位置是(50+100/2, 50+100/2)
*/
// 位置: 调用成员逐一构造器,默认构造器
let origin = Point(x:50, y:50);
// 大小: 调用成员逐一构造器,默认构造器
let size = Size(width:100, height: 100);
// 写法一
let rect1 = Rect(origin:origin, size:size);
// 写法二
// 中心点位置
let center = Point(x: 50+100/2, y: 50+100/2);
let rect2 = Rect(center: center, size: size);
五、类的继承和构造过程
类里面的所有存储型属性,包括继承自父类的属性,都必须在构造过程中设置初始值。
swift提供了两种类型的类构造器来确保所有类实例中的存储型属性能获得初始值,分别为指定构造器和便利构造器。指定构造器是类中最主要的构造器,一个指定构造器将初始化类中提供的所有属性,并根据父类链往上调用父类的构造器来实现父类的初始化。另外每个类都必须拥有至少一个指定构造器,大多数情况下,类都通过继承了父类中的指定构造器而满足了这个条件。便利构造器是类中比较次要的、辅助型的构造器。可以定义便利构造器来调用同一类中的指定构造器,并为其参数提供默认值,也可以定义便利构造器来创建一个特殊用途或特定输入的实例。
- 指定构造器语法,其与值类型构造器一样:
init(parameters) {
statements
}
- 便利构造器语法,不同点在于要加上一个
convenience
关键字:
convenience init(parameters) {
statements
}
-
类的构造器代理规则:
1、指定构造器必须调用其直接父类的指定构造器;
2、便利构造器必须调用同一类中定义的其他构造器;
3、便利构造器必须最终以调用一个指定构造器结束;
【指定构造器必须总是向上代理;便利构造器总是横向代理(即类中某个指定构造器)】
两段式构造过程。在swift中类的构造过程包括两个阶段,第一个阶段,每个存储类型属性通过引入它们的类构造器设置初始值。当每个存储属性值确定后,第二阶段开始,它给每个类一个机会在新实例准备使用之前,再进一步定制它的存储类型属性。
两段式构造器过程的使用栏构造过程更为安全,同时在整个类层次中也更为灵活。两段式构造过程可以防止属性值在初始化之前被访问,也可以防止属性被另外一个构造器意外赋予不同的值。
swift编译器中对于两段式构造过程进行了4中有效的安全检查:
1、指定构造器必须保证它所在类引入的所有属性都必须初始化完成,之后才能调用父类的构造器完成其他构造任务。例如,一个对象的内存只有在所有存储类型属性确定之后才能完全初始化,为了满足这个规则,指定构造器必须保证所在类引入的属性调用父类构造器之前先完成初始化;
2、指定构造器必须调用父类构造器,然后为继承的属性设置新值。如果没有这样做,那么指定构造器赋予的新值将会被父类中的构造器所覆盖;
3、便利构造器必须向调用本类中的其他指定构造器,然后再为任意属性赋新的值。如果这样的操作,那么遍历构造器赋予的新值将被本类中其他指定构造器所覆盖;
4、构造器在第一阶段构造完成之前,不能调用任何实例方法,也不能读取任何实例属性的值,且self
的值不能被引用;-
两段式构造过程的安全检查,阶段一:
1、某个指定构造器或便利构造器被调用;
2、完成新实例内存的分配,但此时内存还没有被初始化;
3、指定构造器确保其所在类引入的所有存储属性都已经被赋初始值,此时存储类型属性所属的内存完成初始化;
4、指定构造器将调用父类的构造器,完成父类属性的初始化;
5、调用父类构造器的过程将会沿着构造器链一直往上执行,直到到达构造器链的最顶部;
6、当到达了构造器最顶部,且确保所有实例包含的存储类型属性都已经赋值,那么实例的存储才算是完全初始化。此时阶段一完成; -
两段式构造过程的安全检查,阶段二:
1、从顶部构造器链一直往下,每个构造器链中类的指定构造器都有机会进一步定制实例,即存储属性都已经被初始化过,现在可以进行修改操作。构造器此时可以访问self
、修改属性并调用实例方法等,因为此时实例已经完全被初始化了;
2、最终,任意构造器链中的便利构造器有机会定制实例和使用self
;
备注: 两段式构造过程以及安全检查只做了解即可,了解系统的实例化过程。
- 构造器的继承和重写。与OC的子类不同,swift的子类不会默认继承父类的构造器,这种机制可以防止一个父类的简单构造器被更专业的子类继承,并错误的用来创建子类的实例。当重写指定构造器时,需要加上
override
修饰符,甚至是重写便利构造器也需要添加该修饰符:
// 类: 人
class Person {
var name:String?
var description:String {
return "name:\(name!)";
}
// 指定构造器
init () {
// 初始化操作
name = "";
}
}
// 类: 学生,继承Person
class Student:Person {
// 学号
var sid:Int?;
// 重写属性
override var description: String {
return "格式:" + super.description + " sid:\(sid!)"
}
// 重写指定构造器,需要`override`修饰
override init() {
// 先调用父类的指定构造器,确保`name`属性已经被初始化
super.init();
// 再修改属性
name = "StudentName";
sid = 100_000_000_001;
}
}
// 实例化学生类
var zhansan = Student();
print(zhansan.description);
注意: 子类可以在初始化时修修改继承来的变量属性,但不能修改该继承过来的常量属性。
- 自动构造器的继承。当满足特定条件,父类构造器是可以被自动继承的:
1、如果子类没有定义任何构造器,该子类将自动继承所有父类的指定构造器;
2、如果子类提供了所有父类指定构造器的实现,不管是通过上述规则1继承过来的,还是通过自定义实现的,该类将自动继承所有父类的便利构造器;
六、可失败构造器
当一个类、结构体或枚举类型的对象,在构造自身的过程中有可能失败,那么为其定义一个可失败构造器,是非常有用的。所谓的"失败"是指,如给构造器传入无效的参数值,或缺少某种所需要的外部资源,又或者不满足某种必要的条件等。你可以添加一个或多个可失败构造器,其语法是init?
,另外通过return nil
语句,来表名可失败构造器在某种情况下是"失败"。
可失败构造器的参数名和类型,不能与其非失败构造器的参数名以及类型相同。
严格来说,构造器都不支持返回值。因为构造器本身的作用,只是为了确保对象自身被正确的构造器,所以即使在表明可失败构造器,失败的这种情况下,用到了return nil
。
class Foot {
var name:String
// 可失败构造器
init?(name:String) {
if name.isEmpty {
// 表示"失败"
return nil;
}
self.name = name;
}
}
// 实例化Foot对象,并检查构造是否成功
// foot1类型是Foot?,而不是Foot
let foot1 = Foot(name:"bread");
if let temp = foot1 {
print("foot1构造成功:\(foot1!.name)");
}
// 传入一个空值,构造失败
let foot2 = Foot(name:"");
if let temp = foot2 {
print("foot2构造成功:\(foot2!.name)");
} else {
print("foot2构造失败");
}
输出结果:
foot1构造成功:bread
foot2构造失败
- 枚举类型的可失败构造器。通过构造一个带一个或多个参数的可失败构造器来获取枚举类型中特定的枚举成员,当不满足枚举成员期望条件时,构造失败:
// 加减乘除枚举
enum ArithmeticSign {
// 对应加减乘除
case Add, Subtract, Multiply, Divide;
// 可失败构造器
init?(sign:String) {
switch sign {
case "+":
self = .Add;
case "-":
self = .Subtract;
case "*":
self = .Multiply;
case "/":
self = .Divide;
default: // 否则即是"失败"
return nil;
}
}
}
// 枚举1
let sign1 = ArithmeticSign(sign: "+");
if sign1 != nil {
print(sign1!);
}
// 枚举2
let sign2 = ArithmeticSign(sign: "~");
if sign2 == nil {
print("sign2构造失败");
}
输出结果:
Add
sign2构造失败
- 带原始值的枚举类型可失败构造器。带原始值的枚举类型会自带一个可失败构造器
init?(rawValue:)
,该可失败构造器有一个名字为rawValue
的默认参数,其类型和枚举类型的原始值类型一致,如果该参数的值能够和枚举类型成员所带的原始值匹配,则该构造器构造一个带此原始值的枚举成员,否则构造失败:
// 加减乘除枚举
enum ArithmeticSign:Character {
// 对应加减乘除
case Add="+", Subtract = "-", Multiply = "*", Divide = "/";
}
// 枚举1
let sign1 = ArithmeticSign(rawValue: "+");
if sign1 != nil {
print(sign1!);
}
// 枚举2
let sign2 = ArithmeticSign(rawValue: "~");
if sign2 == nil {
print("sign2构造失败");
}
- 构造失败的传递。可失败构造器允许在同一类、结构体、枚举中横向调用其他的可失败构造器,类似的子类的可失败构造器也能向上调用父类的可失败构造器。无论向上还是横向调用,如果调用的可失败构造器,在构建过程中触发了构造失败的行为,整个构造过程都将被立即终止,接下来的任何构造代码都不会被执行。而可失败构造器也可以调用其他的非失败构造器,通过此方法,可以为已有的构造过程加入构造失败的条件:
// 食物类
class Foot {
// 注意是常量
let name:String;
// 可失败构造器
init?(name:String) {
self.name = name;
if name.isEmpty {
// 表示"失败"
return nil;
}
}
}
// 面包类
class Bread:Foot {
let quantity:Int;
// 可失败构造器
init?(name:String, quantity:Int) {
// 赋值
self.quantity = quantity;
// 调用父类的构造方法
super.init(name: name);
if quantity < 1 {
// "失败"
return nil;
}
}
}
// 实例1
if let bread1 = Bread(name:"麦香面包", quantity: 1) {
print("\(bread1.name):\(bread1.quantity)");
}
// 实例2
if let bread2 = Bread(name:"麦香面包", quantity: 0) {
print("\(bread2.name):\(bread2.quantity)");
} else { // Bread触发构建失败行为
print("bread2构建失败");
}
// 实例3
if let bread3 = Bread(name:"", quantity: 1) {
print("\(bread3.name):\(bread3.quantity)");
} else { // Foot触发构建失败行为,但整个Bread同样还是失败的
print("bread3构建失败");
}
输出结果:
麦香面包:1
bread2构建失败
bread3构建失败
- 重写一个可失败构造器。可以用子类的非可失败构造器重写一个积累的可失败构造器。好处是,即使基类的构造器为可失败的,但当子类的构造器在构造过程不可能失败时,我们也可以把它修改过来。但是当子类的非可失败构造器重写了一个父类的可失败构造器时,子类的构造器将不能再向上调用父类的可失败构造器,另外一个非失败的构造器永远不能代理调用一个可失败构造器。最后,可以用非可失败构造器重写一个可失败构造器,但反过来却行不通的:
class Foot {
// 注意是变量
var name:String?;
// 指定构造器,构建name为nil
init () {}
// 可失败构造器,构建name为非空字符串
init?(name:String) {
self.name = name;
if name.isEmpty {
// 表示"失败"
return nil;
}
}
}
// 面包类
class Bread:Foot {
// 重写指定构造器
override init() {
// 调用父类的指定构造器
super.init();
// 再赋值
name = "[untitled]";
}
// 重写一个可失败构造器
override init(name: String) {
// 调用父类的指定构造器,即先做初始化相关操作
super.init();
if name.isEmpty {
self.name = "[untitled]";
} else {
self.name = name;
}
}
}
let bread1 = Bread(name:"");
print(bread1.name!);
let bread2 = Bread(name:"奶油面包");
print(bread2.name!);
七、必要构造器
在类的构造器前添加required
修饰符表名所有该类的子类都必须实现该构造器:
// 基类,书写格式
class SomeClass {
required init() {
// 在这里添加该必要构造器的实现代码
}
}
// 子类,书写格式
class SomeSubCalss: SomeClass {
// 子类重写父类的必要构造器时,必须添加`required`,为了保证继承链上子类的构造器也是必要的构造器。另外这里不需要添加`override`修饰符
required init() {
// 在这里添加子类必要构造器的实现代码
}
}
注意: 如果子类继承的构造器能满足必要构造器的需求,则不需要再子类中提供必要构造器的实现。
八、通过闭包和函数设置属性的默认值
如果某个存储类型属性的默认值需要特定的定制操作,那么可以使用闭包或全部函数来为该属性提供定制的默认值。每当某个属性所属的新类型创建时,对应的闭包或函数就会被调用,而它们的返回值会当做默认值赋值给这个属性。
闭包或函数一般会创建与属性类型相同的临时变量,然后修改它的值以满足预期初始状态,最后将这个临时变量的值作为属性的默认值返回即可:
// 格式
class SomeClass {
let someProperty:SomeType = {
// 在这里闭包中给someProperty创建一个默认值
// someValue必须和SomeType类型相同
return someValue;
}(); // 这里后面跟着括号,表示需要立即执行此闭包操作
}
// 猫类
class Cat {
// 猫的颜色,通过闭包产生随机颜色
var color:UIColor = {
let randomRed = Float(arc4random_uniform(255)) / 255.0;
let randomGreed = Float(arc4random_uniform(255)) / 255.0;
let randomBlue = Float(arc4random_uniform(255)) / 255.0;
let tempColor = UIColor(colorLiteralRed: randomRed, green:randomGreed, blue:randomBlue, alpha:1.0);
return tempColor;
}();
}
// 实例化
let myCat = Cat();
// 颜色是随机颜色
print(myCat.color);
注意: 如果你使用闭包来初始化属性的值,但在闭包执行时,实例的其他部分都还没有初始化,即意味此时还不能够在闭包里访问其他的属性,就算这个属性有默认值也不允许,另外也不能使用隐式的
self
属性,或调用其他的实例方法。