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 compile

swiftc 存放在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-ImageView

Playground-ViewController

Playground-controller

常量

只能赋值一次,它的值不要求在编译时期确定,但使用之前必须赋值一次

正确示例
错误示例

标识符

标识符(比如常量名,变量名,函数名)几乎可以使用任何字符,另外标识符不能以数字开头,不能包含空白字符,制表符,箭头特殊字符

常见数据类型


数据类型


字面量

字面量

类型转换

类型转换

元组(Tuple)

元组

流程控制

if-else

if-else

if 后面的条件可以省略小括号,但是条件后面的大括号不可以省略,并且类似>=这种操作运算符后面需要有空格,否则会报错,另外if 后面的条件只能是bool类型,否则会爆出'Int' is not convertible to 'bool'

for循环

for循环

闭区间运算符:a...b,a <= 取值 <= b

半区间运算符:a..<b, a <= 取值 < b

for 与单侧区间的使用

单侧区间

while 循环

while

repeat-while 相当于其他开发原因中的do-while,另外上面的例子不使用num--,是因为从swift3开始,去除了自增(++),自减(--)运算符的操作,可能是因为编译器的不同,担心导致结果不一样

区间类型

区间类型

switch

switch - break
switch - no break
switch {}

从上面的图片展示结果可以看到,case,default后面不能写大括号{},并且默认可以不写break,并不会贯穿到后面的条件

swift中如果想在switch中实现贯穿效果,可以使用关键字fallthrough

fallthrough

Switch 注意点

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的值赋值

finish

lldb输入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 lazy

lazy属性必需是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
下标细节2

subscript可以没有set方法,但必须要有get方法,如果只有get方法,可以省略get,并且可以设置参数标签,下标可以是类型方法

结构体,类作为返回值对比

结构体

结构体需要有set方法才可以赋值,因为结构体是值类型,不能直接更改里面的内容,class是引用类型,可以直接修改

接收多个参数的下标

多参数下标

继承(Inheritance)

值类型(枚举结构体)不支持继承,只有类支持继承

没有父类的类,称为基类

swift没有像OC,Java那样的规定,任何类最终都要继承某个基类

子类可重写父类的下标,方法,属性,重写必需加上override关键字

内存结构

子类会继承父类的存储变量,并且内存中先存储父类的元素,再存储子类的元素,并且分配的内存都是16的倍数

重写类型方法、下标

重写属性

子类可以将父类的属性(存储,计算)重写为计算属性

子类不可以将父类属性重写为存储属性

只能重写var属性,不能重写let属性

重写时,属性名,类型要一致

子类重写的属性权限不能小于父类属性的权限,如果父类属性是只读,那么子类重写后的属性也可以是只读的,也可以是读写的,如果父类属性是可读写的,那么子类重写后的属性也必需是可读写的

重写实例属性

其他补充

窥探内存细节的小工具

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,013评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,205评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,370评论 0 342
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,168评论 1 278
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,153评论 5 371
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,954评论 1 283
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,271评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,916评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,382评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,877评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,989评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,624评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,209评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,199评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,418评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,401评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,700评论 2 345

推荐阅读更多精彩内容