5.函数
1.基本定义
func 函数名(参数) -> 返回值 { }
func sum(num1:Int, num2: Int)-> Int {
return num1 + num2
}
调用:
sum (num1: 1, num2 :2)
2.相关注意点
- 1.参数默认
let
,不是var
,也只能是let
(不能是变量) - 2.隐氏返回
当函数体内的表达式是简单的,可以不写return
func sum(num1:Int, num2: Int)-> Int {
num1 + num2
}
- 3.可以使用元组返回多个值
- 4.可以在形参前再加字符串修改参数标签(num1 与num2),可以使用下划线 _ 省略参数标签
func sum(_ num1:Int, _num2: Int))-> Int {
num1 + num2
}
func sum(by num1:Int, num2: Int))-> Int {
num1 + num2
}
- 5.可变参数:
- 一个函数最多只能有1个可变参数,紧跟在可变参数后面的参数不能省略参数标签
可变参数定义: 在参数类型后面加上...
func sum(_ number:Int...) -> Int {
var total = 0
for num in numbers {
total += number
}
return total
}
sum(10,11,12)
- 6.可以用
inout
定义一个输入输出参数,可以在函数内部修改外部实参的值(swift已经默认屏蔽对象地址)- 可变参数不能标记为
inout
-
inout
参数不能有默认值 -
inout
参数的本质是地址传递(引用传递) -
inout
参数只能传入可以被多次赋值的(var变量,数组值等)
- 可变参数不能标记为
注:调用时,传入的实参需要加&,该函数可以令number的值修改为20
var number = 10
func sum (num: inout Int) {
num = 20
}
sum(&number)
- 7.函数重载
- 规则:
1.函数名相同
2.参数个数不同/参数类型不同/参数标签不同 - 注意点:
1.返回值类型与函数重载无关
2.默认参数值和函数重载一起产生二义性时,编译器并不会报错
3.可变参数,省略参数标签,函数重载一起使用产生二义性时,编译器有可能会报错
- 规则:
- 8.内联函数(即直接将函数的函数体放置在函数调用中)
- 如果开启了编译器优化(release模式默认会开启优化),编译器会自动将某些函数变成内联函数
- 那么,哪些不会被内联?
1.当函数体内容很长时,并不会内联
2.当有递归时,也并不会内联
3.包含动态派发的函数,也不会内联(类似多态时)
注: swift有main函数,只是不需要我们自己写
- 9.函数类型:
1.func sum(Int num1,Int num2)-> Int {
return num1 + num2
}
函数类型为:(Int,Int) -> Int
2.func add() -> {
}
函数类型为:() ->()
定义变量: var fn: (Int,Int) -> Int = sum
fn(1,2) 结果为3 (注:此时调用函数是不需要参数标签的)
注:返回值是函数类型的函数,叫做高阶函数
- 10.
typealias
关键字- 作用:用来给类型起别名
- 按照swift标准库定义,
void
就是空元组
- 11.
@inline
- 永远不会被内联(即使开启了编译器优化)
@inline(never) func test(){ print("test") }
- 开启编译器优化后(相当于debug模式下是不会生效的),即使代码很长,也会被内联(递归调用函数,动态派发的函数除外)
@inline(__always) func test(){ print("test") }
注: release模式下,没太大必要使用@inline
- 12.嵌套函数
含义:将函数定义在函数内部
func forward (_ isAdd :Bool) -> (Int) ->Int {
func add (_ num :Int) ->Int {
return num + 1
}
func minix (_ num:Int)-> Int {
return num - 1
}
return isAdd == True? add:minix
}
forward(true)(3) 结果为:4
forward(false)(3) 结果为:2
6.枚举
-
定义
-
enum Season {
case Spring
case Summer
case Autumn
case Winter
}
等价于:
enum Season {
case Spring,Summer,Autumn,Winter
}
print(Season.spring)
-
-
关联值(将各种类型的数据直接存储在枚举变量里)
-
enum Score {
case Point(Int)
case grade(Character)
}
var score = Score.Point(90)
score = .grade("B")
-
-
原始值
rawValue
-
enum AAA :String {
case aaa
case bbb
}
表示成员的关联值类型为String
- 原始值与关联值的区别:
- 关联值可以被外部赋值的,而原始值是无法被外部赋值的
- 关联值会直接将存储进去的值直接存储在内存中,而原始值不会,原始值是绑定的,是不会占用枚举变量内存的
- 直白来说:关联值今后是可以修改的,所以需要开辟空间去存储,而原始值是绑定的,不动的,不允许外部进行修改.所以只需要1个字节存储即可
- 如果枚举的原始值
Int
,String
,swift会自动分配原始值 - 递归枚举,需要关键字
indirect
- 使用
switch
+enum
,可以在switch
的case
中对枚举进行let
赋值使用,在使用关联值时使用let
-
MemoryLayout
-
- 获取数据类型占用的内存大小
- 用法: e.x
MemoryLayout<Int>.size
-
MemoryLayout.stride
: 分配占用的空间大小 -
MemoryLayout.size
: 实际用到的空间大小 -
MemoryLayout.alignment
: 对齐参数
-
enum Season : String {
case spring = "1",summer = "2",autumn = "3",winter = "4"
}
MemoryLayout<Season>.size = 1
MemoryLayout<Season>.stride = 1
MemoryLayout<Season>.alignment = 1
//因为外部无法修改spring等枚举值,所以spring为原始值,只需要1个字节进行存储即可
- 当同时存在原始值与关联值时,关联值需要开辟空间进行存储,原始值需要开辟1个字节进行辨别,是关联值还是原始值
- 现在的CPU基本是小端模式,内存越高的字节,实际也是高字节,内存比较低的字节,实际也是低字节(高高低低)
enum test {
case num1(Int, Int, Int)
case num2(Int,Int)
case num3(Int)
case num4(Bool)
case num5
}
MemoryLayout<test>.size = 25
MemoryLayout<test>.stride = 32
MemoryLayout<test>.alignment = 8
-
内存布局
-
- 如果有关联值,则必定会有1个字节用于存储成员值(即第几个成员),N个字节存储关联值,N取占用内存最大的关联值,任何一个case的关联值都共用这N个字节
enum test {
case num
}
MemoryLayout<test>.size = 0
MemoryLayout<test>.stride = 1
MemoryLayout<test>.alignment = 1
- 如果枚举里只有一个原始值,则可以这么说,该变量不占内存
enum test {
case num(Int)
}
MemoryLayout<test>.size = 8
MemoryLayout<test>.stride = 8
MemoryLayout<test>.alignment = 8
- 存储成员值的前提是,有超过1个的成员,所以上述不需要专门一个字节去存储成员值
enum test {
case num(Int)
case num2
}
MemoryLayout<test>.size = 8+1 = 9
MemoryLayout<test>.stride = 16
MemoryLayout<test>.alignment = 8
7.可选项
-
-
基本定义
- 一般也叫可选类型,它允许将值设置为
nil
- 在类型名称后面加个问号
?
,来定义一个可选项
var name:String? = "Jack" , name = nil var num :Int? //则num默认的初始值为nil
- 可选项的本质是对其他类型的一层包装,可以将它理解成一个盒子
- 如果为
nil
,那么它是一个空盒子 - 如果不为
nil
,那么盒子里装的,是被包装类型的数据
- 如果为
- 如果要从可选项中取出被包装的数据,需要使用
!
进行强制解包,进行强制解包,只是将可选项里的值拿出来用一下而已,不会因此改变可选项 - 如果对值为
nil
的可选项进行强制解包,将会产生运行时错误
-
-
-
可选项绑定
- 可以使用可选项绑定来判断可选项是否包含值,如果包含就自动解包,把值赋给一个临时的常量(
let
),或者变量(var
),并返回true
,否则返回false
if let number = Int("123") { print(number) //(此时,number不需要加!) } else { print("error") }
- 注意:
- 涉及到可选项绑定时,若
有且
或非
的关系时,不能使用&&
等,需要使用逗号,
,此时,意义是,两侧的表达式必须同时成立才会执行 -
number
的作用域只在if那个{}之前
-
-
-
空合并运算符
- 定义:
a ?? b
-
a
一定要为可选项,b
是可选项,也可以不是可选项 -
b
与a
的存储类型必须相同 - 如果
a
不为nil
,则返回a
,如果a
为nil
,则返回b
- 如果
b
不是可选项,返回a
时会自动解包 - 空合并运算符的返回类型是取决于
b
- 多个
??
一起使用,具体类型看最右边 - 空合并运算符与
if let
相结合使用
-
-
let a:Int? = nil
let b:Int? = 2
if let c = a ?? b {
print(c)
}
类似于if a!= nil || b != nil
if let c = a, let d =b {
print(c,d)
}
类似于if a!= nil && b != nil
-
-
guard
- 通过字典取值返回
dict["name]
的类型是可选类型,而数组取出的值为真实类型(因为数组如果越界,则程序直接crash) - 用法:
guard 条件 else { 退出当前作用域 }
- 当
guard
语句的条件为false
时,就会执行大括号里面的代码
当guard
语句的条件为true
时,就会跳过guard
语句
guard
语句特别适合用来提前退出
- 当使用
guard
语句进行可选项绑定时,绑定的常量let
,变量var
也能在外层作用域中使用
-
-
-
隐式解包(自动解包) (尽量不要使用)
- 在某些情况下,可选项一旦被设定值之后,就会一直拥有值
- 在这种情况下,可以去掉检查,也不必每次访问的时候都进行解包,因为它能确定每次访问的时候都有值
- 可以在类型后面加个感叹号
!
,定义一个隐式解包的可选项 - 如果隐式解包的可选项是
nil
,则程序直接会崩溃 - 用途:希望别人给你具体的值,但也有可能会返回
nil
时
-
-
-
多重可选项 ??
- 实际上,
??
可以理解为对可选项的可选项
然而,let num1 : Int? = 10 let num2 : Int?? = num1 let num3 : Int?? = 10 实际上.num2与num3等价
let num1 : Int? = nil let num2 : Int?? = num1 let num3 : Int?? = nil 此时,num3就是个空值,故num2与num3不等价 因为num1与num3的类型不同,所以num1与num3不等价
-
-
-
可选项的本质
enum
类型public enum Optional<Wrapped>: ExpressibleByNilLiteral { case none case some(Wrapped) public init (_ some: Wrapped) }
var age: Int? = 10 switch age { case let v?: print("1",v) case nil: print("2") } 通过switch判断一个可选类型是否为空,可以在case中使用v?,但不为nil,则会解包,并将解包后的值赋值给v 又因为可选项本身是一个枚举,也可以直接将枚举值用在case中
-
8.结构体与类
-
结构体
1.定义:
struct Data { var year: Int var month: Int var day: Int } var date = Date(year: 2020, month:2, day:20) 所有的结构体都有一个编译器自动生成的初始化器 其中,year,month,day称之为存储属性,可以给存储属性增加初始值
2.编译器会根据情况,可能会为结构体生成多个初始化器,宗旨是:保证所有成员都有初始值
3.一旦在定义结构体时自定义了初始化器,编译器就不会再帮它自动生成其他初始化器 -
类
1.定义:类的定义和结构体类似,但编译器并没有为类自动生成可以传入成员值的初始化器
class Data { var year: Int = 2020 var month: Int = 2 var day: Int = 20 } 若存储属性有初始值,编译器还是会为类生成一个无参的初始化器,成员的初始化就是在初始化器里面完成的 但若没有初始值,连无参的初始化器也都没有
2.结构体与类的本质区别:结构体是值类型(枚举也是值类型),类是引用类型(指针类型)
class Size { var width: Int = 20 var height: Int = 10 } struct origin { var x: Int = 20 var y: Int = 10 } func test() { var size = Size() var origin = origin() }
- 此时,结构体
origin
的内存位于栈空间,第一个地址即是结构体的地址,类(指针)对象的内存地址也是位于栈空间,占8个字节,里面存的是Size
对象地址,位于堆空间 - 通过C语言函数
malloc_size
,可以获取引用对象占用多少字节 - 在Mac,iOS中的
malloc
函数分配的内存大小总是16
的倍数 - 结构体对象地址在哪里取决于该结构体在哪里被使用(创建),如果在函数里,那么结构体地址就是在栈空间,如果在类里,则地址是在堆空间,若在外部,就是在全局数据段
- 指针对象同理,指针对象的内存地址取决于在哪里创建对象,但指针对象的地址是始终位于堆空间
- 只要是函数,都是在栈空间,至于函数是位于哪里,这个是不影响的
3.值类型与引用类型
3.1 引用类型:赋值给var
,let
或者给函数传参,是将内存地址拷贝一份,指向同一个文件,属于浅拷贝tip:今后在汇编中看到rbp-某个值,基本都是局部变量(栈空间),rip + 某个值,基本都是全局变量(全局区)
- swift中的字符串,数组和字典都是结构体,同理,支持上述理论
- 在swift标准中,为了提升性能,字符串,数组,字典采取了
copy on write
的方式,即若仅仅是将数组1
=数组2
,则此时是浅拷贝,当数组2
需要进行修改等写操作时,才为深拷贝
- 在swift标准中,为了提升性能,字符串,数组,字典采取了
- 自己定义的结构体是不符合这条的
3.2 引用类型:赋值给
var
,let
或者给函数传参,是将内存地址拷贝一份,指向同一个文件,属于浅拷贝tip:内存地址格式为rax + 某个值,一般是堆空间
- 堆空间的前16个字节中,前8个字节是用于记录类的一些信息,后8个是记录引用计数,是所有类的堆空间固定的
4.
class_getInstanceSize
: 类的对象至少需要占用多少内存,并非堆空间实际分配的内存大小- 一般把定义在枚举,结构体,类内部的函数,叫做方法,本质就是一个函数
- 方法不占用对象的内存空间
- 方法,函数都存放在代码段,至于函数放在全局区,类里还是哪里,是无关的
5.程序的内存分布:
代码段(函数等)
=>数据段(全局变量等)
=>堆空间
=>栈空间
- 此时,结构体
9.属性
-
属性可以分为两大类:存储属性与计算属性
-
-
存储属性
- 类似于成员变量这个概念
- 存储在实例的内存中
- 结构体、类可以定义存储属性
- 枚举不可以定义存储属性
-
关于存储属性:swift有个明确的规定,在创建类或者结构体的实例时,必须为所有的存储属性设置一个合适的初始值
- 可以在初始化器里给存储属性设置一个初始值
- 可以分配一个默认的属性值作为属性定义的一部分
-
计算属性
- 本质就是方法(函数),本质上增加了2个方法(
set
和get
) - 不占用实例的内存(若一个属性与另一个属性存在逻辑关系,则该属性使用计算属性)
- 本质就是方法(函数),本质上增加了2个方法(
class Person {
var age:Int = 0
//计算属性
var num :Int {
set {
age = newValue *2
}
get {
return newValue
}
}
}
- 关于计算属性:
-
set
传入的新值默认叫做newValue
,也可以自定义,在setter
后面加上括号(),括号里面是新值的名称
-
set(value) { num = value * 2 }
- 2.只读计算属性:只有
get
,没有set
,可以省略get
- 3.计算属性只能用
var
,不能用let
- 4.有
set
,必须有get
tips: 枚举的原始值就是通过只读计算属性实现的,不需要存储原始值(rawValue)
-
延迟存储属性(懒加载lazy)
-
使用lazy
可以定义一个延迟存储属性,在第一次用到属性的时候才会初始化
- 注意点:
-
lazy
属性必须是var
,不能是let
(let
必须在实例的初始化方法完成之前就拥有值) - 如果多条线程同时第一次访问
lazy
属性,无法保证属性只被初始化1次(即不是线程安全的) - 当结构体包含一个延迟存储属性时,只有
var
才能访问延迟存储属性,因为延迟属性初始化时需要改变结构体的内存
struct Point {
var x = 0
var y = 0
lazy var z = 0
}
let p = Point ()
print(p.z) //会报错
因为当创建p时,结构体的内存已经固定,而当p.z调用时,才会令z赋值为0,此时需要改变对应结构体的内存,令结构体Point的z值改变,但p又是使用let,无法改变内存,矛盾,所以只有var才可以访问,但此时依然可以访问x和y
-
属性观察器
-
可以为非lazy
的var
定义的存储属性
设置属性观察者
struct Circle {
var radius :Double {
willSet {
print(newValue)
}
didSet {
print(oldValue, radius)
}
}
init() {
self.radius = 1.0
}
}
radius依然是个存储属性,因为括号里面是willSet和didSet
- 计算属性因为本身就带有
set
和get
,需要监听直接在其set
里面写代码监听就行,无需另写willSet
,且willSet
与set
不能共存 -
willSet
会传递新值,默认叫newValue -
didSet
会传递旧值,默认叫oldValue - 在初始化器中设置属性值不会触发
willSet
和didSet
- 若直接在定义存储属性时直接定义初始值也不会触发属性观察器,因为在定义存储属性时直接定义初始值,本质就是在init初始化器里面进行设置初始值
- 属性观察器、计算属性的功能,同样可以应用在全局变量、局部变量上
var num : Int {
set {
print (newValue)
}
get {
return 10
}
}
num = 11 // 11
print (num) // 10
-
inout底层知识
-
- 计算属性被带有
inout
标签的函数调用,底层逻辑是:会先调用计算属性中的get
方法,将值对应存储在一个局部变量中,将该局部变量的地址传入函数,修改赋值对应的局部变量,当函数执行完毕,将赋值后的局部变量,调用计算属性的set
方法 - 属性观察器被带有
inout
标签的函数调用,底层逻辑是:会先将属性观察器中的存储属性的值存储到一个局部变量中,将该局部变量的地址传入函数,修改赋值对应的局部变量,当函数执行完毕,将赋值后的局部变量调用willSet
和didSet
- 输入输出函数的本质,还是地址(引用)传递,区别只是是谁的地址值
- 总结:
1.如果实参有物理内存地址(存储属性),且没有设置属性观察器,则直接将实参的内存地址传入函数(实参进行引用传递)
2.如果实参是计算属性,或者设置了属性观察器,则会才去"Copy In Copy Out"的做法- 调用函数时,先复制实参的值,产生副本(
get
) - 将副本的内存地址传入函数(副本进行引用传递),在函数内部可以修改副本的值
- 函数返回后,再将副本的值覆盖实参的值(
set
)
- 调用函数时,先复制实参的值,产生副本(
-
类型属性(类属性)
-
- 严格来说,属性可以分为:
- 实例属性:只能通过实例去访问
1.存储实例属性:存储在实例的内存中,每个实例都有1份
2.计算实例属性 - 类型属性:只能通过类型去访问
1.存储类型属性:整个程序运行过程中,就只有1份内存(类似于全局变量)
2.计算类型属性
- 实例属性:只能通过实例去访问
可以通过static定义类型属性,如果是类,也可以用关键字class
struct Car {
static var count: Int = 0 //初始值一定要有
init() {
Car.count += 1
}
}
注:存储实例属性时可以没有初始值,但存储类型属性必须要有初始值,因为类型没有像实例那样的init初始化器来初始化存储属性
- 存储类型属性默认就是
lazy
,会在第一次使用的时候才初始化 - 就算被多个线程同时访问,保证只会初始化一次,即是线程安全的,因为底层使用了
gcd
的dispatch_once
- 类型存储属性也可以使用
let
,与之前的lazy
时不能使用let
不相违背.因为那个针对的是实例,这个是类型 - 枚举类型也可以定义类型属性(存储类型属性,计算类型属性),但不可以定义存储实例属性
-
单例模式
-
public class fileManager {
public static let share = fileManager()
private init() {
}
}
1.类型存储属性本质上就是一个全局变量,只是编译器在限制了对象的访问范围.
2.使用static修饰的变量,只能说,是变量位于全局区
10.方法
枚举、结构体、类都可以定义实例方法、类型方法
- 实例方法:通过实例调用
- 类型方法:通过类型调用,用
static
或者class
关键字定义
class Car {
static var count = 0
init() {
Car.count += 1
}
static func getCount() -> Int { count} //此时,在类型方法中,只能调用类型属性,实例属性无法在类型方法里调用
}
let c0 = Car()
let c1 = Car()
let c2 = Car()
print (Car.getCount()) //3
/* self: 在实例方法中,self代表实例,
在类型方法中,self代表类型
*/
- 结构体中是不可以在实例方法中修改实例对象的,如果要完成这个操作.要在方法前添加关键字
mutating
,结构体和枚举是值类型,默认情况下,值类型的属性不能被自身的实例方法修改
struct Point {
var x = 0.0
var y = 0.0
@discardableResult mutating func moveX(alterX : Double) ->Double {
x += alterX
return x
}
}
var p = Point ()
p.moveX(alterX: 10)
@discardableResult 可以消除函数调用后返回值未被使用的警告
- 同理,枚举也是如此
enum stateSwitch {
case low,middle
mutating func next() {
switch self {
case .low:
self = .middle
case .middle:
self = .low
}
}
}
-
下标(subscript)
- 使用subscript 可以给任意类型(枚举、结构体、类)添加下标功能
subscript的语法类似于实例方法、计算属性,本质就是方法(函数)
class Point {
var x = 0.0
var y = 0.0
subscript (index: Int) ->Double {
set {
if index == 0 {
x = newValue
} else if index == 1 {
y = newValue
}
}
get {
if index == 0 {
return x
} else if index == 1 {
return y
}
return 0
}
}
}
p[0] = 22.2
-
subscript中定义的返回值类型决定了:
-
get
方法的返回值类型 -
set
方法中newValue的类型
-
subscript可以接受多个参数,并且类型任意
subscript可以没有
set
方法,但必须有get
方法subscript如果只有
get
方法,可以省略get
subscript可以像函数一样,设置参数标签,但在调用时,只有设置了参数标签的,才需要加上标签名称,不然默认是不需要标签名称的
subscript可以是类型方法,对应的调用是通过类型进行调用的
struct Point {
var x = 0
var y = 0
class PointManager {
var point = Point()
subscript (index: Int) -> Point {
set {
point = newValue
}
get {
return point
}
}
}
}
var pm = PointManager ()
pm[0].x = 2
//以上调用不会报错,等价于pm[0] = Point(2,pm[0].y)
//若不加上上面的set,则会报错
//若将point从结构体改为类,则以上调用也不会报错
11.继承
- 值类型(枚举、结构体)不支持继承,只有类支持继承
- 没有父类的类,称之为基类,swift并没有像
oc
、java
那样的规定,任何类最终都要继承自某个基类 - 实例对象
- 子类可以重写父类的
下标
、方法
、属性
,重写必须加上关键字override
,之所以下标
和计算属性
可以重写,是因为他们本质是方法 - 继承对于存储属性而言,等价于直接将其从父类拿到自己的类中,且父类的存储属性放在内存的前面
- 子类可以重写父类的
class Animal {
func speak() {
print("speak")
}
subscript (index:Int) ->Double {
return index
}
}
class dog : Animal {
//重写实例方法
override func speak() {
super.speak()
print("speak")
}
//重写下标
override subscript (index:Int) ->Double {
return super[index] +1
}
}
-
类型对象
- 被
class
修饰的类型方法、下标,允许被子类重写 - 被
static
修饰的类型方法、下标,不允许被子类重写 - 如果父类是用
class
创建的类型方法,子类是可以使用static
创建类型方法,影响的只是子类的子类 - 被
class
修饰的计算类型属性,可以被子类重写, - 被
static
修饰的类型属性(计算、存储),不可以被子类重写 - 存储类型属性本身就不可以被
class
修饰
- 被
子类可以将父类的属性(存储、计算)重写为计算属性,但不可以将父类属性重写为存储属性
只能重写
var
属性,不能重写let
属性重写时,属性名、类型要一致
-
子类重写后的属性权限
不能小于
父类属性的权限- 如果父类属性是只读的,那么子类重写后的属性可以是只读的,也可以是可读写的
- 如果父类属性是可读写的,那么子类重写后的属性也必须是可读写的
子类虽然将父类的存储属性重写为计算属性,但创建的子类依然还需要有额外的空间拿来存储父类的存储属性(从父类继承过来的存储属性,在子类都需要分配存储空间,无论子类要不要重写,此时调用使用
super
拿到父类以前的存储属性)如果在子类重写父类属性的
set
或者get
里,使用父类的存储属性,需要使用super.属性
的方式,否则会产生死循环
class Circle {
var radius : Int = 1
}
class subCircle : Circle {
override var radius: Int {
willSet {
print(newValue)
}
didSet {
print(oldValue,radius) //此时子类只是为父类增加属性观察器,并没有将父类的存储属性重写为计算属性.所以此时调用属性不需要super
}
}
}
- 如果父类本身就包含属性观察器,而子类重写了属性生成器,会先调用
子类的willSet
->父类的willSet
->父类的didSet
->子类的didSet
- 子类可以为父类的
计算属性
增加属性观察器(原先说的计算属性和属性观察器不能共存,是在一个类里,现在是继承关系,所以不存在这个问题) -
final
关键字- 被
final
修饰的方法、下标、属性,禁止被重写 - 被
final
修饰的类,禁止被继承
- 被
- 多态的实现原理
类似于C++
的虚表,类的类型信息是存放在一张表里,在编译时就可以确定- 同一个类无论创建多少个对象实例,这些对象的前8个字节都是一样的,都是用于存储一些类信息的
- 由打印地址基本可以判断,类信息的存储信息,位于全局区
12.初始化
-
- 类、结构体、枚举都可以定义初始化器
- 类有2中初始化器:
指定初始化器
和便捷初始化器
//指定初始化器
init (Parameters) {
//初始化代码
}
//便捷初始化器
convenience init (Parameters) {
//初始化代码
}
//枚举的初始化器调用
enum Season :Int {
case Spring = 0,Summer,Autumn,Winter
}
Season(rawValue:1)
- 每个类至少有一个指定初始化器,指定初始化器是类的主要初始化器
- 默认初始化器总是类的指定初始化器
- 类偏向于少量指定初始化器,一个类通常只有一个指定初始化器
- 初始化器的相互调用规则:
1.指定初始化器必须从它的直系父类调用指定初始化器
2.便捷初始化器必须从相同的类里调用另一个初始化器
3.便捷初始化器最终必须调用一个指定初始化器
4.类的指定初始化器必须调用父类的指定初始化器,便捷初始化器必须调用类自己的指定初始化器
5.类的指定初始化器不能调用类自己的其他指定初始化器,便捷初始化器可以调用类自己其他的便捷初始化器,但便捷初始化器最终必须调用另一个指定初始化器
6.子类的指定初始化器只能调用父类的指定初始化器,而不能调用父类的便捷初始化器
- 初始化器的相互调用规则:
- 创建类对象时,系统会自动帮忙创建一个无参的指定初始化器
- 如果有自定义的指定初始化器,则系统默认创建的无参的指定初始化器就会消失,无法再调用
- 但如果创建的是便捷初始化器,则系统默认创建的无参的指定初始化器还在
- 今后编程时,把主要的初始化器使用指定初始化器,把其他的设置成便捷初始化器
- 这一套规则,保证了使用任意的初始化器,都可以完整的初始化实例
- swift在编码安全方面,为了保证初始化过程的安全,设定了
两段式初始化
、安全检查
- swift在编码安全方面,为了保证初始化过程的安全,设定了
-
两段式初始化
第1阶段:初始化所有存储属性
1.外层调用指定/便捷初始化器
2.分配内存给实例,但未初始化
3.指定初始化器确保当前类定义的存储类型都初始化
4.指定初始化器调用父类的初始化器,不断向上调用,形成初始化器链第2阶段:设置新的存储属性值
1.从顶部初始化器往下,链中的每一个指定初始化器都有机会进一步定制实例
2.初始化器现在能够使用self
(访问、修改属性、调用它的实例方法等)
3.最终,链中任何便捷初始化器都有机会定制实例以及使用self
-
安全检查
1.指定初始化器必须保证在调用父类初始化器之前,其所在类定义的所有存储属性都要
2.指定初始化器必须先调用父类初始化器,然后才能为继承的属性设置新值
3.便捷初始化器必须先调用同类中的其他初始化器,然后再为任意属性设置新值
4.初始化器在第1阶段初始化完成之前,不能调用任何实例方法,不能读取任何实例属性的值,也不能引用self
5.直到第1阶段结束,实例才算完全合法 - 当重写父类的指定初始化器时,必须加上
override
(即使子类的实现是便捷初始化器) - 如果子类写了一个匹配父类便捷初始化器的初始化器,不用加上
override
,严格意义上说,这个不叫重写,便捷初始化器只能横向调用,是无法被子类调用的 - 自动继承
- 如果子类没有自定义任何指定的初始化器,它会自动继承父类所有的指定初始化器,在这种情况下,因为默认已经继承了父类的指定初始化器,所以系统默认的无参的初始化器也就不存在了,如果自己写了一个指定初始化器,则父类的指定初始化器不会自动继承了
- 如果子类提供了父类所有指定初始化器的实现(要么通过方式1继承,要么重写),则子类自动继承所有的父类便捷初始化器
- 就算子类添加了更多的便捷初始化器,这些规则仍然使用(如果自定义的初始化器是便捷初始化器,则规则1,2还是能满足的)
- 子类以便捷初始化器的形式重写父类的指定初始化器.也仍然可以作为满足规则2的一部分
- 自动继承
-
required
- 用
required
修饰指定初始化器,表明其所有子类都必须实现该初始化器(通过继承或者重写实现) - 如果子类重写了
required
初始化器,也必须加上required
,不用加override
- 父类的属性在它自己的初始化器中赋值不会触发属性观察器,但在子类的初始化器中赋值会触发属性观察
-
- 可失败初始化器(
init?
)
- 类、结构体、枚举都可以使用
init?
定义可失败初始化器,可失败初始化器返回的对象是个可选项,可能为nil
- 之前接触的可失败初始化器
var num = Int("123") public init?(_ description:String) enum Answer :Int { case Wrong, Right } var ans = Answer(rawValue: 1)
- 不允许同时定义参数标签、参数个数、参数类型相同的可失败初始化器和非可失败初始化器
- 可以用
init!
定义隐式解包的可失败初始化器 - 可失败初始化器可以调用非可失败初始化器,非可失败初始化器调用可失败初始化器需要进行解包
- 如果初始化器调用一个可失败初始化器导致初始化失败,那么整个初始化过程都失败,并且之后的代码都停止执行
- 可以用一个非可失败初始化器重写一个可失败初始化器,但反过来不行
- 可失败初始化器(
-
- 反初始化器(
deinit
)
-
deinit
叫做反初始化器,类似于C++
的析构函数、OC
中的dealloc
函数 - 当类的实例对象被释放内存时,就会调用实例对象的
deinit
方法 -
deinit
不接受任何参数,不能写小括号,不能自行调用
父类的deinit
能被子类继承
子类的deinit
实现执行完毕后,会调用父类的deinit
- 反初始化器(
13.可选链
- 在swift中,
nil
是无法拿来使用的 - 可选链返回的值都是可选类型
- 如果可选项为
nil
,调用方法、下标、属性会失败,结果为nil
- 如果可选项不为
nil
,调用方法、下标、属性会成功,结果会被包装成可选项(如果结果本身就是可选项,不会进行再次包装)
func getName() -> String { "name" } //如果person结果是nil,不会调用getName() person?.name = getName()
- 多个
?
可以连接在一起,如果链中任何一个节点是nil
,那么整个链就会调用失败
var dog = person?.dog // Dog? var weight = person?.dog.weight // Int? var price = person?.car?.price // Int?
var num1: Int? = 5 num1? = 10 //Optional(10) var num2: Int? = nil num2? = 10 //nil 带?进行赋值,会先判断?前的变量是否为nil,不为nil则赋值成功,类型为可选类型,为nil则赋值不成功,依旧为nil
var dict: [String : (Int,Int)->Int] = [ "sum" : (+), "different" : (-) ] var result = dict["sum"]?(10,20) // Optional(30) (+)与(-)是编译器特性,取出的是函数也需要进行为空判断
- 可选类型进行属性引用或方法调用,需要加上
?
- 如果可选项为
14.协议
- 协议可以用来定义方法、属性、下标的声明,协议可以被枚举、结构体、类遵守(多个协议之间用
,
隔开)
- 协议可以用来定义方法、属性、下标的声明,协议可以被枚举、结构体、类遵守(多个协议之间用
protocol Drawble {
func draw()
var x: Int { get set }
var y: Int { get }
subscript { index: Int } -> Int { get set }
}
protocol Drink {
var z: Int { set get }
}
class Person : Drawble , Drink {
}
-
有关协议定义注意点
- 协议中定义方法时不能有默认参数值
- 默认情况下,协议中定义的内容必须被全部实现,也有办法办到只实现部分内容(方式1: 定义协议的
extension
,在extension
中实现的协议即是默认协议,方式2:optional
关键字) - 协议里的属性不要求是计算属性或是存储属性,只要告知是只读属性还是可读可写属性
- 协议中定义属性时必须用
var
关键字 - 实现协议时的属性权限要不小于协议中定义的属性权限
协议定义set
、get
,用var
存储属性或get
、set
计算属性去实现
协议定义get
,用任何属性实现都可以
-
协议中的类型方法,类型属性,类型下标
- 为了保证通用,协议中必须用
static
定义类型方法,类型属性,类型下标
- 为了保证通用,协议中必须用
-
协议中的
mutating
- 只有将协议中的实例方法标记为
mutating
,才允许结构体、枚举的具体实现修改自身内存,类在实现方法时不用加mutating
,且用在类中会报错,枚举、结构体才需要加mutating
- 只有协议中方法加上
mutating
,才能在后续结构体实现协议时,在方法前加上mutating
,在方法内修改成员属性
- 只有将协议中的实例方法标记为
-
协议中的初始化
- 协议中可以定义初始化器
init
,非final
类中实现时必须加上required
(当非final
类实现了协议,其子类也必然需要实现协议,为了约束子类必须实现协议中的init
,则非final
类需要在init
前使用required
) - 如果协议实现的初始化器,刚好是重写了父类的指定初始化器,那么这个初始化器必须同时加
required
、override
protocol Livable { init (age: Int) } class Person { init (age: Int) {} } class Student { required override init (age: Int) { super.init(age: age) } }
- 协议中定义的
init?
、init!
可以用init
、init?
、init!
去实现
协议中定义的init
、可以用init
、init!
去实现
- 协议中可以定义初始化器
-
协议中的继承
一个协议可以继承其他协议 -
协议组合
可以包含1个类类型
协议组合只能包含1个类类型(只能是类类型,不能为结构体等),且最多只有1个
protocol Livable { }
protocol Runner { }
class Person { }
//接收Person或者子类的实例
func fn0(obj: Person) {}
//接收遵守Livable协议的实例
func fn1(obj: Livable) {}
//接收同时遵守Livable、Runner协议的实例
func fn2(obj: Livable & Runner) {}
//接收同时遵守Livable、Runner协议,并且是Person或者其子类的实例
func fn3(obj: Livable & Runner & Person) {}
//接收同时遵守Livable、Runner协议,并且是Person或者其子类的实例,也可以这么写
typealias RealPerson = Livable & Runner & Person
func fn3(obj: RealPerson) {}
-
CaseIterable
协议
让枚举遵守CaseIterable
,可以实现枚举值遍历
enum Season : CaseIterable {
case spring, summer, autumn, winter
}
let season = Season.allCases
print (season.count) // 4
CustomStringConvertible
与CustomDebugStringConvertible
协议
1.遵守CustomStringConvertible
与CustomDebugStringConvertible
可以自定义实例的打印字符串
2.print
调用的是CustomStringConvertible
协议的description
debugPrint
、po
调用的是CustomDebugStringConvertible
协议的debugDescription
Any
与AnyObject
Any
:可以代表任意类型(枚举、结构体、类、函数类型)
AnyObject
:可以代表任意类
类型(在协议后面写上: AnyObject
代表只有类能遵守这个协议)
另外还有一个AnyClass
,相当于AnyObject.type
-
其他一些杂项
Swift中定义一个数组有两种方式:
var ary = Array<Int>()
,或者[Int]()
定义一个字典:
var dic = Dictionary<Int,Int>()
,或者[Int: Int]()
is
用来判断是否为某种类型,as
用来做强制类型转换.self
是一个元类型(metadata
)的指针,metadata
存放着类型相关信息
.self
属于.type
类型,X.type
类型是一个指针变量类型,里面存放着X
的前八个字节的内存地址值Type
函数是直接取出对应变量的前8个字节赋值给前面的变量.实际上是没有分配栈空间,type(of:X)
实际是拿到X.self
//元类型调用的初始化器必须是required修饰的
class Animal { required init () { }}
class Cat: Animal { }
class Dog: Animal { }
func create(_ clses: [Animal.Type]) -> [Animal] {
var arr = [Animal]()
for cls in clses {
arr.append(cls.init())
}
return arr
}
print (create([Cat.self,Dog.self]))
- swift还有个隐藏的基类:
Swift._SwiftObject
-
Self
代表当前类型,一般用作返回值类型,限定返回值跟方法调用者必须是同一类型(也可以作为参数类型),但如果Self
用在类中,要求返回时调用的初始化是required
的
protocol Runner {
func test() -> Self
}
class Person : Runner {
required init() { }
func test() -> Self {
type(of: self).init()
}
}
//以下4句代码本质是一样的
var p0 = Person()
var p1 = Person.self()
var p2 = Person.init()
var p3 = Person.self.init()
-
X
与X.self
的区别
若只想表达元类型,则需要使用X.self
15.错误处理
-
自定义错误
想要自定义错误信息需要:
1.在函数声明里加上throws
2.函数体内抛出一个遵守了Error
协议的实例对象(可以是类,枚举,结构体)
class MyError : Error { }
func divide (_ num1:Int, _ num2:Int) throws -> Int {
if num2 == 0 {
throw MyError()
}
return num1 / num2
}
-
错误处理
- 通过
do-catch
捕捉Error
- 不捕捉
Error
,在当前函数增加throws
声明,Error
将自动抛给上层函数,如果最顶层函数(main
函数)依然没有捕捉Error
,那么程序将终止 - 可以使用
do-catch
来处理异常,当try
中的部分为异常时,会停止执行try
所在作用域结束之前的全部代码,去catch
中匹配对应异常,以上的结果会跳过print(“3”)
这个过程 - 当函数声明有使用
throws
时,在函数调用时需要使用try
进行尝试调用,若函数调用不在最外层,则需要进行do-catch
异常处理,或在函数声明中加上throws
再往上层函数抛出异常
- 通过
func test () {
print("1")
do {
print("2")
print(try divide(200,0))
print("3")
} catch let SomeError.illegalArg(msg) {
print("参数异常")
} catch let SomeError.illegalArg(msg) {
print("数组越界")
}
print("4")
}
test()
- 可以使用
try?
、try!
调用可能会抛出Error
的函数,这样就不用去处理Error
func test () {
print("1")
print("2")
print(try? divide(200,10)) // Optional(20),Int?
print(try? divide(200,0)) // nil
print(try! divide(200,10)) // 20,Int
print("3")
}
test()
var a = try? divide(20,0)
var b = Int?
do {
b = try divide(20,0)
} catch {
b = nil
}
//a与b是等价的
6.rethrows
声明:函数本身不会抛出错误,但调用闭包参数抛出错误,那么它将会向上抛
func exec (_ fn: (Int,Int) throws ->Int,_ num1: Int, _ num2 : Int ) rethrows {
print (try fn(num1,num2))
}
try exec(divide, 20, 0)
rethrows
仅代表抛出的异常是因为传入的参数导致异常,而与函数(闭包)内部的逻辑导致异常无关
throws
代表抛出的异常是因为函数(闭包)内部的逻辑可能导致的异常
其他二者并无本质区别,作用其实差不多
-
defer
语句:用来定义以任何方式(抛错误、return
等)离开代码块前必须要执行的代码
defer
语句将延迟至当前作用域结束之前执行
func open (_ filename: String) -> Int {
print("open")
return
}
func close (_ file: Int) {
print("close")
}
func processFile (_ filename: String) throws {
let file = open(filename)
defer {
close (file)
}
try divide (20,0)
// close将会在这里调用
}
defer
语句的执行顺序与定义顺序相反
func fn1 () {}
func fn2 () {}
func fn3 () {
defer { fn1() }
defer { fn2() }
}
fn3()
//执行顺序为fn2,fn1
- 断言
- 自定义错误进行解决(
throws
->try
),是会被do-catch
捕获的,而断言则不会,断言时,只会抛出错误,并不会被捕捉到 - 很多编程语言都有断言机制,不符合指定条件就弹出运行时错误,常用于调试(Debug)阶段的条件判断
- 默认情况下,swift的断言只会在Debug模式下生效,Release模式下会忽略
func divide (_ v1:Int, _ v2:Int) {
assert(v2 != 0, "除数不能为0")
return v1 / v2
}
divide (20,0)
9.fatalError
函数
如果遇到严重问题,希望结束程序运行时,可以直接使用fatalError
函数抛出错误(这里是无法通过do-catch
捕捉的错误),使用了fatalError
函数,就不需要再写return
func test (_ num : Int) {
if num > 0 {
return 1
}
fatalError("num不能小于0")
}
在某些不得不实现,但不希望别人调用的方法,可以考虑内部使用fatalError
函数
class Person { required init() {} }
class Student : Person {
required init() { fatalError("不要使用这个初始化!")}
init (score: Int) {}
}
16.泛型
- 泛型的作用: 泛型可以将类型参数化,提高代码复用率,减少代码量
- 泛型的定义:
func swapValue<T> (_ a: inout T, _ b: inout T) {
(a,b) = (b,a)
}
// 同理,泛型也可以写多个
func swapValue<T,T1,T2,T3> (_ a: inout T, _ b: inout T) {
(a,b) = (b,a)
}
//当需要将泛型赋值给一个函数时,只需要明确函数的具体类型即可
var fn: (inout Int, inout Int) -> () = swapValue
fn(&1,&2)
为类时,进行类对象创建,由于需要明确具体的类型,但又无法通过初始化器等方式明确具体类型,则需要在创建时明确类型,即加<类型名>
为函数时,因为函数在调用时就知道内部泛型是什么类型,所以不需要明确类型
class stack<E> {
var elements = [E]()
func pop() ->E {
elements.removeLast()
}
func top() ->E {
elements.last!
}
}
泛型的继承:
class stack<E> {
var elements = [E]()
func pop() ->E {
elements.removeLast()
}
func top() ->E {
elements.last!
}
}
class subStack<E> : Stack<E> { }
枚举中的泛型:
enum Score <T> {
case point(T)
case grade(String)
}
let score3 = Score<Int>.grade("A")
枚举中,就算没有用到泛型,如上,没有使用到point,但当为score3分配内存时,还是需要知道泛型T的类型的,所以需要如上写法
- 从本质上来说,泛型的实现是通过类型的元类型实现(
metadata
),而并非实现多个函数的方式
associatedtype
关键字
以上定义泛型的方式成为泛型参数,但泛型参数只能用在类,结构体,枚举中,不能用在协议中,协议想使用泛型需要使用associatedtype
(关联类型)
- 作用:给协议中用到的类型定义一个占位名称(协议中可以拥有多个关联类型)
protocol Stackable {
associatedtype Element // 关联类型
mutating func push(_ element: Element)
mutating func pop() -> Element
}
class StringStack : Stackable {
typealias Element = String //可不写
//这一步即明确遵守协议的类型(有关联类型,在实现协议时都需要指明类型的名称)
var elements = [String]()
func push(_ element: String) { elements.append(element)}
func pop()-> String { elements.removeLast()}
}
var ss = StringStack()
ss.push("aaa")
ss.pop()
//可以通过where对泛型进行进一步的约束,就是一个有条件约束的泛型
func equal<S1: Stackable, S2: Stackable>(_ s1: S1, _ s2:S2) -> Bool where S1.Element == S2.Element,S1.Element : Hashable {
return false
}
不透明类型
-
定义: 使用
some
修饰的那一个整体-
some
关键字: 返回值只能有一种返回类型,除了用在返回值类型上,一般还可以用在属性类型上
-
- 作用: 当只想返回遵守某个协议的对象,但是又不希望外部知道对象的真实类型,只想暴露协议的接口,则可以使用不透明类型
17.关于String与Array
关于String
1.内存地址从低
到高
:
代码区--->常量区--->全局区(数据段)--->堆空间--->栈空间--->动态库
2.常通过对Mach-O
文件进行窥探,字符串是位于常量区
3.字符串的两种内存方式:
- 当字符串的长度<15位时:
var str = “0123456789”
汇编完内存为:0x3736353433323130 0xea0000000000003938
会类似oc
的TaggerPoint
一样,分配16个字节内存,第一位存储类型,多为e
,第二位为长度,后面则直接存储字符串的内容 - 当字符串长度>15位时:
var str = “0123456789ABCDEF”
分配的16位内存中,后8位为字符串的真实地址(通过后8位字符地址- 一个固定的长度(或者+0x20
),即为真实地址),前8位为字符串的长度,内容存放在__TEXT.cstring
中(常量区)
4.append
操作
- 当
append
后的字符串长度<15位时,添加完字符串依然存放在字符串变量的内存中 - 当
append
后的字符串长度>15位时,会开辟堆空间进行存储
5.符号的延迟绑定时通过dyld_stub_binder
完成
6.注:jmpq *0xb31(%rip)
格式的汇编指令,占用6个字节
18.高级运算符
溢出运算符 : &+,&-,&*
溢出的结果依然在取值的范围内,实际上是一个循环取值
重载运算符 : 类、结构体、枚举可以为现有的运算符提供自定义的实现,这个操作叫做运算符重载
1.类似于函数重载,当运算符重载写在结构体中时,需要令重载函数修饰为static
2.在函数定义前加上关键字prefix
(前缀函数)或者关键字postfix
(后缀函数)
3.要想得知2个实例是否等价,一般做法是遵守Equatable
协议,重载==
运算符,与此同时,等价于重载了!=
运算符
enum Answer: Equatable {
case wrong(Int)
case right
}
var s1 = Answer.wrong(10)
var s2 = Answer.wrong(10)
print(s1 == s2) //结果为true
4.Swift为以下类型提供默认的Equatable
实现
- 没有关联类型的枚举
- 只拥有遵守
Equatable
协议关联类型的枚举 - 只拥有遵守
Equatable
协议存储属性的结构体
5.引用类型比较存储的地址值是否相等(是否引用着同一个对象),使用恒等运算符===
、!==
6.要想比较2个实例的大小(Comparable
)
- 遵守
Comparable
协议 - 重载相应的运算符
struct Student: Comparable {
var age: Int
var socre: Int
init(score: Int, age: Int) {
self.socre = score
self.age = age
}
static func < (lhs: Student, rhs: Student) -> Bool {
(lhs.socre < rhs.socre) || (lhs.socre == rhs.socre && lhs.age < rhs.age)
}
static func > (lhs: Student, rhs: Student) -> Bool {
(lhs.socre > rhs.socre) || (lhs.socre == rhs.socre && lhs.age > rhs.age)
}
static func <= (lhs: Student, rhs: Student) -> Bool {
!(lhs > rhs)
}
static func >= (lhs: Student, rhs: Student) -> Bool {
!(lhs < rhs)
}
}
-
自定义运算符
在全局作用域使用operator
进行声明
// MARK - 定义
prefix operator +++ //前缀运算符
postfix operator --- // 后缀运算符
infix operator +- : PlusMinusPrecedence // 中缀运算符 : 优先级组
// MARK - 优先级组
precedencegroup PlusMinusPrecedence {
associativity: none //结合性(left/right/none)
higherThan: AdditionPrecedence //比谁的优先级高
lowerThan: MultiplicationPrecedence //比谁的优先级低
assignment: true //代表在可选链操作中拥有跟赋值运算符一样的优先级
}
struct IntType {
var num1: Int
var num2: Int
init(num1:Int, num2: Int) {
self.num1 = num1
self.num2 = num2
}
static func +- (lhs: IntType, rhs: IntType) -> Int {
(lhs.num1 + rhs.num1) * (lhs.num2 - rhs.num2)
}
}
let opera1 = IntType(num1: 10, num2: 5)
let opera2 = IntType(num1: 5, num2: 1)
print(opera1 +- opera2)
19.扩展
Swift中的扩展,有点类似于OC中的分类
扩展可以为
枚举
、结构体
、类
、协议
添加新功能
可以添加方法、计算属性、下标、(便捷)初始化器、嵌套类型、协议等等-
协议不能办到的事情:
- 不能覆盖原有的功能
- 不能添加存储属性,不能向已有的属性添加属性观察者
- 不能添加父类
- 不能添加指定初始化器,不能添加反向初始化器
1.扩展是不允许影响原来类型的内存结构,所以不能添加存储属性
2.扩展是不允许添加父类,因为涉及到继承,也有可能影响原来类型的内存结构
注:便捷初始化器和指定初始化器只有在类中有具体的区分,在结构体中只有初始化器这一说,只能定义为init
方式
- 如果希望自定义初始化器的同时,编译器也能够生成默认初始化器,可以在扩展中编写自定义初始化器
- 类遵守协议实现的
required
初始化器,不能写在扩展中
- 如果一个类型已经实现了协议的所有要求,但是还没有声明它遵守了这个协议,可以通过扩展的方式让它遵守这个协议
protocol testProtocol {
func run ()
}
class Person {
func run (){
print("run")
}
}
extension Person: testProtocol{}
- Swift的扩展可以给协议扩展方法
- 扩展可以给协议提供默认实现,也间接实现
可选协议
的效果 - 扩展可以给协议扩充
协议中从未声明过的方法
protocol testProtocol { func run () } extension testProtocol { func run () { print("testProtocol_run") } func talk() { print("testProtocol_talk") } } class Person : testProtocol { func run() { print("Person_run") } func talk() { print("Person_talk") } } let p : testProtocol = Person() p.run() // Person_run p.talk() // testProtocol_talk talk方法出现这个结果是因为,在协议中是没有talk的声明的,因而不能保证今后实例中会包含这个talk的实现,就不会去实例中寻找,优先从扩展中寻找
- 扩展可以给协议提供默认实现,也间接实现
- 带条件的扩展
class Stack<E> {
var elements = [E]()
func push(_ element:E) {
elements.append(element)
}
func pop() {
elements.removeLast()
}
func size() -> Int {
return elements.count
}
}
//扩展中依然可以使用原类型中的泛型类型
extension Stack {
func top() -> E {
return elements.last!
}
}
//符合条件才扩展
extension Stack : Equatable where E: Equatable {
static func == (left: Stack, right: Stack) -> Bool {
left.elements == right.elements
}
}
20.访问控制
- 在访问控制这块,Swift提供了5个不同的访问级别(以下是从高到低排序,实体指被访问级别修饰的内容)
-
open
: 允许在定义实体的模块、其他模块中访问,允许其他模块进行继承、重写(open
只能用在类、类成员上) -
public
: 允许在定义实体的模块、其他模块中访问,不允许其他模块进行继承、重写 -
internal
: 只允许在定义实体的模块中访问,不允许在其他模块中访问 -
fileprivate
: 只允许在定义实体的源文件中访问 -
private
: 只允许在定义实体的封闭声明中访问
-
绝大部分实体默认都是internal
级别
系统类型默认是public
级别
- 一个实体不可以被更低访问级别的实体定义,比如:
- 变量/常量类型 >= 变量/常量
- 参数类型、返回值类型 >= 函数
- 父类 >= 子类
- 父协议 >= 子协议
- 原类型 >=
typealias
- 原始值类型、关联值类型 >= 枚举类型
- 定义类型A时使用到的其他类型 >= 类型A
fileprivate class Person { } internal var person : Person //会报错,因为不满足条件1
-
元组
- 元组类型的访问级别是所有成员类型最低的那个
internal struct Dog {} fileprivate struct Cat {} //(Dog,Cat)的访问级别是fileprivate fileprivate var data1: (Dog , Cat) private var data2: (Dog, Cat)
-
泛型
- 泛型类型的访问类型是
类型的访问级别
以及所有泛型类型参数的访问级别
中最低的那个
internal class Car {} fileprivate class Dog {} public class Person<T1,T2> {} //Person<Car,Dog>的访问级别是fileprivate fileprivate var p = Person<Car,Dog>()
- 泛型类型的访问类型是
- 类型的访问级别会影响成员(属性、方法、初始化器、下标)、嵌套类型的默认访问级别
- 一般情况下,类型为
private
或fileprivate
,那么成员/嵌套类型默认也是private
或fileprivate
- 一般情况下,类型为
internal
或public
,那么成员/嵌套类型默认是internal
- 一般情况下,类型为
- 直接在全局作用域下定义的
private
等价于fileprivate
-
getter/setter
-
getter
、setter
默认自动接收它们所属环境的访问级别 - 可以给
setter
单独设置一个比getter
更低的访问级别,用以限制写的权限
fileprivate(set) public var num = 10 class Person { private(set) var age = 0 fileprivate(set) public var weight: Int { set {} get { return 10 } } internal(set) public subscript(index: Int) -> Int { set {} get { index } } }
-
getter
是不能比setter
更低的访问级别的,只能有private(set)
,这个操作
-
-
初始化器
- 如果一个
public
类想在另一个模块调用编译生成的默认无参初始化器,必须显式提供public
的无参初始化器,因为public
类的默认初始化器是internal
级别 -
required
初始化器必须跟它所属类拥有相同的访问级别 - 如果结构体有
private/fileprivate
的存储实例属性,那么它的成员初始化器也是private/fileprivate
,否则默认就是internal
(其中有关required的部分替换为required
初始化器 >= 它的默认访问级别) - 若在结构体中,成员变量中至少有一个成员变量使用
private
修饰,则所有带成员变量的默认初始化器都不能使用,只能使用无参的默认初始化器
- 如果一个
-
枚举类型的
case
- 不能给
enum
的每个case
单独设置访问级别 - 每个
case
自动接收enum
的访问级别(public enum
定义的case
也是public
)
- 不能给
-
协议
- 协议中定义的要求自动接收协议的访问级别,不能单独设置访问级别(
public
协议定义的要求也是public
) - 协议实现的访问级别必须
>=
类型的访问级别,或者>=
协议的访问级别 - 即协议的访问控制与枚举一致
- 协议中定义的要求自动接收协议的访问级别,不能单独设置访问级别(
-
扩展
- 如果有显示设置扩展的访问级别,扩展添加的成员自动接收扩展的访问级别
- 如果没有显示设置扩展的访问级别,扩展添加的成员的默认访问级别,跟直接在类型中定义的成员一样
- 可以单独给扩展添加的成员设置访问级别
- 不能给用于遵守协议的扩展显示设置扩展的访问级别
- 在同一文件中的扩展,可以写成类似多个部分的类型声明
- 在原本的声明中声明一个私有成员,可以在同一文件的扩展中访问它
- 在扩展中声明一个私有成员,可以在同一文件的其他扩展中、原本声明中访问它
public class Person { private func run0 () {} private func eat0 () { run1() } } extension Person { private func run1() {} private func eat1() { run0() } } extension Person { private func eat2 (){ run1() } }
- 有关
var
或let
定义函数变量struct Person { var age: Int func run(_ v: Int) { print("func run",age,v) } static func run(_ v: Int) { print("static func run", v) } } //当存在类型方法和实例方法重名时,默认是调用类型方法 var fn = Person.run fn(20) //static func run 20 //可以显示声明调用实例方法类型来调用 //即需要调用一次person实例,才能获取到真正的对象,通过该对象去调用对应的对象方法 var fn1: (Person) -> (Int) -> () = Person.run fn1(Person(age: 20))(30) //func run 20 30
- 方法的重写与协议的访问控制类似,即重写的访问级别>= 类的访问级别,或者>=被重写方法的访问级别
- 父类的成员不能被成员作用域外定义的子类重写
class Person { private func run (){} } class Student: Person { override func run (){} //会报错 } //若将Student的类定义在Person中,则可以
21.内存管理
跟OC一样,Swift也是采取基于引用计数的ARC内存管理方案(针对堆空间)
1. Swift的ARC中有3种引用
- 强引用: 默认情况下,引用都是强引用
- 弱引用: 通过
weak
定义弱引用- 必须是可选类型
var
,因为实例销毁后,ARC会自动将弱引用设置为nil
- ARC自动给弱引用设置为
nil
时,不会触发属性观察器
- 必须是可选类型
- 无主引用: 通过
unowned
定义无主引用
1.不会产生强引用,实例销毁后仍然存储着实例的内存地址(类似于OC的unsafe_unretained
)
2.试图在实例销毁后访问无主引用,会产生运行时错误( 野指针 )
注: weak
和unowned
只能用在类实例上面
Swift中自动释放池的创建:
class Person {
var name : String
var age : Int
init(name: String, age: Int) {
self.name = name
self.age = age
}
func run() {
print("Person Run")
}
}
autoreleasepool {
let p = Person(name: "Jack", age: 20)
p.run()
}
-
循环引用问题
-
weak
、unowned
都能解决循环引用问题,unowned
要比weak
少一些性能消耗- 在生命周期中可能会变为
nil
的使用weak
- 初始化赋值后再也不会变为
nil
的使用unowned
- 在生命周期中可能会变为
2.闭包表达式默认会对用到的外层对象产生额外的强引用(对外层对象进行了
retain
操作)以下代码会产生循环引用,导致Person对象无法释放(看不到Person的deinit被调用) class Person { var fn: (() -> ())? func run() { print("Person Run") } deinit { print("deinit") } } func test() { let p = Person() p.fn = { p.run() } } test()
上述代码的解决循环引用的方式是: 通过添加捕获列表,解决循环引用问题
class Person { var fn: (() -> ())? func run() { print("Person Run") } deinit { print("deinit") } } func test() { let p = Person() p.fn = { [weak p] in p?.run() } } test()
若
Person
类函数定义有参数类型时,在捕获列表后紧跟()
,声明参数即可,即:class Person { var fn: ((Int) -> ())? func run() { print("Person Run") } deinit { print("deinit") } } func test() { let p = Person() p.fn = { [weak p](num: Int) in p?.run() } } test()
另外,也可以在捕获列表中定义新的变量,或者重新定义变量名等
p.fn = { [weak wp = p, unowned up = p, a = 10 + 20]() in wp?.run() }
如果想在定义闭包属性的同时引用
self
,这个闭包必须是lazy
的,因为在实例初始化完毕之后才能引用self
class Person { lazy var fn: (() -> ()) = { [weak self] in self?.run() } func run() { print("Person Run") } deinit { print("deinit") } } 上述的闭包fn内部如果用到了实例成员(属性、方法),编译器会强制要求明确写出self
如果
lazy
属性是闭包调用的结果,那么不用考虑循环引用的问题,因为闭包调用后,闭包的生命周期就结束了class Person { var age: Int = 0 lazy var getAge: Int = { self.age }() deinit { print("deinit") } }
其中,第一个
Person
需要加lazy
关键字,是因为在初始化时可知,self
需要在第二阶段才能使用,因而在定义fn
时直接使用self
是不允许的,因而需要在调用时懒加载初始化fn
,此时,可以等到使用fn
时再初始化
以上第一个与第二个的区别:第二个在闭包后面添加了()
,即直接直接执行了闭包,即本质上第二个Person
中,getAge
不是闭包表达式,而是一个int
类型,此时getAge
中可以不用显示写self
. -
-
逃逸闭包与非逃逸闭包
- 一般都是当做参数传递给函数
- 非逃逸闭包: 闭包调用发生在函数结束前,闭包调用在函数作用域内
- 逃逸闭包: 闭包有可能在函数结束后调用,闭包调用逃离了函数的作用域,需要通过
@escaping
声明
typealias Fn = () -> () //fn 是非逃逸闭包 func test1 (_ fn: Fn) { fn() } var gFn: Fn? // fn 是逃逸闭包 func test2 (_ fn: @escaping Fn) { gFn = fn } // fn 是逃逸闭包 func test3 (_ fn: @escaping Fn) { DispatchQueue.global().async { fn() } } 其中,GCD的异步函数是逃逸闭包
- 举例:在GCD函数中,若直接使用
self
,则无论如何都会都会执行到函数结束才会销毁self
,这就有可能产生例如空指针等问题,因而如果使用捕获列表,则可以进行判断,若此时self
已经被释放,则不会执行后续代码 - 逃逸闭包不可以捕获
inout
参数,非逃逸闭包则可以捕获inout
参数
-
内存访问冲突
- 内存访问冲突会在两个访问同时满足下列条件时发生:
- 至少一个是写入操作
- 它们访问的是同一块内存
- 它们的访问时间重叠(比如在同一个函数内)
//存在内存访问冲突 //Thread 1: Simultaneous accesses to 0x100008020, but modification requires exclusive access var step = 1 func increment (_ num: inout Int) { num += step } increment(&step)
解决方式:
var step = 1 func increment (_ num: inout Int) { num += step } var copyOfStep = step increment(©OfStep) step = copyOfStep
- 如果下面的条件可以满足,就说明重叠访问结构体的属性是安全的
- 你只访问实例存储属性,不是计算属性或者类属性
- 结构体是局部变量而非全局变量
- 结构体要么没有被闭包捕获要么只被非逃逸闭包捕获
- 以下代码如果是在全局区,则会报错
var tulpe = (health: 10, energy:20) balance(&tulpe.health, &tulpe.energy) var holly = Player(name: "Jack", health: 10, energy:20) balance(&holly.health, &holly.energy)
- 内存访问冲突会在两个访问同时满足下列条件时发生:
22.指针
- Swift中也有专门的指针类型,这些都被定性为"Unsafe"(不安全的),常见的有以下4种类型:
-
UnsafePointer<Pointee>
类似于const Pointee *
-
UnsafeMutablePointer<Pointee>
类似于Pointee *
-
UnsafeRawPointer
类似于const void
-
UnsafeMutableRawPointer
类似于void
-
- 相关操作(API)
-
pointee
: 取出指针里面的值
2.func test1 (_ ptr: UnsafeMutablePointer<Int>) { // int * ptr.pointee = 20 print(ptr.pointee) }
load
: 取出对应字节的值func test2 (_ ptr: UnsafeRawPointer) { // const void * var d = ptr.load(as: Int.self) print(d) 此时,d就是int类型的 }
-
storeBytes
: 存储对应字节的值
func test3 (_ ptr: UnsafeMutableRawPointer) { // void * ptr.storeBytes(of: 30, as: Int.self) }
- 数组的遍历操作
var arr = NSArray(objects: 11,22,33,44) for (idx,element) in arr.enumerated() { print((idx,element)) if idx == 3 { break } }
- 正常情况下,是无法将指针类型赋值给一个变量的,需要通过定义
withUnsafePointer
这个函数
指向某个变量的指针:var age = 20 var ptr = withUnsafePointer(to: &age) { $0 } 若要赋值的变量今后是可以修改的,需要使用withUnsafeMutablePointer这个函数
var age = 20 var ptr = withUnsafePointer(to: &age) { UnsafeRawPointer($0) } var ptr = withUnsafeMutablePointer(to: &age) { UnsafeMutableRawPointer( $0 ) }
withUnsafePointer
和withUnsafemMutablePointer
中闭包的返回类型就是该指针类型的返回类型,即上述age
是什么类型,该返回就是什么类型var p = Person(name: "Jack") var ptrP = withUnsafePointer(to: &p) { $0 } 此时,ptrP是指向Person对象p的指针 今后,ptrP.pointee等价于p
- 指向堆空间实例的指针:
此时,var ptr1 = UnsafeMutableRawPointer(bitPattern: 0x000000010000ef40)
ptr1
就是后面传的内存地址-
address
就是p
堆空间的地址值
class Person { var name: String init(name: String) { self.name = name } } var p = Person(name: "Jack") var ptr2 = withUnsafePointer(to: &p) {UnsafeRawPointer($0)} var address = ptr2.load(as: UInt.self)
-
ptr2
获取了person
堆空间的地址值
也可以直接使用class Person { var name: String init(name: String) { self.name = name } } var p = Person(name: "Jack") var ptr2 = withUnsafePointer(to: &p) {UnsafeRawPointer($0)} var address = ptr2.load(as: UInt.self) var ptr3 = UnsafeMutableRawPointer(bitPattern: address)
unsafeBitCast
实现var ptr4 = unsafeBitCast(p, to: UnsafeRawPointer.self)
- 有关指针的存、取、创建、销毁
//创建 var ptr = malloc(16) //存 ptr?.storeBytes(of: 11, as: Int.self) ptr?.storeBytes(of: 22, toByteOffset: 8, as: Int.self) //取 ptr?.load(as: Int.self) ptr?.load(fromByteOffset: 8, as: Int.self) //销毁 free(ptr)
- 只有带
mutable
的才能使用allocate
去调用内存
var ptr2 = UnsafeMutableRawPointer.allocate(byteCount: 16, alignment: 1) ptr2.deallocate()
-
advanced
函数,直接将指针偏移字节
上述直接将22存储到prt.advanced(by: 8).storeBytes(of: 22, as Int.self)
ptr
后面的8个字节var ptr = UnsafeMutablePointer<Int>.allocate(capacity: 3) ptr.initialize(to: 11) ptr.successor().initialize(to: 22) //successor为后继,即ptr.successor取的是ptr的后8个字节 ptr.successor().successor().initialize(to: 33) //如果是通过initialize初始化,则一定需要deinitialize方式,不然会有内存泄漏 ptr.deinitialize(count: 3) ptr.deallocate() 其中:ptr + 1与ptr[1] 与ptr.successor()都等价
- 指针类型转换
- 方式1:
因为ptr是rawPointer,所以ptr+8确实是+8个字节,而泛型的的指针,假如是int,则+1即+8个字节(1个int有8个字节) var ptr2 = UnsafeMutableRawPointer.allocate(byteCount: 16, alignment: 1) ptr2.assumingMemoryBound(to: Int.self).pointee = 11 (ptr2 + 8).assumingMemoryBound(to: Double.self).pointee = 22.0
- 方式2:
var ptr3 = unsafeBitCast(ptr, to: UnsafeMutablePointer<Int>.self)
unsafeBitCast
是忽略数据类型的强制转换,不会因为数据类型的变化而改变原来的内存数据,类似于C++ 中的reinterpret_cast
,相当于直接将二进制数据搬过去,只是数据类型发生了变化 -