站在汇编角度深入了解 Swift(二)

424 阅读4分钟

if-else

  • c 中的 do-whileswift 中变成了 repeat-while
  • if 后面只能跟 bool 类型

for

  • 闭区间运算符: m...n
  • 开区间运算符: m..<n
  • 单侧区间: 让区间朝一个方向尽可能的远: ...n
let range1: CloseRange<Int> = 1...3
let range2: Range<Int> = 1..<3
let range3: PartialRangeThrough<Int> = ...5

switch

  • case、default 后面不能写大括号
  • 默认不写 break,并不会贯穿,如果想贯穿可以利用 falthrough(也可以把两个条件写在一起,用逗号隔开)
  • 必须保证能处理所有情况
  • case、default 后面至少要有一条语句
  • 如果不想做任何事,加个 break 即可
  • 支持更多的类型,比如 string、enum、int...
  • 区间匹配/元组匹配

where

这个在 swift 中很常用,可以在扩展/类定义/函数泛型中约束类型。

// 这种会比 if 更加优雅
for i in 0 ..< 100 where i % 2 == 0 {

}
extension Array where Element == Int {
}

函数的定义

func sum() -> Void { }
  • 默认参数值
  • 可变参数
public func print(_ items: Any..., separator: String = " ", terminator: String = "\n")
  • 输入输出参数inout: 可以在函数内部修改外部实参内存中的值
    • 本质其实就是地址传递,跟 c 语言里面的一样的
    • 站在汇编角度深入了解 Swift(七) 里面还有补充一个 Copy In Copy Out的策略,而且这个外部实参应该有物理内存地址就行,不然你传一个立即数,他也不能改了立即数
var num =10
func fix(_ num: inout Int) {
    num = 20
}
fix(&num)

思考:为什么利用元组可以不利用中间变量就进行交换?

func swap_(_ v1: inout Int, _ v2: inout Int) {
    (v1, v2) = (v2, v1)
}

汇编分析

  1. 如何证明 inout 就是地址传递?
func test(a: inout Int) {
    a = 20
}
var a = 10
test(a: &a)
var b = a

汇编

swiftstudy`main:
...
leaq   0x1109(%rip), %rax        ; swiftstudy.a : Swift.Int
movq   $0xa, 0x10fc(%rip)        ; _dyld_private + 4
movq   %rax, %rdi
leaq   0x10da(%rip), %rdi        ; swiftstudy.a : Swift.Int // 在这里就可以看出来其实他传的是地址,如果不是 inout 这个其实直接是 movq
callq  0x100000f60               ; swiftstudy.test(a: inout Swift.Int) -> () at main.swift:11
leaq   -0x18(%rbp), %rdi
...

swiftstudy`test(a:):
->  0x100000f60 <+0>:  pushq  %rbp
    0x100000f61 <+1>:  movq   %rsp, %rbp
    0x100000f64 <+4>:  movq   $0x0, -0x8(%rbp)
    0x100000f6c <+12>: movq   %rdi, -0x8(%rbp)
    0x100000f70 <+16>: movq   $0x14, (%rdi)
    0x100000f77 <+23>: popq   %rbp
    0x100000f78 <+24>: retq   

函数重载

  • 规则
    • 函数名相同
    • 参数个数不同 || 参数类型不同 || 参数标签不同
func sum(v1: Int, v2: Int) -> Int
func sum(v1: Int, v2: Int, v3: Int) -> Int
func sum(v1: Double, v2: Double) -> Double
  • 注意点
    • 返回值类型与函数重载无关
    • 默认参数产生的二义性编译器不会报错
    • 可变参数、省略参数标签、函数重载一起使用产生二义性时,编译器有可能会报错
sum 这个函数有二义性,就是有歧意
error: ambiguous use of 'sum'

汇编

func test(c: Int) { }
func test(a: Int, b: Int = 10) { }
test(c: 11)
test(a: 12)
=================================
StudySwift`main:
...
callq  0x100000f60               ; StudySwift.test(c: Swift.Int) -> () at main.swift:11
->  0x100000f3c <+28>: callq  0x100000f80               ; default argument 1 of StudySwift.test(a: Swift.Int, b: Swift.Int) -> () at main.swift
...

StudySwift`default argument 1 of test(a:b:):
    0x100000f80 <+0>:  pushq  %rbp
    0x100000f81 <+1>:  movq   %rsp, %rbp
    0x100000f84 <+4>:  movl   $0xa, %eax
    0x100000f89 <+9>:  popq   %rbp
    0x100000f8a <+10>: retq  

内联函数(Inline Function)

  • 如果开启了编译器优化(Release模式默认会开启优化),编译器会自动将某些函数变为内联函数
Optimization Level 设置了 Optimiz for Speed [-O]
  • 调用函数都要做很多工作:调用前要先保存寄存器,并在返回时恢复,复制实参,程序还必须转向一个新位置执行
  • 其实就是将函数的调用展开成函数体,这样就减少了调用函数的开销
  • 哪些函数不会被自动内联?
    • 理论上不要内联超过十行的函数
    • 包含递归调用
    • 包含动态派发
  • swift 也提供了一些关键字来让我们自己来控制是否内联
    • @inline(never)
    • @inline(__always) 除了递归和动态派发的函数,就算函数体较长他也会内联
  • 思考一下内联函数和宏有什么区别?
    • 宏定义和内联函数的区别
    • 宏基本没啥优势,所以 swift 里面就不能定义宏这种东西
    • 内联函数是代码被插入到调用者代码处的函数。如同 #define 宏,内联函数通过避免被调用的开销来提高执行效率,尤其是它能够通过调用(“过程化集成”)被编译器优化。 宏定义不检查函数参数,返回值什么的,只是展开,相对来说,内联函数会检查参数类型,所以更安全。内联函数和宏很类似,而区别在于,宏是由预处理器对宏进行替代,而内联函数是通过编译器控制来实现的。而且内联函数是真正的函数,只是在需要用到的时候,内联函数像宏一样的展开,所以取消了函数的参数压栈,减少了调用的开销。你可以象调用函数一样来调用内联函数,而不必担心会产生于处理宏的一些问题。

函数类型(Function Type)

  • 可以把函数也看成是一种类型,这样子就可以把函数当作参数或者返加值,或者是属性 ...
    • 返回值是函数类型的函数,叫做高阶函数
    • 比如 snapkit
一个函数有函数名函数体参数返回值
func sum(_ a: Int, _ b: Int) -> Int { }
func p(f: (Int, Int) -> Int, a: Int, b: Int) { }
  • typealias
    • 用来给类型起别名
    typealias Void ()