【Swift】基础(7)—— 构造过程与析构过程

740 阅读8分钟

构造过程

构造过程是为了使用某个类、结构体或枚举类型的实例而进行的准备过程。这个过程包含了为实例中的每个属性设置初始值和为其执行必要的准备和初始化任务。

与OC不同,Swift的构造器无需返回值,它们的主要任务是保证新实例在第一次使用前完成正确的初始化。

类实例也可以通过定义析构器(deinitializer)在类实例释放之前执行清理内存的工作。

存储属性的初始化

  • 类和结构体在实例创建时,必须为所有存储型属性设置合适的初始值。
  • 存储属性在构造器中赋值时,它们的值是被直接设置的,不会触发任何属性观测器。

当属性在整个程序中单独使用相同的值时,可以单独在声明部分声明它,而不用在init()中初始化它。

使用默认值能让你的构造器更简洁、更清晰,且能通过默认值自动推导出属性的类型。

如:

struct rectangle {
   var length = 6
   var breadth = 12
}

默认构造器

对于值类型和引用类型,默认构造器是不同的。

如果(class)给所有的存储属性赋了默认值,且没有实现任何自定义的构造器,那么 Swift 会提供一个默认的构造器,所有属性设置默认值,如:

class ShoppingListItem {
    var name: String?
    var quantity = 1
    var purchased = false
}
var item = ShoppingListItem()

如果结构体(struct)没有实现任何自定义构造器,不管它有没有给存储属性赋默认值, 都会提供默认构造器,如:

struct Size {
    //var width, height: Double 也会提供默认构造器
    var width = 0.0, height = 0.0
}

let twoByTwo = Size(width: 2.0, height: 2.0)

当给存储属性分配默认值或者通过构造器设置初始值的时,属性的值被直接设置,不会触发属性观察

不设置外部名称参数

如果你在定义构造器时没有提供参数的外部名字,Swift 会为每个构造器的参数自动生成一个跟内部名字相同的外部名,调用时需要加上参数名。

如果不希望为构造器的某个参数提供外部名字,可以使用下划线_来显示描述它的外部名,这样就可以直接传入参数值进行调用了,例:

struct Rectangle {
    var length: Double
    
    init(frombreadth breadth: Double) {
        length = breadth * 10
    }
    
    init(frombre bre: Double) {
        length = bre * 30
    }
    //不提供外部名字
    init(_ area: Double) {
        length = area
    }
}

// 调用不提供外部名字
let rectarea = Rectangle(180.0)

// 调用不提供外部名字
let rearea = Rectangle(frombreadth: 370.0)

可选属性类型

当某个实例的存储属性没有返回任何值时,该属性被声明为optional类型,表明该特定类型可以返回nil

当存储属性声明为optional时,它会在初始化过程中自动将值初始化为nil,如:

struct Rectangle {
   var length: Double?
}

初始化时修改常量属性

只要在构造过程结束前常量的值能确定,可以在构造过程中的任意时间点修改常量属性的值。

对某个类实例来说,它的常量属性只能在定义它的类的构造过程中修改;不能在子类中修改。

尽管 length 属性现在是常量,我们仍然可以在其类的构造器中设置它的值:

struct Rectangle {
    let length: Double?

    init(frombreadth breadth: Double) {
        length = breadth * 10
    }
    
    init(frombre bre: Double) {
        length = bre * 30
    }
    
    init(_ area: Double) {
        length = area
    }
}

值类型的构造器代理

对于值类型,可以在自定义的构造器中使用self.init引用相同类型中的其它构造器。

如果为某个值类型定义了一个自定义的构造器,就无法访问到默认构造器(主要是为了避免在一个构造器中做了一些重要设置,但有人不小心使用自动生成的构造器而导致错误的情况。)。

如果想让默认构造器、自定义的构造器都可以使用的话,你可以将自定义的构造器放在扩展中,如:

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()
    
    /**
    // 由于添加了自定义构造器,默认构造器失效,需要自己写一遍默认构造器
    init(center: Point, size: Size) {
        let originX = center.x - (size.width / 2)
        let originY = center.y - (size.height / 2)
        self.init(origin: Point(x: originX, y: originY), size: size)
    }
    init(origin: Point, size: Size) {
        self.origin = origin
        self.size = size
    }
    **/
}

// 可以使用扩展的方式添加自定义构造器
extension Rect {
    init(center: Point, size: Size) {
        let originX = center.x - (size.width / 2)
        let originY = center.y - (size.height / 2)
        self.init(origin: Point(x: originX, y: originY), size: size)
    }
}

类的构造器

Swift的类有两种构造器,分别为指定构造器便利构造器

指定构造器

指定构造器的定义语法如下:

init(parameters) {
    // 初始化过程
}

便利构造器

便利构造器使用convenience关键字,语法如下:

convenience init(parameters) {
    // 初始化过程
}

规则

  • 指定构造器必须调用其直接父类的的指定构造器。
  • 便利构造器必须调用同类中定义的其它构造器。
  • 便利构造器最后必须调用指定构造器。

即:

指定构造器必须总是向上调用(去父类)。

便利构造器必须总是横向调用(在本类)。

类的构造过程

Swift通过4步安全检查来确定构造器的成功执行:

1、指定构造器必须在完成本类所有存储属性赋值之后,才能调用父类的构造器。

2、指定构造器必须在为继承的属性设置新值之前调用父类构造器。

3、便利构造器必须先调用其他构造器,再为任意属性(包括所有同类中定义的)赋新值。

4、构造器在第一阶段构造完成之前,不能调用任何实例方法,不能读取任何实例属性的值,不能引用 self 作为一个值。

例如:

class Animal {
    var head = 1
}

class Dog: Animal {
    var foot: Int
    
    // 指定构造器
    override init() {
        foot = 4
        // 指定构造器必须在完成本类所有存储属性赋值之后,才能调用父类的构造器
        super.init()
        // 指定构造器必须在为继承的属性设置新值之前调用父类构造器
        head = 2
        // 如果上面的未完成,是不能调用run()的,因为self还没有完整的创建
        run()
    }
    
    // 便利构造器
    convenience init(foot: Int) {
        // 先调用其他构造器,如果此处不调用会编译出错
        self.init()
        // 再为任意属性(包括所有同类中定义的)赋新值
        self.foot = foot
        head = 3
    }
    
    func run() {
        //do something
    }
}

可失败构造器

如果一个类,结构体或枚举类型的对象,在构造自身的过程中有可能失败,则为其定义一个可失败构造器。

变量初始化失败可能的原因有:

  • 传入无效的参数值。
  • 缺少某种所需的外部资源。
  • 没有满足特定条件。

为了妥善处理这种构造过程中可能会失败的情况。

可以在一个类,结构体或是枚举类型的定义中,添加一个或多个可失败构造器。其语法为在init关键字后面加添问号(init?)或感叹号(init!)。

例:

struct Animal {
    let species: String
    init?(species: String) {
        // 检查传入参数是否为空字符串
        if species.isEmpty { return nil }
        self.species = species
    }
}

enum TemperatureUnit {
    // 开尔文,摄氏,华氏
    case Kelvin, Celsius, Fahrenheit
    init?(symbol: Character) {
        // 验证字符是否满足规则
        switch symbol {
            case "K":
                self = .Kelvin
            case "C":
                self = .Celsius
            case "F":
                self = .Fahrenheit
            default:
                return nil
        }
    }
}

可以用子类的可失败构造器覆盖基类的可失败构造器。 也可以用子类的非可失败构造器覆盖一个基类的可失败构造器。

可以用一个非可失败构造器覆盖一个可失败构造器,但反过来却行不通。

一个非可失败的构造器永远也不能调用一个可失败构造器。

析构过程

在一个类的实例被释放之前,析构函数被立即调用。用关键字deinit来标示析构函数,类似于初始化函数用init来标示。析构函数只适用于类(class)类型。

原理

Swift 会自动释放不再需要的实例以释放资源。

Swift 通过自动引用计数(ARC)处理实例的内存管理。

通常当你的实例被释放时不需要手动地去清理。但是,当使用自己的资源时,你可能需要进行一些额外的清理。

例如,如果创建了一个自定义的类来打开一个文件,并写入一些数据,你可能需要在类实例被释放之前关闭该文件。

语法

类最多只能有一个析构函数。析构函数不带任何参数,在写法上不带括号:

deinit {
    // 执行析构过程
}

例:

var counter = 0;  // 引用计数器
class BaseClass {
    init() {
        counter += 1;
    }
    deinit {
        counter -= 1;
    }
}

var show: BaseClass? = BaseClass()
print(counter) // 1
// show = nil 语句执行时,会调用deinit
show = nil
print(counter) // 0

参考文章

Swift 构造过程

Swift 析构过程

深入了解Swift中的初始化(Initialization)