该系列主要是记录Swift中与OC差异较大,较容易忘记的知识点。
该篇主要是关于类层面的知识点。(比如类,属性,协议等)
1. 类和结构体
为什么要把它们放在一起讲?因为在Swift中,类和结构体的关系要比在其他语言中更加的密切。
Swift 中类和结构体有很多共同点。共同处在于:
- 定义属性用于存储值
- 定义方法用于提供功能
- 定义附属脚本用于访问值
- 定义构造器用于生成初始化值
- 通过扩展以增加默认实现的功能
- 符合协议以对某类提供标准功能
与结构体相比,类还有如下的附加功能:
- 继承允许一个类继承另一个类的特征
- 类型转换允许在运行时检查和解释一个类实例的类型
- 解构器允许一个类实例释放任何其所被分配的资源
- 引用计数允许对一个类的多次引用
结构体
Swift 结构体是构建代码所用的一种通用且灵活的构造体。
我们可以为结构体定义属性(常量、变量)和添加方法,从而扩展结构体的功能。
与 C 和 Objective C 不同的是:
结构体不需要包含实现文件和接口。
结构体允许我们创建一个单一文件,且系统会自动生成面向其它代码的外部接口。
结构体总是通过被复制的方式在代码中传递,因此它的值是不可修改的。
// 结构体(值类型)
struct MarksStruct {
var mark: Int
// 内置逐一构造器(为每个值赋值)
init(mark: Int) {
self.mark = mark
}
}
var aStruct = MarksStruct(mark: 98)
var bStruct = aStruct // aStruct 和 bStruct 是使用相同值的结构体!
bStruct.mark = 97 //通过点语法赋值或取值
print(aStruct.mark) // 98
print(bStruct.mark) // 97
类
// 类(引用类型)
class MarksClass {
var mark: Int
// 初始化方法(自定义的)
init(mark: Int) {
self.mark = mark
}
}
var aClass = MarksClass(mark: 98)
var bClass = aClass // aClass 和 bClass 是引用类型(指向同一对象)
aClass.mark = 97 //通过点语法赋值或取值
print(aClass.mark) // 97
print(bClass.mark) // 97
// ===恒等运算符 !==不恒等运算符
// 用来判断两个常量或者变量是否引用同一个类实例
if aClass === bClass {
print("两个常量或者变量是否引用同一个类实例")
}
结构体和类的选择
结构体实例总是通过值传递,类实例总是通过引用传递。这意味两者适用不同的任务。当你在考虑一个工程项目的数据结构和功能的时候,你需要决定每个数据结构是定义成类还是结构体。
按照通用的准则,当符合一条或多条以下条件时,请考虑构建结构体:
- 该数据结构的主要目的是用来封装少量相关简单数据值。
- 有理由预计该数据结构的实例在被赋值或传递时,封装的数据将会被拷贝而不是被引用。
- 该数据结构中储存的值类型属性,也应该被拷贝,而不是被引用。
- 该数据结构不需要去继承另一个既有类型的属性或者行为。
举例来说,以下情境中适合使用结构体:
- 几何形状的大小,封装一个width属性和height属性,两者均为Double类型。
- 一定范围内的路径,封装一个start属性和length属性,两者均为Int类型。
- 三维坐标系内一点,封装x,y和z属性,三者均为Double类型。
Swift 中,许多基本类型,诸如Integer,Boolean,String,Array和Dictionary类型均以结构体的形式实现。这意味着被赋值给新的常量或变量,或者被传入函数或方法中时,它们的值会被拷贝。(Swift 在幕后只在绝对必要时才执行实际的拷贝,保证了性能的最优化)
Objective-C 中NSString,NSArray和NSDictionary类型均以类的形式实现,而并非结构体。它们在被赋值或者被传入函数或方法时,不会发生值拷贝,而是传递现有实例的引用。
2. 属性
属性将值跟特定的类、结构或枚举关联。存储属性存储常量或变量作为实例的一部分,而计算属性计算(不是存储)一个值。计算属性可以用于类、结构体和枚举,存储属性只能用于类和结构体。
存储属性和计算属性通常与特定类型的实例关联。但是,属性也可以直接作用于类型本身,这种属性称为类型属性。
另外,还可以定义属性观察器来监控属性值的变化,以此来触发一个自定义的操作。属性观察器可以添加到自己定义的存储属性上,也可以添加到从父类继承的属性上。
存储属性
简单来说,一个存储属性就是存储在特定类或结构体实例里的一个常量或变量。
struct FixedLengthRange {
// 变量存储属性和常量存储属性
var firstValue: Int
let length: Int
}
// 该区间表示整数0,1,2
// 由于结构体(struct)属于值类型。当值类型的实例被声明为常量的时候,它的所有属性也就成了常量。
// 属于引用类型的类(class)则不一样。把一个引用类型的实例赋给一个常量后,仍然可以修改该实例的变量属性。
// 例子:假如rangeOfThreeItems是let,即使是firstValue(var)也不能被修改
var rangeOfThreeItems = FixedLengthRange(firstValue: 0, length: 3)
// 该区间现在表示整数6,7,8
rangeOfThreeItems.firstValue = 6
储存属性还有一种类型(延迟存储属性),也就是我们常用的懒加载。延迟存储属性是指当第一次被调用的时候才会计算其初始值的属性。在属性声明前使用 lazy 来标示一个延迟存储属性。
需要注意的是:
必须将延迟存储属性声明成变量(使用var关键字),因为属性的值在实例构造完成之前可能无法得到。而常量属性在构造过程完成之前必须要有初始值,因此无法声明成延迟属性。
延迟存储属性一般用于:
- 延迟对象的创建。
- 当属性的值依赖于其他未知类。
class sample {
lazy var no = number() // `var` 关键字是必须的
}
class number {
var name = "Runoob Swift 教程"
}
var firstsample = sample()
print(firstsample.no.name)
计算属性
除存储属性外,类、结构体和枚举可以定义计算属性,计算属性不直接存储值,而是提供一个 getter 来获取值,一个可选的 setter 来间接设置其他属性或变量的值。
只有 getter 没有 setter 的计算属性就是只读计算属性。只读计算属性总是返回一个值,可以通过点(.)运算符访问,但不能设置新的值。
需要注意的是:
必须使用var关键字定义计算属性,包括只读计算属性,因为它们的值不是固定的。let关键字只用来声明常量属性,表示初始化后再也无法修改的值。
class sample {
var no1 = 0.0, no2 = 0.0
var length = 300.0, breadth = 150.0
// 计算属性(通过外界的值来计算)
var middle: (Double, Double) {
get{
return (length / 2, breadth / 2)
}
set(axis){
no1 = axis.0 - (length / 2)
no2 = axis.1 - (breadth / 2)
}
}
// 只读计算属性(只是简单举个例子)
var metaInfo: [String:String] {
return [
"no1": "\(self.no1)",
"no2":"\(self.no2)"
]
}
}
var result = sample()
print(result.middle)
// 计算属性,可设置
result.middle = (0.0, 10.0)
// 只读计算属性,只需要获取不能设置
result.metaInfo
print(result.no1)
print(result.no2)
属性观察器
属性观察器监控和响应属性值的变化,每次属性被设置值的时候都会调用属性观察器。可以为除了延迟存储属性之外的其他存储属性添加属性观察器,也可以通过重载属性的方式为继承的属性(包括存储属性和计算属性)添加属性观察器。
class Samplepgm {
var counter: Int = 0{
// 在设置新的值之前调用
// willSet和didSet观察器在属性初始化过程中不会被调用
// 可以单独设置willSet和didSet观察器
willSet(newTotal){
print("计数器: \(newTotal)")
}
// 在新的值被设置之后立即调用
didSet{
if counter > oldValue {
print("新增数 \(counter - oldValue)")
}
}
}
}
let NewCounter = Samplepgm()
NewCounter.counter = 100
NewCounter.counter = 800
计算属性和属性观察器所描述的功能也可以用于全局变量和局部变量(全局变量是在函数、方法、闭包或任何类型之外定义的变量。局部变量是在函数、方法或闭包内部定义的变量)。
另外,在全局或局部范围都可以定义计算型变量和为存储型变量定义观察器。计算型变量跟计算属性一样,返回一个计算结果而不是存储值,声明格式也完全一样。
需要注意的是:
全局的常量或变量都是延迟计算的,跟延迟存储属性相似,不同的地方在于,全局的常量或变量不需要标记lazy修饰符。局部范围的常量或变量从不延迟计算。
类型属性
类型属性是作为类型定义的一部分写在类型最外层的花括号({})内。使用关键字 static 来定义值类型的类型属性,关键字 class 来为类定义类型属性(支持子类对父类的实现进行重写)。
类型属性用于定义某个类型所有实例共享的数据,比如所有实例都能用的一个常量(就像 C 语言中的静态常量),或者所有实例都能访问的一个变量(就像 C 语言中的静态变量)。存储型类型属性可以是变量或常量,计算型类型属性跟实例的计算型属性一样只能定义成变量属性。
需要注意的是:
跟实例的存储型属性不同,必须给存储型类型属性指定默认值,因为类型本身没有构造器,也就无法在初始化过程中使用构造器给类型属性赋值。
存储型类型属性是延迟初始化的,它们只有在第一次被访问的时候才会被初始化。即使它们被多个线程同时访问,系统也保证只会对其进行一次初始化,并且不需要对其使用 lazy 修饰符。
struct StudMarks {
//使用的时候直接用类名去调用,只能取值,不能赋值。
//类型属性可以当做类里面的一个静态参数,相当于OC中的static
static let markCount = 97
static var totalCount = 0
// 计算属性
var InternalMarks: Int = 0 {
didSet {
// StudMarks.markCount用类名调用
if InternalMarks > StudMarks.markCount {
InternalMarks = StudMarks.markCount
}
if InternalMarks > StudMarks.totalCount {
StudMarks.totalCount = InternalMarks
}
}
}
func doSomeThing() -> Void {
// 类型属性的调用:直接用类型加.
StudMarks.totalCount = 5
}
}
// 创建实例
var stud1Mark1 = StudMarks()
var stud1Mark2 = StudMarks()
// 所有实例共用那些类型属性
stud1Mark1.InternalMarks = 98
print(stud1Mark1.InternalMarks)
stud1Mark2.InternalMarks = 87
print(stud1Mark2.InternalMarks)
// 类型属性的调用:直接用类型加.
print(StudMarks.totalCount)
补充一个通过闭包或函数设置属性的默认值:
// 如果没有()就是赋值闭包了
class SomeClass {
let someProperty: SomeType = {
// 在这个闭包中给 someProperty 创建一个默认值
// someValue 必须和 SomeType 类型相同
return someValue
}()
}
3. 方法
Swift 方法是与某些特定类型相关联的函数。在 Objective-C 中,类是唯一能定义方法的类型。但在 Swift 中,你不仅能选择是否要定义一个类/结构体/枚举,还能灵活的在你创建的类型(类/结构体/枚举)上定义方法。
实例方法
在 Swift 语言中,实例方法是属于某个特定类、结构体或者枚举类型实例的方法。跟OC的实例方法大同小异。所以主要说说Swift 的特别点:
① 函数参数可以同时有一个局部名称(在函数体内部使用)和一个外部名称(在调用函数时使用)
② 类型的每一个实例都有一个隐含属性叫做self,self 完全等同于该实例本身。(self是可以省略的)。
③ 可变方法。
Swift 语言中结构体和枚举是值类型。一般情况下,值类型的属性不能在它的实例方法中被修改。可以选择变异(mutating)这个方法,然后方法就可以从方法内部改变它的属性;并且它做的任何改变在方法结束时还会保留在原始结构中。方法还可以给它隐含的self属性赋值一个全新的实例,这个新实例在方法结束后将替换原来的实例。
struct area {
var length = 1 //→ 3 → 9
var breadth = 1 //→ 5 → 15
func area() -> Int {
return length * breadth
}
// 可变方法
mutating func scaleBy(res: Int) {
// 变了就是变了
length *= res
breadth *= res
print(length)
print(breadth)
}
}
var val = area(length: 3, breadth: 5)
val.scaleBy(res: 3)
val.scaleBy(res: 30)
val.scaleBy(res: 300)
类型方法
类型方法就是类型本身调用的方法,也就是我们常说的类方法。
声明结构体和枚举的类型方法,在方法的func关键字之前加上关键字static。类可能会用关键字class来允许子类重写父类的实现方法。
4. 下标(一种特殊的方法)
下标可以定义在类、结构体和枚举中,是访问集合,列表或序列中元素的快捷方式。可以使用下标的索引,设置和获取值,而不需要再调用对应的存取方法。例如:someArray[index],someDictionary[key]。(其实就是实现一个下标方法)
一个类型可以定义多个下标,通过不同索引类型进行重载。下标不限于一维,你可以定义具有多个入参的下标满足自定义类型的需求。
class daysofaweek {
private var days = ["Sunday", "Monday", "Tuesday", "Wednesday",
"Thursday", "Friday", "saturday"]
// 使用subscript关键字,显式声明入参(一个或多个)和返回类型
// 可以设定为读写或只读
// 可以使用下标为自己实现各种便捷的方法
subscript(index: Int) -> String {
get {
return days[index] // 声明下标脚本的值
}
set(newValue) {
self.days[index] = newValue // 执行赋值操作
}
}
}
var p = daysofaweek()
//直接通过下标取
print(p[0])
print(p[1])
print(p[2])
print(p[3])
5. 继承
在 Swift 中,类可以调用和访问超类的方法,属性和下标脚本,并且可以重写它们。我们也可以为类中继承来的属性添加属性观察器。
// 超类(也是基类)
// 我们可以使用final关键字(加在前面)防止它们(方法,属性,属性观察器)被重写(编译时检查)。
// 在关键字class前添加final特性(final class)来将整个类标记为 final 的,这样的类是不可被继承的,否则会报编译错误。
class Circle {
var radius = 12.5
var area: String {
return "矩形半径为 \(radius) "
}
}
// Circle的子类
class Rectangle: Circle {
var print = 7
// 重写计算属性
override var area: String {
return super.area + " ,但现在被重写为 \(print)"
}
}
let rect = Rectangle()
rect.radius = 25.0
rect.print = 3
print("半径: \(rect.area)")
// Rectangle的子类
class Square: Rectangle {
// 添加属性观察器
// 不可以为继承来的常量存储型属性或继承来的只读计算型属性添加属性观察器
override var radius: Double {
didSet {
print = Int(radius/5.0)+1
}
}
}
let sq = Square()
sq.radius = 100.0
print("半径: \(sq.area)")
6. 构造过程(创建)和析构过程(销毁)
构造过程
构造过程是为了使用某个类、结构体或枚举类型的实例而进行的准备过程。这个过程包含了为实例中的每个属性设置初始值和为其执行必要的准备和初始化任务。
以下有几个注意点:
- ① 与 Objective-C 中的构造器不同,Swift 的构造器无需返回值,它们的主要任务是保证新实例在第一次使用前完成正确的初始化。
- ② 类和结构体在实例创建时,必须为所有存储型属性设置合适的初始值(一种是在构造器中赋值,一种是在属性设置)。
- ③ 你可以在定义构造器 init() 时提供构造参数,跟函数和方法参数相同,构造参数也存在一个在构造器内部使用的参数名字和一个在调用构造器时使用的外部参数名字。
struct rectangle {
// 可以在属性声明时为其设置默认值:var length = 6
// 使用默认值能让你的构造器更简洁、更清晰,且能通过默认值自动推导出属性的类型。
var length: Double
var breadth: Double
// 如果你定制的类型包含一个逻辑上允许取值为空的存储型属性,你都需要将它定义为可选类型。
// 当存储属性声明为可选时,将自动初始化为空 nil。
var width: Double?
// 构造函数(构造器) → 无构造参数
// 类和结构体在实例创建时,必须为所有存储型属性设置合适的初始值(一种是在构造器中赋值,一种是在属性设置)。
init() {
// 这边是直接赋值,不会触发属性观察器(类似没走setter getter方法)
length = 6
breadth = 12
width = 10
}
// 构造函数 → 以下是有构造参数的情况
// 可以自己决定是否要外部参数或者用“_”替代
init(lenght: Double,breadth: Double,width: Double) {
self.length = lenght
self.breadth = breadth
self.width = width
}
}
var area = rectangle()
print("矩形面积为 \(area.length*area.breadth)")
只要在构造过程结束前常量的值能确定,你可以在构造过程中的任意时间点修改常量属性的值。对某个类实例来说,它的常量属性只能在定义它的类的构造过程中修改;不能在子类中修改。举个简单的例子:
// 尽管 length 属性现在是常量,我们仍然可以在其类的构造器中设置它的值:
let length: Double?
init(frombreadth breadth: Double) {
length = breadth * 10
}
默认构造器
在一些情况下,实例会获得默认构造器:
当基类(意味着它没继承其他构造方法)所有属性有默认值时,它将自动获得一个可以为所有属性设置默认值的默认构造器。
如果结构体对所有存储型属性提供了默认值且自身没有提供定制的构造器,它们能自动获得一个逐一成员构造器。
如果你为某个值类型定义了一个自定义的构造器,你将无法访问到默认构造器(如果是结构体,还将无法访问逐一成员构造器)。“假如你希望默认构造器、逐一成员构造器以及你自己的自定义构造器都能用来创建实例,可以将自定义的构造器写到扩展(extension)中,而不是写在值类型的原始定义中。*
// 类
class ShoppingListItem {
// 以下三种都有默认值,如果没有会报错
var name: String?
var quantity = 1
var purchased = false
}
// 默认构造器(同init())
var item = ShoppingListItem()
// 结构体
struct Rectangle {
var length = 100.0, breadth = 200.0
}
// 逐一成员构造器
let area = Rectangle(length: 24.0, breadth: 32.0)
为了能减少多个构造器间的代码重复,构造器可以通过调用其它构造器来完成实例的部分构造过程,这一过程称为构造器代理。(就是调用自己的其他构造器方法)。
值类型和类类型不同的是:值类型不支持继承,所以它们只能代理给本身提供的其它构造器。类类型可以继承自其它类,就可以代理给自己或父类的其他构造器,但是有责任保证其所有继承的存储型属性在构造时也能正确的初始化。
指定构造器和便利构造器
Swift 为类类型提供了两种构造器来确保实例中所有存储型属性都能获得初始值,它们分别是指定构造器和便利构造器。
指定构造器是类中主要且必要的构造器。一个指定构造器将初始化类中提供的所有属性,并根据父类链往上调用父类的构造器来实现父类的初始化。
便利构造器是类中比较次要的、辅助型的构造器。你可以定义便利构造器来调用同一个类中的指定构造器,并为其参数提供默认值。你也可以定义便利构造器来创建一个特殊用途或特定输入值的实例。
指定构造器必须总是向上代理(调用直接父类的指定构造器)。便利构造器必须总是横向代理(调用类中的其他构造器,最后肯定有一个指定构造器被调用)。
Swift 中类的构造过程包含两个阶段:
- 阶段 1
① 某个指定构造器或便利构造器被调用。
② 完成新实例内存的分配,但此时内存还没有被初始化。
③ 指定构造器确保其所在类引入的所有存储型属性都已赋初值。存储型属性所属的内存完成初始化。
④ 指定构造器将调用父类的构造器,完成父类属性的初始化。
⑤ 这个调用父类构造器的过程沿着构造器链一直往上执行,直到到达构造器链的最顶部。
⑥ 当到达了构造器链最顶部,且已确保所有实例包含的存储型属性都已经赋值,这个实例的内存被认为已经完全初始化。此时阶段 1 完成。 - 阶段 2
① 从顶部构造器链一直往下,每个构造器链中类的指定构造器都有机会进一步定制实例。构造器此时可以访问self、修改它的属性并调用实例方法等等。
② 最终,任意构造器链中的便利构造器可以有机会定制实例和使用self。
简单理解就是先初始化自己的属性,再调用父类的方法,整个初始化下来,再去调用方法,修改属性。
构造器的继承和重写
跟 Objective-C 中的子类不同,Swift 中的子类默认情况下不会继承父类的构造器(因为可能有些属性没被初始化)。所以当编写一个和父类中指定构造器相匹配的构造器时,实际上是在重写(所以要加override)。如果是便利构造器,不需要加,因为子类不能直接调用父类的便利构造器,所以不算重写。
但是如果满足特定条件,是会自动继承的:
假设你为子类中引入的所有新属性都提供了默认值(大前提),并且子类没有定义任何指定构造器,它将自动继承所有父类的指定构造器(便利构造器好像也可以,如下例子),如果子类提供了所有父类指定构造器的实现(无论是继承还是自定义实现(子类可以将父类的指定构造器实现为便利构造器)),它将自动继承所有父类的便利构造器。
// 食物
class Food {
var name: String
// 指定构造器
init(name: String) {
self.name = name
}
// 便利构造器
convenience init() {
self.init(name: "[Unnamed]")
}
}
// 菜谱原料
// 没有继承父类的构造器
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)
}
}
// 购物单项目
// 由于它为自己引入的所有属性都提供了默认值,并且自己没有定义任何构造器,
// ShoppingListItem将自动继承所有父类中的指定构造器和便利构造器。
class ShoppingListItem: RecipeIngredient {
var purchased = false
var description: String {
var output = "\(quantity) x \(name)"
output += purchased ? " ✔" : " ✘"
return output
}
}
可失败构造器和必要构造器
可失败构造器(?或!)和必要构造器(required)无非就是加个符号来表示。
如果一个类,结构体或枚举类型的对象,在构造自身的过程中有可能失败,则为其定义一个可失败构造器。
class Product {
let name: String
// 创建后对象类型时Product?
// 也可以用init!,会强制解包
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)
}
}
// 判断方式也可以用 CartItem(name: "1", quantity: 1) == nil
if let oneUnnamed = CartItem(name: "1", quantity: 1) {
print("Item: \(oneUnnamed.name), quantity: \(oneUnnamed.quantity)")
} else {
print("Unable to initialize one unnamed product")
}
在类的构造器前添加required修饰符表明所有该类的子类都必须实现该构造器。
class SomeClass {
// 在子类重写父类的必要构造器时,子类构造器前也添加required修饰符。
//在重写父类中必要的指定构造器时,不需要添加override修饰符。
required init() {
// 构造器的实现代码
}
}
析构过程
在一个类的实例被释放之前,析构函数被立即调用。用关键字deinit来标示析构函数,类似于初始化函数用init来标示。(析构函数只适用于类类型,因为类类型才有引用计数)
deinit {
// 执行析构过程,释放内存
}
7. 自动引用计数
Swift 和OC一样,都是使用自动引用计数(ARC)机制来跟踪和管理你的应用程序的内存(ARC的原理就不展开)。ARC最常见的就是循环引用的问题:
- ① 类实例之间的循环强引用
- ② 闭包实例之间的循环强引用
Swift 提供了两种办法用来解决你在使用类的属性时所遇到的循环强引用问题:弱引用(weak)和无主引用(unowned)。
对于生命周期中会变为nil的实例使用弱引用。相反的,对于初始化赋值后再也不会被赋值为nil的实例,使用无主引用。和弱引用类似,无主引用不会牢牢保持住引用的实例。和弱引用不同的是,无主引用在其他实例有相同或者更长的生命周期时使用。
简单总结下,如果属性是可选类型,只能用weak修饰符避免循环引用。所引用对象被回收后改属性会被自动置为nil
如果属性不是可选类型,只能用无主引用。所引用对象被回收后属性不会被置为nil,此时访问会导致运行时错误。类似OC中的unsafe_unretained修饰符。
class Module {
let name: String
init(name: String) { self.name = name }
var sub: SubModule?
deinit { print("\(name) 主模块") }
}
class SubModule {
let number: Int
init(number: Int) { self.number = number }
// 弱引用(主模块没了,对副模块的引用也会置为nil)
// 如果加上unowned(无主引用)
weak var topic: Module?
deinit { print("子模块 topic 数为 \(number)") }
}
var toc: Module?
var list: SubModule?
toc = Module(name: "ARC")
list = SubModule(number: 4)
toc!.sub = list
list!.topic = toc
// 主模块和副模块依次销毁
toc = nil
list = nil
同理,当闭包和捕获的实例总是互相引用时并且总是同时销毁时,将闭包内的捕获定义为无主引用。相反的,当捕获引用有时可能会是nil时,将闭包内的捕获定义为弱引用。如果捕获的引用绝对不会置为nil,应该用无主引用,而不是弱引用。
class HTMLElement {
let name: String
let text: String?
lazy var asHTML: () -> String = {
// 捕获列表是[unowned self],表示“将self捕获为无主引用而不是强引用”
// 这边unowned适合,如果是weak,会把self置为nil
[unowned self] in
if let text = self.text {
return "<\(self.name)>\(text)\(self.name)>"
} else {
return "<\(self.name) />"
}
}
init(name: String, text: String? = nil) {
self.name = name
self.text = text
}
deinit {
print("\(name) 被析构")
}
}
//创建并打印HTMLElement实例
var paragraph: HTMLElement? = HTMLElement(name: "p", text: "hello, world")
print(paragraph!.asHTML())
// HTMLElement实例将会被销毁,并能看到它的析构函数打印出的消息
paragraph = nil
8. 可选链
可选链是一种可以请求和调用属性、方法和子脚本(下标)的过程,用于请求或调用的目标可能为nil。 如果目标有值,调用就会成功,返回该值,如果目标为nil,调用将返回nil。多次请求或调用可以被链接成一个链,如果任意一个节点为nil将导致整条链失效。
可选链可以调用属性、方法、下标,可以链接起来(需要注意的是,调用总是返回一个可选类型)。也可以利用可选链来赋值。
class Person {
var residence: Residence?
}
// 定义了一个变量 rooms,它被初始化为一个Room[]类型的空数组
class Residence {
var rooms = [Room]()
var numberOfRooms: Int {
return rooms.count
}
subscript(i: Int) -> Room {
return rooms[i]
}
func printNumberOfRooms() {
print("房间号为 \(numberOfRooms)")
}
var address: Address?
}
// Room 定义一个name属性和一个设定room名的初始化器
class Room {
let name: String
init(name: String) { self.name = name }
}
// 模型中的最终类叫做Address
// 可选链定义的模型类(类似可选链)
class Address {
var buildingName: String?
var buildingNumber: String?
var street: String?
func buildingIdentifier() -> String? {
if (buildingName != nil) {
return buildingName
} else if (buildingNumber != nil) {
return buildingNumber
} else {
return nil
}
}
}
let john = Person()
// 不使用可选链的情况会崩溃,因为residence默认为nil
// let roomCount = john.residence!.numberOfRooms
// 访问属性
// 链接可选residence?属性,如果residence存在则取回numberOfRooms的值
// 返回的roomCount是Int?类型,所以不存在时为nil
if let roomCount = john.residence?.numberOfRooms {
print("John 的房间号为 \(roomCount)。")
} else {
print("不能查看房间号")
}
// 访问方法
// 如果方法通过可选链调用成功,printNumberOfRooms的隐式返回值将会是Void,如果没有成功,将返回nil。
if john.residence?.printNumberOfRooms() != nil {
print("指定了房间号)")
} else {
print("未指定房间号")
}
// 访问下标
if let firstRoomName = john.residence?[0].name {
print("第一个房间名为\(firstRoomName)")
} else {
print("无法检索到房间")
}
9. 错误处理
错误处理是响应错误以及从错误中恢复的过程。某些操作无法保证总是执行完所有代码或总是生成有用的结果。可选类型可用来表示值缺失,但是当某个操作失败时,最好能得知失败的原因,从而可以作出相应的应对。
在 Swift 中,错误用符合Error协议的类型的值来表示。这个空协议表明该类型可以用于错误处理。
enum VendingMachineError: Error {
case invalidSelection //选择无效
case insufficientFunds(coinsNeeded: Int) //金额不足
case outOfStock //缺货
}
Swift 中处理错误的方式:
// throws写在括号后面
// throwing 函数可以在其内部抛出错误,并将错误传递到函数被调用时的作用域。
// 也就是在调用canThrowErrors的地方会接收到这个错误,所以调用需要在前面加try canThrowErrors()
// Error可以通过throwing 函数一直抛出去
func canThrowErrors() throws {
// 错误时抛出
if XXX{
throw VendingMachineError.InvalidSelection
}
}
// 抛出Error到do..catch这边去处理
do {
// 执行do这部分,如果有错误抛给catch的符合条件的部分执行
// 如果catch没有符合的,就让他周围的作用域处理
try expression
statements
} catch pattern 1 {
statements
} catch pattern 2 where condition {
statements
}
// try后面加上?和!也是不同的处理方案
// try?等同于下面的do...catch
// try!表达式前面写try!来禁用错误传递(知道某个throwing函数实际上在运行时是不会抛出错误的)
let x = try? someThrowingFunction()
let y: Int?
do {
y = try someThrowingFunction()
} catch {
y = nil
}
10. 类型转换
Swift 中类型转换使用 is 和 as 操作符实现,is 用于检测值的类型,as 用于转换类型。类型转换也可以用来检查一个类是否实现了某个协议。
向下转型,用类型转换操作符(as? 或 as!)。当你不确定向下转型可以成功时,用类型转换的条件形式(as?)。条件形式的类型转换总是返回一个可选值,并且若下转是不可能的,可选值将是 nil。当确定向下转型一定会成功时,才使用强制形式(as!),否则会触发一个运行时错误。
// is的应用(是否是XXX)
// 如果是一个 Chemistry 类型的实例,返回 true,相反返回 false。
if item is Chemistry {
// 如果是化学
} else if item is Maths {
// 如果是数学
}
// as的应用(转换成XXX)
if let show = item as? Chemistry {
print("化学主题是: '\(show.physics)', \(show.equations)")
// 强制形式
} else if let example = item as? Maths {
print("数学主题是: '\(example.physics)', \(example.formulae)")
}
Swift为不确定类型提供了两种特殊类型别名:AnyObject可以代表任何class类型的实例。Any可以表示任何类型,包括方法类型。(使用期望的明确的类型总是比较好)
// 可以存储Any类型的数组 exampleany
// 如果是AnyObject则只能存储对象类型(比如下面的Chemistry实体)
var exampleany = [Any]()
exampleany.append(12)
exampleany.append(3.14159)
exampleany.append("Any 实例")
exampleany.append(Chemistry(physics: "固体物理", equations: "兆赫"))
// 使用强制形式的类型转换操作符(as, 而不是 as?)来检查和转换到一个明确的类型。
for item2 in exampleany {
switch item2 {
case let someInt as Int:
print("整型值为 \(someInt)")
case let someDouble as Double where someDouble > 0:
print("Pi 值为 \(someDouble)")
case let someString as String:
print("\(someString)")
case let phy as Chemistry:
print("主题 '\(phy.physics)', \(phy.equations)")
default:
print("None")
}
}
11. 扩展(extension)
扩展就是向一个已有的类、结构体或枚举类型添加新功能。扩展可以对一个类型添加新的功能,但是不能重写已有的功能。
可以看做是扩充原来的功能,写法上常使用extension来分割不同的功能块。
Swift 中的扩展可以:
- 添加计算型属性和计算型静态属性
- 定义实例方法和类型方法
- 提供新的构造器(只能是便利构造器,不能是指定构造器)
- 定义下标(下标方法)
- 定义和使用新的嵌套类型(这其实类似于添加属性)
- 使一个已有类型符合某个协议(写法上常把遵守某个协议写在一个extension,方便阅读)
extension SomeType {
// 加到SomeType的新功能写到这里
}
12. 协议(protocol)
协议规定了用来实现某一特定功能所必需的方法和属性。类,结构体或枚举类型都可以遵循协议,并提供具体实现来完成协议定义的方法和功能。
protocol classa {
// 指定特定的实例属性或类属性(不用指定是存储型属性或计算型属性)
// 必须指明是只读的还是可读可写的。{ set get }来表示属性是可读可写的
var marks: Int { get set }
var result: Bool { get }
// 如果是值类型遵守的协议,会改变其中的属性,需要在前面加上Mutating
func attendance() -> String
func markssecured() -> String
}
// 协议的继承(可以多个比如classb: classa,classc)
// 通过添加class关键字,限制协议只能适配到类(class)类型
protocol classb: class,classa {
var present: Bool { get set }
var subject: String { get set }
var stname: String { get set }
}
class classc: classb {
var marks = 96
let result = true
var present = false
var subject = "Swift 协议"
var stname = "Protocols"
func attendance() -> String {
return "The \(stname) has secured 99% attendance"
}
func markssecured() -> String {
return "\(stname) has scored \(marks)"
}
}
// 判断检验协议(is as? as)
// is操作符用来检查实例是否遵循了某个协议。
// as?返回一个可选值,当实例遵循协议时,返回该协议类型;否则返回nil。
// as用以强制向下转型,如果强转失败,会引起运行时错误。
// 如果同时遵守两个协议可以用classb&classd来表示
let c = classc.init()
if c is classb {
// 有遵守协议
} else {
// 没有遵守协议
}
另外,可以在遵循该协议的类中实现构造器,并指定其为类的指定构造器或者便利构造器。在这两种情况下,你都必须给构造器实现标上required修饰符。
尽管协议本身并不实现任何功能,但是协议可以被当做类型来使用(类比OC中的子类)。
protocol tcpprotocol {
init(no1: Int)
}
class mainClass {
var no1: Int // 局部变量
init(no1: Int) {
self.no1 = no1 // 初始化
}
}
// 使用required修饰符可以保证:所有的遵循该协议的子类,同样能为构造器规定提供一个显式的实现或继承实现。
// 如果一个子类重写了父类的指定构造器,并且该构造器遵循了某个协议的规定,那么该构造器的实现需要被同时标示required和override修饰符:
class subClass: mainClass, tcpprotocol {
var no2: Int
init(no1: Int, no2 : Int) {
self.no2 = no2
super.init(no1:no1)
}
// 因为遵循协议,需要加上"required"; 因为继承自父类,需要加上"override"
required override convenience init(no1: Int) {
self.init(no1:no1, no2:0)
}
}
class someClass {
// 协议类型充当属性(也可以作为参数返回值,或字典数组中的元素)
// 有点类似OC中的子类
let generator: tcpprotocol
// 代理人(委托模式)
var delegate:tcpprotocol?
init(generator: tcpprotocol) {
self.generator = generator;
}
}
12. 泛型
Swift 提供了泛型让你写出灵活且可重用的函数和类型。Swift 标准库是通过泛型代码构建出来的(比如数组和字典类型都是泛型集)。
所谓泛型,就是定义一个泛指的类型,来让我们使用各种类型都能实现功能。
// 定义一个交换两个变量的函数
// 表示T是占位类型名
// 泛型使用了占位类型名来代替实际类型名(例如 Int、String 或 Double),省去了写重复代码来实现交换功能
func swapTwoValues(_ a: inout T, _ b: inout T) {
let temporaryA = a
a = b
b = temporaryA
}
var numb1 = 100
var numb2 = 200
print("交换前数据: \(numb1) 和 \(numb2)")
swapTwoValues(&numb1, &numb2)
print("交换后数据: \(numb1) 和 \(numb2)")
var str1 = "A"
var str2 = "B"
print("交换前数据: \(str1) 和 \(str2)")
swapTwoValues(&str1, &str2)
print("交换后数据: \(str1) 和 \(str2)")
泛型的比较复杂的应用例子:
(常在协议中使用associatedtype来代替一种类型(可以当成占位符),在类中用具体实例或者泛型来替代之前的占位符)
// Container 协议
protocol Container {
// 使用 associatedtype 关键字来设置关联类型实例,也就是指后面的泛型Element
associatedtype ItemType
// 添加一个新元素到容器里
mutating func append(_ item: ItemType)
// 获取容器中元素的数
var count: Int { get }
// 通过索引值类型为 Int 的下标检索到容器中的每一个元素
subscript(i: Int) -> ItemType { get }
}
// // 遵循Container协议的泛型TOS类型
struct Stack: Container {
// Stack 的原始实现部分
var items = [Element]()
mutating func push(_ item: Element) {
items.append(item)
}
mutating func pop() -> Element {
return items.removeLast()
}
// Container 协议的实现部分
mutating func append(_ item: Element) {
self.push(item)
}
var count: Int {
return items.count
}
subscript(i: Int) -> Element {
return items[i]
}
}
// 扩展,将 Array 当作 Container 来使用
extension Array: Container {}
// 类型约束语法(泛型函数)→C1和C2参数必须是遵守Container协议
// where定义参数的约束(也就是要满足where后面的情况)
func allItemsMatch
(_ someContainer: C1, _ anotherContainer: C2) -> Bool
where C1.ItemType == C2.ItemType, C1.ItemType: Equatable {
// 检查两个容器含有相同数量的元素
if someContainer.count != anotherContainer.count {
return false
}
// 检查每一对元素是否相等
for i in 0..()
tos.push("google")
tos.push("runoob")
tos.push("taobao")
var aos = ["google", "runoob", "taobao"]
if allItemsMatch(tos, aos) {
print("匹配所有元素")
} else {
print("元素不匹配")
}
13. 访问控制(作用域)
访问控制可以限定其他源文件或模块中代码对你代码的访问级别。
你可以明确地给单个类型(类、结构体、枚举)设置访问级别,也可以给这些类型的属性、函数、初始化方法、基本类型、下标索引等设置访问级别。协议也可以被限定在一定的范围内使用,包括协议里的全局常量、变量和函数。
访问控制基于模块(我们项目就是一个模块,引入的库也是一个模块)与源文件(一个.swift文件)。
访问级别 | 定义 |
---|---|
public | 可以访问自己模块中源文件里的任何实体,别人也可以通过引入该模块来访问源文件里的所有实体。(第三方库会声明public,让我们impot使用) |
internal | 可以访问自己模块中源文件里的任何实体,但是别人不能访问该模块中源文件里的实体。(不写就是默认访问级别internal) |
fileprivate | 文件内私有,只能在当前源文件中使用。(在该.swift文件都可以使用) |
private | 只能在类中访问,离开了这个类或者结构体的作用域外面就无法访问。 |
除非有特殊的说明,否则实体都使用默认的访问级别 internal。函数的访问级别需要根据该函数的参数类型和返回类型的访问级别得出。
// 假如SomeInternalClass是internal级别,SomePrivateClass是private级别
// 可以推导出someFunction是private,所以必须写上,没写默认是internal
private func someFunction() -> (SomeInternalClass, SomePrivateClass) {
// 函数实现
}
同理,有以下这几种常见的规则。
① 枚举中成员的访问级别继承自该枚举,你不能为枚举中的成员单独申明不同的访问级别。
② 子类的访问级别不得高于父类的访问级别。比如说,父类的访问级别是internal,子类的访问级别就不能申明为public。
③ 常量、变量、属性不能拥有比它们的类型更高的访问级别。
④ 下标也不能拥有比索引类型或返回类型更高的访问级别。
⑤ 常量、变量、属性、下标索引的Getters和Setters的访问级别继承自它们所属成员的访问级别。Setter的访问级别可以低于对应的Getter的访问级别,这样就可以控制变量、属性或下标索引的读写权限。
⑥ 默认初始化方法的访问级别与所属类型的访问级别相同。
协议和扩展等等的访问权限也是需要去了解......(具体问题具体分析)
补充typealias和associatedtype的知识点:
typealias 是用来为已经存在的类型重新定义名字的,associatedtype 是用来指定关联类型。
在定义协议时,可以用associatedtype声明一个或多个类型作为协议定义的一部分,叫关联类型。这种关联类型为协议中的某个类型提供了别自定义名字,其代表的实际类型或实际意义在协议被实现时才会被指定。然后在不同的类中通过typealias指定不同的类型。
//模型
struct Model {
let age: Int
}
//协议,使用关联类型(不指定类型,等遵守协议了再决定类型)
protocol TableViewCell {
associatedtype T
func updateCell(_ data: T)
}
//遵守TableViewCell
class MyTableViewCell: UITableViewCell, TableViewCell {
// 定义类型名
// typealias Pat = Cat & Dog(后面直接用Pat代替)
typealias T = Model
func updateCell(_ data: Model) {
// do something ...
}
}