本文为私人学习笔记,仅仅做为记录使用,详情内容请查阅 中文官方文档。
属性
- 属性包装器
属性包装器在管理如何存储和定义属性的代码之前添加了一个分隔层。举个例子,如果你想要对属性进行线程安全地存取,那你势必要在所有存取的地方编写相同的代码进行线程安全管理,是不是非常的麻烦?属性包装器则帮你实现一次编写,终身复用的效果。
定义一个属性包装器,你需要创建一个具有 warppedValue
属性的结构体、枚举或者类。这个 warppedValue
属性就是包装器需要的包装属性。
定义一个 TwelveOrLess
结构体,确保包装器所修饰的属性存储的值都是小于或等于 12 的数字。
@propertyWrapper
struct TwelveOrLess {
private var number: Int
init() { self.number = 0 }
var wrappedValue: Int {
get { return number }
set { number = min(newValue, 12) }
}
}
你现在可以在属性前面写上包装器,将包装器作为特性的方式来使用。
struct SmallRectangle {
@TwelveOrLess var height: Int
@TwelveOrLess var width: Int
}
var rectangle = SmallRectangle()
print(rectangle.height)
// 打印 "0"
rectangle.height = 10
print(rectangle.height)
// 打印 "10"
rectangle.height = 24
print(rectangle.height)
// 打印 "12"
上述过程中,当你操作 rectangle 的属性进行读写时,都会经过包装器 wrappedValue
属性的读写。编译器将会合成包装器所提供的存储空间和访问属性的代码。过程就像下面这样。
struct SmallRectangle {
private var _height = TwelveOrLess()
private var _width = TwelveOrLess()
var height: Int {
get { return _height.wrappedValue }
set { _height.wrappedValue = newValue }
}
var width: Int {
get { return _width.wrappedValue }
set { _width.wrappedValue = newValue }
}
}
- 类型属性
类型属性存储的数据为共享数据。存储型类型属性可以是常量也可以是变量,但是计算型类型属性和实例的计算型属性一样,只能定义成变量属性。
需要注意的是,由于类型本身并没有构造器,也就无法在初始化过程中为类型属性赋值,因此必须给存储型类型属性指定默认值。存储型类型属性是延迟初始化的,它们只有在第一次被访问时才被初始化。即使它们被多个线程同时访问,系统也保证智慧对其进行一次初始化,并且不需要对其使用 lazy
修饰符。这种特性也保证了对内存的优化。
方法
结构体和枚举能够定义方法是 Swift 和 OC 的主要区别之一。
- mutating 可变方法
和类不同,值类型的结构体和枚举默认情况下,其属性不能够修改,但是你确实需要修改时,可以为方法添加 可变 mutating
行为。
struct Point {
var x = 0.0, y = 0.0
mutating func moveBy(x deltaX: Double, y deltaY: Double) {
x += deltaX
y += deltaY
}
}
- 类型方法
和 OC 一样,类型方法属于类本身。有两种方式定义类型方法,func
前添加 static
或 class
,后者允许子类重写。类型方法位于方法体 {}
中的最前面,作用域属于该类的范围。
- self 属性
在实例方法中,self
属性指向实例本身,你可以使用 self
来引用当前实例,当然 Swift
并不推荐使用它,self
的使用场景主要是在实例方法的某个参数名和实例的属性名重复时,用 self
区分参数还是属性,否则,参数优先,两个都被认为是参数名。
struct Point {
var x = 0.0, y = 0.0
func isToTheRightOf(x: Double) -> Bool {
return self.x > x
}
}
在类型方法中,self
属性指向类本身,而不是该类型的某个实例。一般来说,在类型方法的方法体中,可以直接通过类型方法的名称调用本类中的其他类型方法,而不需要在方法名称前面加上类型名,类似的,类型属性也同样可以通过名称直接访问其他类型属性,而不需要前面加上本类的类名。
下标
快捷访问集合、列表或序列中的元素而不需要使用存取方法,可以定义在类、结构体和枚举中。下标不限于一维。
- subscript
subscript(index: Int) -> Int {
get {
// 返回一个适当的 Int 类型的值
}
set(newValue) {
// 执行适当的赋值操作
}
}
示例
struct Collection {
let data : [Any]
subscript(index: Int) -> Any? {
get {
if index < data.count {
return data[index]
} else {
return nil
}
}
}
}
let cc = Collection(data : ["0","1","2","3"])
print(cc[1] ?? "无")
- 类型下标
类似类型方法,通过 static
或 class
来定义类型下标,后者可以重写。
enum Planet: Int {
case mercury = 1, venus, earth, mars, jupiter, saturn, uranus, neptune
static subscript(n: Int) -> Planet {
return Planet(rawValue: n)!
}
}
let mars = Planet[4]
print(mars)
继承
继承和引用特性是区分类和结构体、枚举的基本特征。Swift 中的类不需要像 OC 中从一个通用的 NSObject
基类继承而来。不继承其他类的类,称之为基类。
- 重写属性的 Getters 和 Setters
重写的属性的限制不变小,即:可以将一个继承来的只读属性重写为一个读写属性,也可以将读写属性重写为一个只读属性。另外,如果你在重写属性中提供来了setter
,那么你一定要提供对应的 getter
,反之没有限制。
- 属性观察器
可以通过重写属性为一个继承来的属性添加属性观察器。这样一来,无论被继承属性原本是如何实现的,当其属性值发生改变时,你就会被通知到。
无论是存储类型还是计算类型的属性都可以添加属性观察器,但是依旧有限制,那些常量存储类型或者只读计算类型属性无法添加,因为这些值是不可以被改变的,为它们设置 willSet
和 didSet
是不太恰当的操作,另外,属性观察器和 setter
是不能同时存在的,因为 setter
本身就已经可以观察到属性的变化了。
- final
final
关键字来限制被重写、继承,这意味着被修饰的属性、方法或者类都是最终类型。例如:final var
、final func
、final class func
以及 final subscript
。
构造过程
设置实例存储属性的初始值和执行其他必须的设置或构造过程。
你可以在构造器中为存储类属性设置初始值,或者在定义属性时分配默认值。(这两种方式是直接设置的,不会触发任何属性观察者)
// 构造器中设置初始值
struct Fahrenheit {
var temperature: Double
init() {
temperature = 32.0
}
}
// 默认属性值
struct Fahrenheit {
var temperature = 32.0
}
- 构造过程中常量属性赋值
你可以在构造过程中的任意时间点给常量属性赋值,只要在构造过程结束时特设置成确定的值。一旦常量属性被赋值,它将永远不可更改。
class SurveyQuestion {
let text: String
init(text: String) {
self.text = text // 常量属性只能在定义它的类的构成过程中修改,不能在其子类中修改
}
}
- 默认构造器
如果结构体或类为所有属性提供类默认值,又没有提供任何自定义的构造器,那么 Swift 会给这些结构体或类提供一个默认构造器。这个默认构造器将简单地创建一个所有属性值都设置为它们默认值的实例。
- 结构体的逐一成员构造器
结构体如果没有定义任何自定义构造器,它们将自动获得一个逐一成员构造器(memberwise initializer)。不像默认构造器,即使存储型属性没有默认值,结构体也能获得逐一成员构造器。
struct Size {
var width = 0.0, height = 0.0
}
let twoByTwo = Size(width: 2.0, height: 2.0)
- 值类型的构造器代理
构造器可以通过调用其他构造器来完成实例的部分构造过程,这一过程称为构造器代理,它能避免过个构造器间的代码重复。
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(origin: Point, size: Size) {
self.origin = origin
self.size = size
}
}
如果你为某个值类型定了一个自定义的构造器,你将无法访问到默认构造器(如果是结构体,还将无法反问逐一成员构造器)。这种限制避免了在一个更复杂的构造器中做了额外的重要设置,但是使用的人不小心使用自动生成的构造器而导致错误的情况。
如果你仍然希望默认构造器、逐一成员构造器以及自定义构造器都能用来创建实例,那可以将自定义的构造器写到扩展(extension)中,而不是写在值类型的原始定义中。
类的继承和构造过程
类中的所有存储属性(包括所有继承自父类的属性)都必须在构造过程中设置初始值。
Swift 为类提供类两种构造器来确保实例中所有存储属性都能获得初始值:指定构造器和便利构造器。
每一个类都必须至少拥有一个指定的构造器。便利构造器是类中比较次要的、辅助性的构造器。你可以定义便利构造器来调用同一个类中的指定构造器,并为部分形参提供默认值。你也可以定义便利构造器来创建一个特殊用途或特定输入值的实例。
// 指定构造器
init(parameters) {
statements
}
// 便利构造器
convenience init(parameters) {
statements
}
- 类构造器代理
Swfit 构造器之间的代理调用遵循三条规则:
- 指定构造器必须调用其直接父类的指定构造器
- 便利构造器必须调用同类中定义的其他构造器
- 便利构造器最后必须调用指定构造器
即:指定构造器必须总是向上(父类)代理,便利构造器必须总是横向(同类)代理。
- 构造器的继承和重写
指定构造器
当你编写一个和父类中指定构造器相匹配的子类构造器时,你实际上是在重写父类的这个指定构造器。因此,你必须在定义子类构造器时带上 override
修饰符,该修饰符会让编译器去检查父类中是否有相匹配的指定构造器,并验证构造器参数是否被按照预想中被指定。
便利构造器
如果你编写了一个和父类便利构造器相匹配的子类构造器,由于子类不能直接调用父类的便利构造器(构造器代理规则中指出便利构造器需要横向调用其他构造器),因此,严格意义上来讲,你的子类并未对一个父类构造器提供重写,因此你“重写”一个父类便利构造器时,不需要添加 override
修饰符。
子类可以在构造过程中修改继承来的变量属性,但是不能修改继承来的常量属性。
构造器的自动继承
子类在默认情况下不会继承父类的构造器。但是如果满足一定的条件,父类构造器是可以被自动继承的,这意味着在许多常见场景你不必重写父类的构造器,并且可以在安全的情况下以最小的代价继承父类的构造器。
假如你为子类中引入的所有新属性都提供了默认值,下面两个规则将适用:
- 如果子类没有定义任何指定构造器,它将自动继承父类所有的指定构造器
- 如果子类提供了所有父类指定构造器的实现,它将自动继承父类所有的便利构造器
即使你在子类中添加了更多的便利构造器,这两条规则仍然适用。
实例:
class Food {
var name: String
init(name: String) { // 指定构造器
self.name = name
}
convenience init() { // 便利构造器,需要横向代理其他构造器
self.init(name: "[Unnamed]")
}
}
下图展示了 Food
的构造器链:
class RecipeIngredient: Food {
var quantity: Int
init(name: String, quantity: Int) {
self.quantity = quantity
super.init(name: name)
}
override convenience init(name: String) {
self.init(name: name, quantity: 1)
}
}
下图展示了 RecipeIngredient
类的构造器链:
RecipeIngredient
类拥有一个新的指定构造器 init(name: String, quantity: Int)
用来设置新引入的属性,在该构造器中,向上代理到父类的指定构造器。
RecipeIngredient
也定义类一个便利构造器,init(name: String)
,它横向代理到本类中的指定构造器,用来方便地创建 quantity
为 1 的实例。另外,该便利构造器使用了跟 Food
中指定构造器 init(name: String)
相同的形参,因此这个便利构造器重写了父类的指定构造器,前面需要使用修饰符 override
。
尽管 RecipeIngredient
将父类的指定构造器重写为了便利构造器,但是它依然提供了父类所有的指定构造器的实现,因此,RecipeIngredient
会自动继承父类的所有便利构造器。在 Food
中,它的一个便利构造器是 init()
,这个会给 RecipeIngredient
继承。它会代理到 RecipeIngredient
重写的 init(name: String)
中,接着代理到其他构造器中。
class ShoppingListItem: RecipeIngredient {
var purchased = false
var description: String {
var output = "\(quantity) x \(name)"
output += purchased ? " ✔" : " ✘"
return output
}
}
ShoppingListItem
为自己引入的所有属性提供了默认值,并且没有定义自己的任何构造器,因此它将自动继承所有父类中指定构造器和便利构造器。你可以使用其父类中的所有构造器来创建实例。
下图展示三个类的构造器链:
- 可失败的构造器
所谓的失败指的是给构造器传入类无效的形参,或者缺少某种所必须的外部资源而导致构造失败的情况。
语法为在 init
关键字后面添加问号(init?
)。
可失败构造器会创建一个类型为自身类型的可选类型的对象。你通过 return nil
语句来表明可失败构造器在何种情况下应该失败。
严格来讲,Swift 的构造器都不支持返回值,因为构造器本身的作用,只是为了确保对象能被正确的构造。因此你只需要用 return nil
表明可失败构造器构造失败,而不要使用关键字 return
来表明构造成功。
带原始值的枚举类型的可失败构造器
此种枚举类型会自带一个可失败构造器 init?(rawValue:)
,该可失败构造器有一个合适的原始值类型的参数 rawValue
形参,找到匹配的原始值则构造成功,否则构造失败。
enum TemperatureUnit: Character {
case Kelvin = "K", Celsius = "C", Fahrenheit = "F"
}
// 构造成功
let fahrenheitUnit = TemperatureUnit(rawValue: "F")
// 构造失败
let unknownUnit = TemperatureUnit(rawValue: "X")
枚举类型的可失败构造器
你可以通过一个或者多个形参的可失败构造器来获取枚举类型中特定的枚举成员。
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
}
}
}
// 可以正确获取到枚举值
let fahrenheitUnit = TemperatureUnit(symbol: "F")
// 将无法匹配,构造失败
let unknownUnit = TemperatureUnit(symbol: "X")
构成失败的传递
类、结构体、枚举的可失败构造器可以横向代理到它们自己的其他可失败构造器。类似的,子类的可失败构造器也能向上代理到父类的可失败构造器。
class Product {
let name: String
init?(name: String) {
if name.isEmpty { return nil }
self.name = name
}
}
class CartItem: Product {
let quantity: Int
init?(name: String, quantity: Int) {
if quantity < 1 { return nil }
self.quantity = quantity
super.init(name: name)
}
}
Product
类的构造条件是 name
不为空,CartItem
类首先要保证 quantity
的常量存储类型的属性的值至少为 1,然后向上代理到父类,同样要保证 name
的值不能为空,两个条件不满足任意一个都会导致构造失败。
重写可失败构造器
你可以在子类中可以重写父类的可失败构造器。当你重写父类可失败构造器为非可失败构造器时,需要对父类的可失败构造器进行强制解包。
你可以将可失败的构造器重写为非可失败的构造器,反过来则不可以
class Document {
var name: String?
// 该构造器创建了一个 name 属性的值为 nil 的 document 实例
init() {}
// 该构造器创建了一个 name 属性的值为非空字符串的 document 实例
init?(name: String) {
if name.isEmpty { return nil }
self.name = name
}
}
子类将可失败构造器重写为非可失败构造器:
class AutomaticallyNamedDocument: Document {
override init() {
super.init()
self.name = "[Untitled]"
}
override init(name: String) {
super.init()
if name.isEmpty {
self.name = "[Untitled]"
} else {
self.name = name
}
}
}
class UntitledDocument: Document {
override init() {
super.init(name: "[Untitled]")! // 提供默认值,并强制解包
}
}
必要构造器
在类的构造器前添加 required
修饰符表明所有该类的子类都必须实现该构造器:
class SomeClass {
required init() {
// 构造器的实现代码
}
}
在子类重写父类的必要构造器时,必须在子类的构造器前也添加 required
修饰符,表明该构造器要求也应用于继承链后面的子类。在重写父类中必要的指定构造器时,不需要添加 override
修饰符:
class SomeSubclass: SomeClass {
required init() {
// 构造器的实现代码
}
}
如果子类继承的构造器能满足必要构造器的要求,则无须在子类中显式提供必要构造器的实现。
通过闭包或函数设置属性的默认值
如果某个 存储型 属性的默认值需要一些自定义或者设置,你可以使用闭包或者全局函数为其提供定制的默认值。每当某个属性所在类型的新实例被构造时,那么对应的闭包或者函数会被调用,而它们返回值会当作默认值赋值给这个属性。
class SomeClass {
let someProperty: SomeType = {
// 在这个闭包中给 someProperty 创建一个默认值
// someValue 必须和 SomeType 类型相同
return someValue
}()
}
当 SomeClass
类实例化时, 属性 someProperty
后的闭包会被调用,注意花括号后面的一对小括号,这使用告诉 Swift 立即执行此闭包,如果忽略了这对括号,相当于将闭包本身作为值赋值给了属性,而不是将闭包的返回值赋值给属性。
如果你使用闭包来初始化属性,请记住在闭包执行时,实例的其他部分都还没有初始化。这意味着你不能在闭包里访问其他属性,即使这些属性有默认值。同样,你也不能使用隐式的 self
属性,或者调用任何实例方法。
如果你依旧想使用 self
属性,或者调用其他实例方法,你可以使用懒加载的形式,通过关键字 lazy
来创建。之所以可以使用其他属性或者实例方法,是因为该属性已经是在类实例化之后才被调用,实例中的属性都已经被初始化,所以可以使用。
lazy var someProperty: SomeType = {
// 在这个闭包中给 someProperty 创建一个默认值
// someValue 必须和 SomeType 类型相同
// 可以使用 self 等实例属性或方法
return someValue
}()
析构过程
析构器只适用于类,当一个类的实例被释放之前,析构器会被立即调用。析构器用关键字 deinit
来标记。
deinit {
// 执行析构过程
}
Swift 会自动释放不再需要的实例以释放资源。通常不需要你手动去清理,但是,当你需要进行额外的清理时,你则可能需要使用到析构器。
析构器是被自动调用的,你不能主动调用析构器。子类继承了父类的析构器,并且在子类析构器实现的最后,父类的析构器依然会被自动调用。即使子类没有提供自己的析构器,父类的析构器也同样会被调用。