Swift基础语法

作者: coder_feng | 来源:发表于2019-07-14 12:47 被阅读0次

    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属性

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

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

    重写实例属性

    其他补充

    窥探内存细节的小工具

    相关文章

      网友评论

        本文标题:Swift基础语法

        本文链接:https://www.haomeiwen.com/subject/vzuncctx.html