Swift基础语法
swift是Apple在2014年6月WWDC发布的全新编程语言,中文名和LOGO是雨燕,Swift是由Chris Lattner之父主导开发的,Chris Lattner也是Clang编译器作者,LLVM项目的主要发起人,目前已从Apple离职了,先后跳槽到Tesla,Google,目前在Google Brain从事AI研究g
Swift 版本
经过5年时间的发展,从Swift1.x 发展到了Swift5.x版本,经历了多次重大改变,ABI终于稳定;
API(Application Programming Interface):应用程序编程接口
源代码和库之间的接口
ABI(Application Binary Interface):应用程序二进制接口
应用程序与操作系统之间的底层接口
涉及的内容有:目标文件格式,数据类型的大小、布局、对齐、函数调用约定等等
目前Swift完全开源,github链接,主要采用C++编写
Swift编译流程
编译流程图如下:
swift compileswiftc 存放在Xcode内部,路径是/Applications/Xcode/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/bin
Swift 命令行编译
生成语法树:swiftc -dump-ast main.swift
生成最简洁的SIL代码:swiftc -emit-sli main.swift
生成LLVM IR 代码:swift -emit-ir main.swift -o main.s
基本语法篇
用Xcode生成一个macOS 下的命令行项目,可以发现swift里面是没有编写main函数的,Swift将全局范围内的首句可执行代码作为程序入口,例如项目中生成的print("Hello World!"),在学习语法的过程中,可以通过创建Playground 项目,因为Playground可以快速预览代码效果,对学习语法比较有帮助
Command + shift + Enter:运行整个Playground
Shift + Enter:运行截止到某一行代码
Playground操作
Playground-View
Playground-View Playground-ImageViewPlayground-ViewController
Playground-controller常量
只能赋值一次,它的值不要求在编译时期确定,但使用之前必须赋值一次
正确示例 错误示例标识符
标识符(比如常量名,变量名,函数名)几乎可以使用任何字符,另外标识符不能以数字开头,不能包含空白字符,制表符,箭头特殊字符
常见数据类型
数据类型字面量
字面量类型转换
类型转换元组(Tuple)
元组流程控制
if-else
if-elseif 后面的条件可以省略小括号,但是条件后面的大括号不可以省略,并且类似>=这种操作运算符后面需要有空格,否则会报错,另外if 后面的条件只能是bool类型,否则会爆出'Int' is not convertible to 'bool'
for循环
for循环闭区间运算符:a...b,a <= 取值 <= b
半区间运算符:a..<b, a <= 取值 < b
for 与单侧区间的使用
单侧区间while 循环
whilerepeat-while 相当于其他开发原因中的do-while,另外上面的例子不使用num--,是因为从swift3开始,去除了自增(++),自减(--)运算符的操作,可能是因为编译器的不同,担心导致结果不一样
区间类型
区间类型switch
switch - break switch - no break switch {}从上面的图片展示结果可以看到,case,default后面不能写大括号{},并且默认可以不写break,并不会贯穿到后面的条件
swift中如果想在switch中实现贯穿效果,可以使用关键字fallthrough
fallthroughSwitch 注意点
switch 必需要保证能处理所有情况,case ,default 后面只要要有一条语句,如果不想做任何事情,加个break即可
default保留情况如果能保证已处理所有情况,也可以不必保留default
不需要保留default情况可以看到如果answer已经是前面确定过类型的变量的话,那么case是可以省略掉类型的
switch 复合条件
复合条件区间匹配,元组匹配
区间匹配,元组匹配可以使用下划线_忽略某个值
值绑定
值绑定where
where标签语句
标签语句函数
函数定义
函数如果整个函数体是一个单一表达式,那么函数会隐式返回这个表达式
func sum(v1: Int, v2:Int) -> Int{
v1 + v2
}
sum(v1:10,v2:20) // 30
返回元组:实现多返回值
元组返回多个值参数标签(Argument Label)
Argement Label默认参数值(Default Parameter Value)
Default Parameter Value可变参数(Variadic Parameter)
Variadic Parameter一个函数最多只能有一个可变参数,紧跟在可变参数后面的参数不能省略参数标签
func test(_ numbers:Int...,string:String,_ other:String) {}
test(10,20,30,string:"Jack","Rose")//参数string不能省略标签
Swift自带的print函数
print函数 print测试输入输出参数(In-Out Parameter)
In-Out Parameter可以用inout定义一个输入输出参数,可以在函数内部修改外部实参的值;
可变参数不能标记为inout,并且不能有默认值,参数只能传入可以被多次赋值的
函数重载
函数重载函数重载注意点:返回值类型与函数重载无关
测试另外默认参数值和函数重载一起使用产生二义性时,编译器并不会报错,在C++中会报错
二义性 测试内联函数(Inline Function)
如果在xcode中开启了编译器优化(Release 模式默认开启优化,debug可以手动更改),编译器会自动将某些函数变成内联函数,将函数调用展开成函数体,xcode开启编译器优化:
编译器优化开启那些函数不会被自动内联呢?
函数体比较长;包含递归调用;包含动态派发
@inline(never) func test(){print ("test") }//永远不会被内联,即使开启了编译器优化
@inline(__always) func test(){print ("test" }//开启编译器优化后,即使代码很长,也会被内联(递归调用函数,动态派发的函数除外)
在Release模式下,编译器已经开启优化,会自动决定哪些函数需要内联,因此没必要使用@inline
测试案例:
设置汇编调试 查看函数有没有被内联debug模式下编译器没有开启
优化debug模式下,编译器优化:
可以看到源码中的test()打断点没有进入,但是方法被执行了,从汇编代码里面可以看到print函数直接嵌在main函数里面了
函数体过长测试 汇编函数体比较长不会内联
递归测试 汇编递归调用不会触发内联
可以发现递归调用也不会内联
动态派发动态派发不会内联
函数类型(Function Type)
每一个函数都是有类型的,函数类型由形式参数类型,返回值类型组成
函数类型函数作为函数参数的使用例子
函数作为参数函数类型作为函数返回值使用例子
函数作为返回类型typealias
typealias枚举
基本用法
枚举基本用法关联值(Associated Values)
关联值原始值(Raw Values)
raw注意:原始值不占用枚举变量的内存
隐式原始值(Implicitly Assigned Raw Values)
隐式递归枚举
递归枚举MemoryLayout
MemoryLayout从上面的图中展示可以看到Password和Season占用的内存不一样,Password中占用32个字节,Season占用1个字节,为什么会这样呢?是因为Password是关联值相关,Season是原始值,关联值可以动态更改里面的值,原始值一开始就有固定值了,另外红色框中为什么会有33个字节,不是32个字节呢,按道理32个字节就已经可以存储值了,例如我现在赋值pwd(含有32个字节),这个时候赋值给other就可以了,但是会有这样一种情况出现:
32+1可选项
可选项,一般也叫可选类型,它允许将值设置为nil,在类型后面加个问号?就可以定义一个可选项:
可选项强制解包(Forced Unwrapping)
可选项是对其他类型的一层包装,可以将它理解为一个盒子,如果为nil,那么它就是一个空盒子,如果不为nil,那么盒子里装的是:被包装类型的数据,如果需要从可选项中取出被包装的数据,需要使用感叹号!进行强制解包,如果对值为nil的可选项进行强制解包,将会发生运行时错误
强制解包流程 非nil解包 nil解包判断可选项是否包含值
判断可选值可选项绑定(Optional Binding)
可以使用可选项绑定来判断可选项是否包含值,如果包含就自动解包,把值赋给一个临时的常量(let)或者变量(var),并返回true,否则返回false
可选项绑定 可选项判断while 循环中使用可选项绑定
while 循环中使用可选项绑定空盒运算符 ??(Nil-Coalescing Operator)
空合并运算符a ?? b
a 是可选项 b是可选项或者不是可选项,并且b跟a的存储类型必须相同,如果a不为nil,就返回a,如果a为nil,就返回b,如果b不是可选项,返回a时会自动解包
??if 语句使用
if 语句guard语句
guard 条件 else{
//do something
退出当前作用域
return break,continue,throw error
}
当guard语句的条件为false时,就会执行大括号里面的代码,当guard语句的条件为true时,就会跳过guard语句,guard语句特别适合用来”提前退出“
guard隐式解包(Implicitly Unwrapped Optional)
在某些情况下,可选项一旦被设定值之后,就会一直拥有者,另外也不必每次访问的时候进行解包,并且可以在类型后面加个感叹号!定义一个隐式解包的可选项
隐式解包字符串插值
可选项在字符串插值或者直接打印时,编译器会发出警告
字符串插值多重可选项
多重可选项1上面的num2 = num3 ture
lldb 调试图1 多重可选项 lldb 调试2例子解说:
例子解说num2 可以通过上面的多重可选项里面发现??的盒子不等于空,所以这个num2 ?? 1 是返回num2的,因为?? 1 中的1是非可选项,所以会自动解包一次,也就是说num2 ?? 1 返回的相当于是num2 解包一次之后的Int ? 类型,也就是上面num2中的绿色盒子,然后再将解包一次之后的num2 ?? 2 ,这个时候num2进行第二次解包,解包之后发现num2 = nil,所以返回后面的2,num3的原理类似
结构体
在swift标准库中,绝大多数的公开类型都是结构体,而枚举和类只占很小一部分,比如Bool,Int,Double,String,Array,Dictionary 等常见类型都是结构体
结构体所有结构体都有一个编译器自动生成的初始化器(initializer,初始化方法,构造函数)
结构体的初始化器
编译器会根据情况,可能会为结构体生成多个初始化器,宗旨是:保证所有成员都有初始值
初始化 初始化 初始化 初始化自定义初始化器
一旦在定义结构体的时候自定义了初始化器,编译器就不会再帮它自动生成其他初始化器
自定义初始化窥探初始化器的本质
初始化本质结构体的内存结构
内存结构类
类的定义和结构体类似,但编译没有为类自动生成可以传入成员值的初始化器
类初始化如果类的所有成员在定义的时候指定了初始值,编译器会为类生成无参构造
类初始化结构体与类的本质区别
结构体是值类型(枚举也是值类型),类是引用类型(指针类型)
内存布局 证明例子但是你如果通过MemoryLayout打印内存内存的话:你会发现Size只占用8个字节内存,Point占用16个字节
memoryLayout其实Size返回8是正常的,因为8是指的是一个size指针的大小,一个指针的大小的确是8个字节,如果想知道Size对象在初始化的时候分配多少内存,可以用下面的方法:
size 内存占用字节对象的堆空间申请过程
在Swift,创建类的实例对象,要向堆空间申请内存,大概流程如下
Class._allocating_init()->libswiftCore.dylib_swift_allocObject->libswiftCore.dylib:swift_slowAlloc->libsystem_malloc.dylib:malloc,在Mac,iOS中的malloc的函数分配的内存大小总是16的倍数,通过class_getInstanceSize可以得知:类的对象至少需要占用多少内存
值类型
操作例子1:
值类型例子1 值类型例子布局1操作例子2:
值类型操作2 内存布局汇编证明
源码测试源码图
汇编调试1汇编断点
从8,9行中可以知道将结果赋值为edi 和esi寄存器,实际上也就是rdi和rsi的寄存器的低位,si进入到init初始化方法中:
init 调试init的4,5行可以看到将rdi和rsi的值又赋值给rad和rdx,也就是将上面的edi和esi的值赋值
finishlldb输入finish回到函数,从前面可以知道rax == 10,rdx == 20,然后11,12,13,14行可以看到rax和rdx赋值两次,然后15.16行可以看到11,22 给了rbp-0x12 和rpb - 0x18 ,也就是13,14行的地址,由此可见这几行汇编代码对应的源代码是:
源码代码对应图这里就验证了我们值类型例子布局1 图的正确性
引用类型
例子1:
引用测试代码1 内存布局1例子2:
例子2值类型,引用类型的let
let可以看到let 定义的结构体或者类都不能更改,但是类可以更改里面的属性,如果还是没有搞清楚类可以更改,结构体不能更改的话,请看回前面的内存布局相关吧
嵌套类型
嵌套类型闭包
闭包定义:一个函数和它所捕捉的变量\常量环境组合起来,称为闭包;
一般之定义在函数内部的函数,一般捕捉的是最外层函数的局部变量\常量,例如下面所示:
闭包闭包表达式(Closure Expression)
在Swift中,可以通过func定义一个函数,也可以通过闭包表达式定义一个函数
闭包表达式闭包表达式的简写
表达式简写尾随闭包
如果将一个很长的闭包表达式作为函数的最优一个实参,使用尾随闭包增强函数的可读性,尾随闭包是一个被书写在函数调用括号外面(后面)的闭包表达式
尾随闭包如果闭包表达式是函数的唯一实参,而且使用了尾随闭包的语法,那就不需要在函数后面写圆括号
闭包-数组的排序
数组排序自动闭包
自动闭包@autoclosure 会自动将20封装成闭包{ 20 },
@autoclosure 只支持() -> T 格式的参数
@autorelease并非只支持最后一个参数
空合并运算符 ?? 使用了@autorelease 技术
有@autoclosure,无@autoclosure,构成了函数重载
属性
在Swift中跟实例相关的属性可分为2大类
存储属性(Stored Property)
类似成员变量这个概念;存储在实例的内存中,结构体、类可以定义存储属性,枚举不可以定义存储属性
计算属性(Computed Property)
本质就是方法(函数);不占用实例内存,枚举、结构体、类都可以定义计算属性
示例图:
属性示例图从上图中可以看到Int本身就已经占用了8个字节,但是Circle又总共是占用了8个字节而已,这就证明了计算属性不占用内存,现在我们从汇编角度来证明一下,radius内存是存储在实例对象中,然后计算属性是不占用内存的:
测试源代码:(证明存储属性存储在实体对象的内存中)
源代码测试汇编断点图:
11赋值图可以看到16行将一个11赋值给了一个全局变量,而在调试代码当中只有c是全局变量,所以可以证明了存储属性radius是存储在对象实体当中的;现在我们再将断点打到c.diameter = 12 这个位置:
调试断点图 汇编图可以看到c.diameter = 12 其实是调用了setter方法,再更改一下调试代码:
调试代码 断点调试可以看到var d = c.diameter 代码本质调用的是getter方法,这也就解释了为什么Circle只占用8个字节
存储属性
在创建类或结构体的实例时,必须为所有的存储属性设置一个合适的初始值,可以在初始化器里为存储属性设置一个初始值,也可以分配一个默认的属性值作为属性定义的一部分;
计算属性
set传入的新值默认叫做newValue,也可以自定义,例如下面:
计算属性上面中定义的计算属性是可读可写的,只有get,没有set的计算属性是只读的,定义计算属性只能用var,不能用let,let代表是常量一成不变的,计算属性的值是可能发生变化的
枚举rawValue原理
枚举原始值rawValue的本质是:只读计算机属性
测试代码 汇编图 rawvalue赋值图可以看到rawValue不能赋值,并且本质就是调用getter方法
延迟存储属性(Lazy Stored Property)
lazy no lazylazy属性必需是var,不能是let,let必需在实例化的初始化方法完成之前就拥有值,如果多线程同时第一次访问lazy属性,无法保证属性只被初始化1次
延迟存储属性注意点
当结构体包含一个延迟存储属性时,只要var才能访问延迟存储属性,因为延迟属性初始化需要改变结构体的内存
延迟存储属性注意点可以看到访问p.z 会报错,因为let p就已经说明这个p是不能改动内存的了,而p.z 执行之后会马上改变内存,所以是不允许的,因此会报错
属性观察器(Property Observer)
属性观察值willSet 会传递新值,默认叫newValue,didSet会传递旧值,默认叫oldValue,在初始化器中设置属性值不会触发willSet和didSet,在属性定义时设置初始值也不会触发willSet和didSet
全局变量&局部变量
属性观察器,计算属性的功能,同样可以应用在全局变量,局部变量身上
全局变量局部变量类型属性(Type Property)
严格来说属性可以分为:
实例属性(Instance Property):只能通过实例去访问
存储实例属性(Stored Instance Property):存储在实例的内存中,每个实例都有一份
计算实例属性(Computed Instance Property)
类型属性(Type Property):只能通过类型去访问
存储类型属性(Stored Type Property):整个程序运行过程中,就只有1份内存(类似全局变量)
存储类型默认就是lazy,会在第一次使用的时候爱初始化,就算被多个线程同时访问,保证只会初始化一次
计算类型属性(Computed Type Property)
可以通过static定义类型属性,如果是类,也可以通过class;
类型属性细节
不同于存储实例属性,你必需给存储类型属性设定初始值,因为类型没有像实例那样的init初始化器来初始化存储属性
村粗类型默认就是lazy,会在第一次使用的时候爱初始化,就算被多个线程同时访问,保证只会初始化一次
存储类型属性可以是let
inout的本质
源码测试 汇编测试可以看到是直接将age的地址值传递进去达到修改的目的,现在再看看计算属性或者设置了属性观察器的情况
测试代码测试计算属性:
计算属性测试 计算属性汇编可以看到在调用test方法会先调用get方法,调用test方法之后会调用setter方法,并且这里和之前的普通变量传递进去地址处理方式不一样,这里是先将getter方法的返回值通过rax传到rbp的地址当中,然后再传递给rdi(参数接收),然后通过test方法更改值为20,之后再通过27行的指令取出-0x28(%rpb)的值(20)传递给rdi,这个rdi再传递进去setter方法,从而更改这个变量值。另外带有属性观察器的处理流程大同小异哈
inout的本质总结
如果实参有物理内存地址,且没有设置属性观察器
直接将实参的内存地址传入参数(实参进行引用传递)
如果实参是计算属性或者设置了属性观察值
采取了Copy In Copy Out的做法,调用该函数时,先复制实参的值,产生一个局部变量副本,然后将这个副本的内存地址传入函数(副本进行引用传递),在函数内部可以修改副本的值,函数返回后,再将副本的值覆盖实参的值
换句话说inout的本质就是引用传递
方法(Method)
枚举,结构体,类都可以定义实例方法,类型方法
实例方法(Instance Method):通过实例对象调用
类型方法(Type Method):通过类型调用,用static或者class关键字定义
方法定义self:在实例方法中代表实例对象,在类型方法中代表类型
在类型方法static func getCount中,count等价于self.count,Car.self.count,Car.count
mutating
结构体和枚举是值类型,默认情况下,值类型的属性不能被自身的实例方法修改
在func 关键字前加mutating可以允许这种修改行为
mutating@discardableResult
discardableResult下标
下标subscript中定义的返回值类型决定了
get方法的返回值类型
set方法中newValue的类型
subscript可以接受多个参数,并且类型任意
下标的细节
下标细节1 下标细节2subscript可以没有set方法,但必须要有get方法,如果只有get方法,可以省略get,并且可以设置参数标签,下标可以是类型方法
结构体,类作为返回值对比
类 结构体结构体需要有set方法才可以赋值,因为结构体是值类型,不能直接更改里面的内容,class是引用类型,可以直接修改
接收多个参数的下标
多参数下标继承(Inheritance)
值类型(枚举结构体)不支持继承,只有类支持继承
没有父类的类,称为基类
swift没有像OC,Java那样的规定,任何类最终都要继承某个基类
子类可重写父类的下标,方法,属性,重写必需加上override关键字
内存结构
子类会继承父类的存储变量,并且内存中先存储父类的元素,再存储子类的元素,并且分配的内存都是16的倍数
重写类型方法、下标
重写属性
子类可以将父类的属性(存储,计算)重写为计算属性
子类不可以将父类属性重写为存储属性
只能重写var属性,不能重写let属性
重写时,属性名,类型要一致
子类重写的属性权限不能小于父类属性的权限,如果父类属性是只读,那么子类重写后的属性也可以是只读的,也可以是读写的,如果父类属性是可读写的,那么子类重写后的属性也必需是可读写的
重写实例属性