Swift 面试题汇总

https://www.jianshu.com/p/bdaa49f9d1a4

1、Swift 比 Objective-C 有什么优势?

Swift 速度更快,运算性能更高。
Swift 语法简单易读,代码更少,更加清晰,易于维护。
Swift 更加安全,它是类型安全的语言
Swift 泛型,结构体,枚举都很强大。
Swift 是静态语言,有类型推断,OC 是动态语言
Swift 面向协议编程,OC 面向对象编程。
Swift 注重值类型,OC注重引用类型。
Swift 支持泛型,OC 只支持轻量泛型。
Swift 支持静态派发(效率高)、动态派发(函数表派发、消息派发),OC 支持动态派发(消息派发)。
Swift 支持函数式编程。
Swift 的协议不仅可以被类实现,也可以被 struct 和 enum 实现。
Swift 有元组类型、支持运算符重载。
Swift 支持命名空间。
Swift 支持默认参数。

2、struct 与 class 的区别

1、struct 是值类型,class 是引用类型。
值类型的变量直接包含它们的数据,对于值类型都有它们自己的数据副本,因此对一个变量操作不会影响另一个变量。值类型包括结构体 (数组和字典)、枚举、基本数据类型 (boolean、integer、float等)。
引用类型的变量存储对他们的数据引用,对一个变量操作可能影响另一个变量。
2、property 的初始化不同
class 在初始化时不能直接把 property 放在默认的 constructor 的参数里,而是需要自己创建一个带参数的 constructor。struct 可以把属性放在默认的 constructor 的参数里。
3、变量赋值方式不同
struct 是值拷贝,class 是引用拷贝。
4、immutable 变量
swift 的可变内容和不可变内容用 var 和 let 来甄别,如果初始为 let 的变量再去修改会发生编译错误。struct 遵循这一特性,class 不存在这样的问题。
5、mutating function
struct 和 class 的差別是 struct 的 function 要去改变 property 的值的时候要加上 mutating,而 class 不用。
6、继承
struct 不可以继承,class可以继承。
7、struct 比 class 更轻量
struct 分配在栈中,class分配在堆中。
8、struct 会自动生成需要的构造方法(constructor),哪个属性没有赋初始值就会生成以哪个属性为参数的构造方法。而 class 没有,要自己写。

//struct 
struct StructTest {
    var name:String
    var age:Int
}

//class
class ClassTest {
    var name = "XIAOMING"
    var age = 18
}

//class
class ClassTest {
    var name:String?
    var age:Int?
}
var structTest = StructTest(age: 66)
var classTest = ClassTest()

9、struct 的属性可以不赋初始值,而 class 的属性必须赋初始值或者设为可选类型(下面也是可以的),区别只是 struct 自动生成了带参数的 init 方法。

class ScanInfo: NSObject {
    var advertisementData: [String : Any]
    var rssi: NSNumber
    init(advertisementData: [String : Any], rssi: NSNumber) {
        self.advertisementData = advertisementData
        self.rssi = rssi
    }
}

10、class 中可以有单例对象属性,struct 中不能有(在 let 前加 static,struct 中也可以创建单例)

class ClassTest {
    let sharaedInstance = ClassTest()
    private init() {
        print("调用单利类")
    }
}
struct StructTest {
    static let sharaedInstance = StructTest()
    private init() {
        print("调用单利类")
    }
}

11、Struct 不能被序列化成 NSData 对象,原因是无法归解档。归解档的类必须遵守 NSCoding 协议,struct 不能遵守 NSCoding 协议。
12、当项目的代码是 Swift 和 Objective-C 混合开发时,会发现在 Objective-C 的代码里无法调用 Swift 的 Struct。因为要在 Objective-C 里调用 Swift 代码的话,对象需要继承于 NSObject。

3、Swift 为什么将 String、Array、Dictionary 设计成值类型?

1、值类型相比引用类型最大的优势在于内存使用的高效。值类型在栈上操作,引用类型在堆上操作。栈上的操作仅仅是单个指针的上下移动,而堆上的操作则牵涉到合并、移位、重新链接等。Swift 这样设计大幅减少了堆上的内存分配和回收的次数,同时写时复制又将值传递和复制的开销降到了最低。
2、String、Array、Dictionary 设计成值类型也是为了线程安全考虑。通过 Swift 的 let 设置,使得这些数据达到了真正意义上的“不变”,它也从根本上解决了多线程中内存访问和操作顺序的问题。
3、设计成值类型还可以提升 API 的灵活度。

4、swift 属性

1、计算属性和存储属性

存储型属性:即成员变量,存储在实例对象的内存中。
计算型属性:本质就是方法(函数),不占用实例对象的内存,不能用来存储值。

struct Circle {
    // 存储属性
    var radius: Double
//    //或者可以这么写
//    var radius: Double {
//        willSet {
//            print("在willSet中,radius = /(radius),newValue = /(newValue)")
//        }
//        
//        didSet {
//            print("在didSet中,radius = /(radius),oldValue = /(oldValue)")
//        }
//    }

    // 计算属性
    var diameter: Double {
        set {
            radius = newValue / 2
        }
        get {
            return radius * 2
        }
    }
}

注:
计算型属性特点:仅有 get 或者有 get+set 的属性是计算型属性。这里的 get+set 仅仅是用来作为其他属性的外部接口。

2、类型属性(Type Property)

属性可以分为:
实例属性(Instance Property):只能通过实例对象去访问。
存储实例属性(Stored Instance Property):存储在实例对象的内存中,每个实例对象都有1份。
计算实例属性(Computed Instance Property)

类型属性(Type Property):只能通过类型去访问。
存储类型属性(Stored Type Property):整个程序运行过程中,就只有1份内存(类似于全局变量)。
计算类型属性(Computed Type Property)

存储实例属性不同,存储类型属性必须要设定初始值,因为类型没有像实例对象那样的 init 初始化器来初始化存储属性。存储类型属性默认就是 lazy,会在第一次使用的时候才初始化,就算被多个线程同时访问,保证只会初始化一次。

3、重写属性

1、子类可以将父类的属性(存储、计算)重写为计算属性;不可以将父类属性重写为存储属性。
2、只能重写 var 属性,不能重写 let 属性。
3、重写时,属性名、类型要一致。
4、子类重写后的属性权限不能小于父类属性的权限。
-- 如果父类属性是只读的,那么子类重写后的属性可以是只读的、也可以是可读写的。
-- 如果父类属性是可读写的,那么子类重写后的属性也必须是可读写的。
5、被 class 修饰的计算类型属性,可以被子类重写;被 static 修饰的类型属性(存储、计算),不可以被子类重写。

4、extension 中能增加存储属性吗?

extension 中能增加计算属性不能增加存储属性。extension 是用来给存在的类型添加新行为的并不能改变类型或者接口本身。如果增加存储属性,需要额外的内存空间存储新的值。extension 是不能管理这样的任务的。

5、Swift 是面向对象还是函数式编程语言?

Swift 既是面向对象的,又是函数式的编程语言
说 Swift 是面向对象的语言,是因为 Swift 支持类的封装、继承和多态。
说 Swift 是函数式编程语言,是因为 Swift 支持 map、reduce、filter、flatmap 这类去除中间状态、数学函数式的方法,更加强调运算结果而不是中间过程。

6、什么是函数式编程?

面向对象编程:将要解决的问题抽象成一个类,通过给类定义属性和方法,让类帮助我们解决需要处理的问题(即命令式编程,给对象下一个个命令)。
函数式编程:数学意义上的函数,即映射关系(如:y = f(x),就是 y 和 x 的对应关系,可以理解为"像函数一样的编程")。它的主要思想是把运算过程尽量写成一系列嵌套的函数调用。
例:
数学表达式
(1 + 2) * 3 - 4
传统编程
var a = 1 + 2
var b = a * 3
var c = b - 4
函数式编程
var result = subtract(multiply(add(1,2), 3), 4)

函数式编程的特点
1、函数是"第一等公民"
函数和其他数据类型一样,可以作为参数、可以赋值给其他变量、可以作为返回值。
2、高阶函数
高阶函数:接受至少一个函数作为参数,返回的结果是一个函数。
3、柯里化
把一个多参数的函数,转换为单参数函数且这个函数的返回值也是一个函数。

例:
// 柯里化之前
function add(x, y) {
  return x + y;
}
add(1, 2) // 3

// 柯里化之后
function addX(y) {
  return function (x) {
    return x + y;
  };
}
addX(2)(1) // 3

4、没有"副作用"
不会产生运算以外的结果。
5、纯函数
纯函数编程和函数编程的区别:是否允许在函数内部执行一些非函数式的操作,同时这些操作是否会暴露给系统中的其他地方,也就是是否存在副作用。如果不存在副作用或者说可以不用在意这些副作用,那么就将其称为纯粹的函数式编程。
6、引用透明性
函数无论在何处何时调用,如果使用相同的输入总能得到相同的结果。这种不依赖外部变量或“状态”,只依赖输入参数的特性就被称为引用透明性。

函数式编程的好处
代码简洁,开发迅速;
接近自然语言,易于理解;
更方便的代码管理;
易于"并发编程";
代码的热升级。

7、Swift 的派发

1、OC 中的方法是动态派发(方法调用),Swift 中的方法分为静态派发和动态派发。
2、动态派发:运行时查找函数表,找到后再跳转到实现。动态派发仅仅多一个查表环节并不是他慢的原因,真正的原因是它阻止了编译器可以进行的内联等优化手段。
3、静态派发:编译器将函数地址直接编码在汇编中,调用的时候根据地址直接跳转到实现。
4、静态派发的特点:静态派发更高效,不仅因为静态派发免去了查表操作,更因为编译器进行内联等优化;静态派发的条件是方法内部的代码必须对编译器透明且在运行时不能被更改。
5、值类型对象的函数的调用方式是静态调用,即直接地址调用。调用函数指针,这个函数指针在编译、链接完成后就已经确定了,存放在代码段,而结构体内部并不存放方法。因此可以直接通过地址直接调用。
6、直接派发包含:所有值类型;协议和 class 的 extensions ;NSObject 的子类用 @nonobjc 或 final 修饰的函数;添加 static 关键字函数;添加 @inline 关键字的函数。
函数表派发:初始声明函数。
消息派发:dynamic 修饰的函数;dynamic 修饰的 extensions ;添加 @objc 关键字的函数。

8、闭包

1、闭包是自包含的函数代码块,可以在代码中被传递和使用

闭包可以捕获和存储其所在上下文中任意常量和变量的引用,被称为包裹常量和变量。 Swift 会为你管理在捕获过程中涉及到的所有内存操作。
1、闭包捕获值的本质是在堆区开辟内存然后存储其在上下文中捕获到的值。
2、修改值也是修改的堆空间的值。
3、闭包是一个引用类型。
4、闭包的底层结构是一个结构体。存储闭包的地址,加上捕获值的地址。
5、在捕获的值中,会对定义的变量和函数中的参数分开存储。
6、存储的时候内部会有一个 HeapObject 结构,用于管理内存,引用计数。
7、函数是特殊的闭包,只不过函数不捕获值,所以在闭包结构体中只存储函数地址,不存储指向捕获值的指针。

使用闭包有很多好处
1、可以利用上下文自动推断参数和返回值的类型。
2、隐式返回单表达式闭包,也就是单表达式的时候可以省略 return 关键字。
3、参数名称可以简写或者不写,使用 $0 表示第一个参数。
4、尾随闭包表达式。

2、逃逸闭包

当闭包作为函数的参数且可能在函数结束之后才被调用,即逃离了函数的作用域(例:网络请求)。
特点:必须添加 @escaping 关键字。

func methodCallback(closure: @escaping () -> Void) {
    DispatchQueue.main.asyncAfter(deadline: .now()+2) {
        closure()
    }
}

print("start at /(Date.now)")
methodCallback {
    print("callback at /(Date.now)")
}
3、尾随闭包

闭包作为方法的最后一个参数时,就是尾随闭包。

//尾随闭包,定义一个方法,最后一个参数是闭包
func methodWithClosure(closure:(_ info: String) -> Void) {
    closure("info from closure")
}

//普通的写法
methodWithClosure { info in
    print(info)
}

//这是尾随闭包的写法
methodWithClosure() {info in
    print(info)
}

//如果闭包内实现只有一行代码,可以这样简写(装逼)
methodWithClosure() { print($0) }
4、自动闭包

自动闭包是一种自动创建的闭包,用于包装传递给函数作为参数的表达式。这种闭包不接受任何参数,当它被调用的时候,会返回被包装在其中的表达式的值。这种便利语法让你能够省略闭包的花括号,用一个普通的表达式来代替显式的闭包。自动闭包让你能够延迟求值,因为直到你调用这个闭包,代码段才会被执行。延迟求值对于那些有副作用和高计算成本的代码来说是很有益处的,因为它使得你能控制代码的执行时机。

func serve(customer customerProvider: @autoclosure () -> String) {
    print("Now serving /(customerProvider())!")
}
serve(customer: customersInLine.remove(at: 0))
5、swift 中 closure 与 OC 中 block 的区别?

1、closure 是匿名函数,block 是一个结构体对象。
2、closure 可以通过逃逸闭包来在内部修改变量,block 通过 __block 修饰符。

9、泛型

泛型(generic)可以使我们在程序代码中定义一些可变的部分,在运行的时候指定。使用泛型可以最大限度地重用代码,保护类型的安全以及提高性能。

func areTheyEqual(x: T, _ y: T) -> Bool {
  return x == y 
}
areTheyEqual("ray", "ray")
areTheyEqual(1, 1)

10、Optional(可选型)

Optional 是一个泛型枚举

enum Optional {
  case none
  case some(Wrapped)
}

Optional 类型表示:有值 / 没有值。
在 Objective-C 中并没有 Optional 类型,只有 nil 并且 nil 只能用于表示对象类型无值,并不能用于基础类型(int, float)、枚举和结构体。基础类型需要返回类似 NSNotFound 的特殊值来表示无值。所以在 Swift 中定义了 Optinal 类型来表示各种类型的无值状态,并规定了nil 不能用于非可选的常量和变量,只能用于Optinal 类型。
解决方式:

强行打开 - 不安全
let a: String = x!

隐式解包变量声明 - 在许多情况下不安全
var a = x!

可选链接 - 安全
let a = x?.count

无合并操作员 - 安全
let a = x ?? ""

可选绑定 - 安全
if let a = x {
  print("x was successfully unwrapped and is = /(a)")
}

警卫声明 - 安全
guard let a = x else {
  return
}

可选模式 - 安全
if case let a? = x {
  print(a)
}

11、不通过继承,代码复用的方式有哪些?

全局函数,类扩展,泛型。

12、权限修饰符

open:公开权限, 最高的权限, 可以被其他模块访问, 继承及复写。只能用于类和类的成员。
public:公有访问权限,类或者类的公有属性或者公有方法可以从文件或者模块的任何地方进行访问。public 的权限在 Swift 3.0 后无法在其他模块被复写方法/属性或被继承。
internal:顾名思义,internal 是内部的意思,即有着 internal 访问权限的属性和方法说明在模块内部可以访问,超出模块内部就不可被访问了。在 Swift 中默认就是 internal 的访问权限。
fileprivate:文件私有访问权限,被 fileprivate 修饰的类或者类的属性或方法可以在同一个物理文件中访问。如果超出该物理文件,那么有着 fileprivate 访问权限的类, 属性和方法就不能被访问。
private:私有访问权限,被 private 修饰的类或者类的属性或方法可以在同一个物理文件中的同一个类型(包含 extension)访问。如果超出该物理文件或不属于同一类型,那么有着 private 访问权限的属性和方法就不能被访问。
从高到低排序如下:
open > public > interal > fileprivate > private

模块与源文件
模块:指的是独立的代码分发单元, 框架或应用程序会作为一个独立的模块来构建和发布(一个 App 就是一个模块,一个第三方 API, 第三方框架等都是一个完整的模块)。在 Swift 中,一个模块可以使用 import 关键字导入另外一个模块。
源文件:是 Swift 中的源代码文件,它通常属于一个模块,即一个应用程序或者框架。尽管我们一般会将不同的类型分别定义在不同的源文件中,但是同一个源文件也可以包含多个类型、函数之类的定义。

使用规则
1、一个实体不能被更低访问级别的实体定义。
定义时的访问级别要不小于使用时的访问级别;
函数中参数类型、返回值类型的访问级别要不小于函数的访问级别;
父类的访问级别要不小于子类的访问级别;
父类协议的访问级别要不小于子类协议的访问级别;
原类型的访问权限要不小于 typealias 定义的访问权限;
原始值类型、关联值类型的访问权限要不小于枚举类型;
2、元祖类型的访问级别取决于元祖中访问权限最低的成员。
3、泛型类型的访问级别取决于类型及参数中访问权限最低的那个。
4、类型的访问级别会影响成员(属性、方法、初始化器、下标)、嵌套类型的默认访问级别。若类型的访问级别小于 internal,则成员和嵌套类型的默认访问级别和类型的访问级别相同;若类型的访问级别不小于 internal,则成员和嵌套类型默认的访问级别为 internal。

setter/getter
setter/getter 默认接收它们所属环境的访问级别。可以单独设置 setter 的级别,但 setter 的访问级别要比 getter 的访问级别低。

枚举
不能给枚举中的每个 case 单独设置访问级别,故每个 case 的访问级别和枚举的访问级别相同。若枚举的访问级别为 public,则每个 case 的访问级别也为 public。

协议
不能给协议中的每个成员单独设置访问级别,故每个成员的访问级别和协议的访问级别相同。若协议的访问级别为 public,则每个成员的访问级别也为 public。
协议使用时的访问级别取决于协议和使用者两者中最低的访问级别。

扩展
若显示的设置了扩展的访问级别,则扩展中的成员的访问级别和扩展相同;若没有显示的设置扩展的访问级别,则扩展中的成员的访问级别和类中成员访问级别规则相同。
扩展中的成员可以单独设置访问级别。

示例

以下代码运行在全局作用域
private struct Dog {
    var age: Int = 0
    func run() {}
}
fileprivate struct Person {
    var dog: Dog = Dog()
    mutating func walk() {
        dog.run()
        dog.age = 1 
    } 
}
结果:在全局作用域下可以正常使用。
说明:fileprivate 的作用域是在当前源文件。private 的作用域是在其所定义地方都可以访问,而在全局作用域下的 private 所定义的作用域为当前源文件,故在全局作用域下定义的 private 等价于 fileprivate。此时内部的成员默认也是为 fileprivate。

将代码修改一下:
private struct Dog {
    private var age: Int = 0
    private func run() {}
}
fileprivate struct Person {
    var dog: Dog = Dog()
    mutating func walk() {
        dog.run()
        dog.age = 1
    }
}
结果:以上代码无论是不是在全局都不能正常运行。
说明:private 修饰的成员只能在当前大括号中使用,即 Dog 的大括号中使用。
class test {
    private struct Dog {
        var age: Int = 0
        func run() {}
    }
    private struct Person {
        var dog: Dog = Dog()
        mutating func walk() {
            dog.run()
            dog.age = 1
        }
    }
}
结果:以上代码可以通过
说明:Dog 的作用域在整个 test 的大括号内。Dog 成员的默认访问权限为 private,但成员的作用域和 Dog 的作用域相同即 test 的大括号内。

将代码修改一下:
class test {
    private struct Dog {
        private var age: Int = 0
        private func run() {}
    }
    private struct Person {
        var dog: Dog = Dog()
        mutating func walk() {
            dog.run()
            dog.age = 1
        }
    }
}
结果:以上代码不能通过。
说明:Dog  中 private 修饰的成员作用域只在 Dog 的大括号中,不能在 Person 中调用。

13、swift 中模式匹配

模式:单个或者复合值的结构,即模式不是一个特定的值,是一种抽象结构。
模式分为两种,一种是匹配任何类型的值;另外一种在运行时匹配某个特定的值,可能会失败。
第一种模式用于结构简单变量,常量和可选绑定中的值。此类模式包括通配符模式,标识符模式,以及包含前两种模式的值绑定模式和元组模式。你可以为这类模式指定一个类型标注,从而限制它们只能匹配某种特定类型的值。
第二种模式用于全局模式匹配。这种情况下,你试图匹配的值在运行时可能不存在。此类模式包括枚举用例模式、可选模式、表达式模式、类型转换模式。你在 switch 语句的 case 标签中,do 语句的 catch 子句中,或者在 if、while、guard、for-in 语句的 case 条件语句中使用这类模式。

模式匹配:
通配符模式:通配符模式由一个下划线(_)构成,用于匹配并忽略任何值,当你想忽略被匹配的值时可以使用该模式。

for _ in 1...3{
    
}

标识符模式:标识符模式可以匹配任何值,并将匹配的一个变量或者常量绑定起来。

let someValue = 42

值绑定模式:值绑定模式把匹配到的值绑定给一个变量或者常量。把匹配到的值给常量是用关键字 let, 给变量时用关键字 var。

let point = (3, 2)
switch point {
    //将point中的元素绑定到 x 和 y
case let (x, y):
    print("point is at x:(/(x), /(y)")
}

元祖模式:元组模式是由逗号分隔的,具有零个或多个模式的列表,并由一对圆括号括起来。元组模式匹配到响应的元组类型值。

let points = [(0, 0), (1, 0), (1, 1), (2, 0), (2, 1)]
//便利 y==0 x是任意值的元组
for (x, y) in points where y==0 {
    print("/(x) and /(y)")
}

枚举用例模式:枚举用例模式匹配现有的某个枚举类型的某个用例。枚举用例模式出现在switch 语句中的 case 标签中,以及 if,while,guard,for-in 语句的 case 条件中。
可选模式:可选项模式匹配 Optional 枚举在 some(Wrapped) 中包装的值。可选项模式为 for-in 语句提供了一种迭代数组的简便方式,只为数组中非 nil 的元素执行循环体。

let someOptional: Int? = 42
if case .some(let x) = someOptional {
    print(x)//42
}

if case let x? = someOptional {
    print(x)//42
}

let arrayOfOptionalInts: [Int?] = [nil, 2, 3, nil, 5]
for case let number? in arrayOfOptionalInts {
    print("fond a /(number)")//2,3,5
}

类型转换模式:case is Int: 或者 case let n as String:
表达式模式:范围匹配 case (0..<2) case(0...2, 2...4)
条件句中使用where: case (let age) where age > 30
if case let:if case let .Student(name) = xiaoming { }
for case let: for case let x in array where x > 10 {} 或者 for x in array where x > 10

14、观察者模式

观察者模式(Observer Pattern):定义对象间的一对一,一对多的依赖关系。当被观察对象发生改变时,所有观察对象都会收到通知。
观察者 Observer:通过 NSNotificationCenter 的 addObserver:selector:name:object 接口来注册通知。
NSNotificationCenter:通知中心,负责广播通知。
被观察的对象,通过 postNotificationName:object:userInfo: 发送通知。
通知对象 NSNotification,当有通知来的时候,Center 会调用观察者注册的接口来广播通知,同时传递存储着更改内容的 NSNotification 对象。

15、两段式初始化和安全检查

Swift 在编码安全方面为了保证初始化过程的安全,设定了两段式初始化、安全检查。
两段式初始化
第1阶段:初始化所有存储属性。
1、外层调用指定/便捷初始化器。
2、分配内存给实例,但未初始化。
3、指定初始化器确保当前类定义的存储属性都初始化。
4、指定初始化器调用父类的初始化器,不断向上调用,形成初始化器链。
第2阶段:设置新的存储属性值
1、从顶部初始化器往下,链中的每一个指定初始化器都有机会进一步定制实例。
2、初始化器现在能够使用 self(访问、修改它的属性,调用它的实例方法等等)。
3、最终,链中任何便捷初始化器都有机会定制实例以及使用 self。

安全检查
1、指定初始化器必须保证在调用父类初始化器之前,其所在类定义的所有存储属性都要初始化完成。
2、指定初始化器必须先调用父类初始化器,然后才能为继承的属性设置新值。
3、便捷初始化器必须先调用同类中的其它初始化器,然后再为任意属性设置新值。
4、初始化器在第1阶段初始化完成之前,不能调用任何实例方法、不能读取任何实例属性的值,也不能引用 self。
5、直到第1阶段结束,实例才算完全合法。

16、mutating 关键字的使用

默认情况下,不能在实例方法中修改值类型的属性。若在实例方法中使用 mutating 关键字,不仅可以在实例方法中修改值类型的属性,而且会在方法实现结束时将其写回到原始结构。
mutating 特点:
1、结构体中的函数如果想修改其中的属性,需要在函数前加上 mutating,而类则不用。
2、mutating 本质是加一个 inout 修饰的 self,将 self 从 let 常量改成了 var 变量。
3、Inout 相当于取地址,可以理解为地址传递,即引用。
4、mutating 修饰方法,而 inout 修饰参数。

17、inout 的作用

inout 可以修改属性的值。inout 是放在参数类型前,冒号后,mutating 放在方法的前面修改属性值。值类型变量作为参数传入函数,外界和函数参数的内存地址一致,函数内对参数的更改得到了保留。
1、函数参数默认为常量。试图从函数主体内部更改函数参数的值会导致编译时错误。这意味着您不能错误地更改参数的值。如果您希望函数修改参数的值并且希望这些更改在函数调用结束后仍然存在,请将该参数定义为输入输出参数。
2、可以将 inout 关键字放在参数类型的前面来编写输入/输出参数。
3、只能将变量作为输入输出参数的参数传递,不能将常量或文字值作为参数传递,因为无法修改常量和文字。将一个与号(&)作为变量传入 inout 参数时,将它放在变量名的前面,以表明该变量可以被函数修改。
4、输入输出参数不具有默认值且可变参数不能标记为 inout。

注意事项
1、使用 inout 关键字的函数,在调用时需要在该参数前加上 & 符号。
2、inout 参数在传入时必须为变量,不能为常量或字面量(literal)。
3、inout 参数不能有默认值,不能为可变参数。
4、inout 参数不等同于函数返回值,是一种使参数的作用域超出函数体的方式。
5、多个 inout 参数不能同时传入同一个变量,因为拷入拷出的顺序不定,那么最终值也不能确定。

inout 参数的传递过程
1、当函数被调用时,参数值被拷贝。
2、在函数体内,被拷贝的参数修改。
3、函数返回时,被拷贝的参数值被赋值给原有的变量。

func swap( a: inout Int, b: inout Int) {
    let temp = a
    a = b
    b = temp
}
var a = 1
var b = 2
print(a, b)// 1 2
swap(a: &a, b: &b)
print(a, b)// 2 1

18、associatedtype 的作用

关联类型:为协议中的某个类型提供了一个别名,其代表的真实类型在实现者中定义。

//协议,使用关联类型
protocol TableViewCell {
    associatedtype T
    func updateCell(_ data: T)
}
 
//遵守TableViewCell
class MyTableViewCell: UITableViewCell, TableViewCell {
    typealias T = Model
    func updateCell(_ data: Model) {
        // do something ...
    }
}

19、map、filter、reduce 的作用

map 用于映射,可以将一个列表转换为另一个列表。

[1, 2, 3].map{"/($0)"}// 数字数组转换为字符串数组
["1", "2", "3"]

filter 用于过滤,可以筛选出想要的元素。

[1, 2, 3].filter{$0 % 2 == 0} // 筛选偶数
// [2] 

reduce 合并

[1, 2, 3].reduce(""){$0 + "/($1)"}// 转换为字符串并拼接
// "123"

20、map 与 flatmap 的区别

1、map 可以对一个集合类型的所有元素做一个映射操作。
2、flatMap
第一个作用和 map 一样,对一个集合类型的所有元素做一个映射操作且可以过滤为 nil 的情况。

例如:
let array = [1,2,5,6,7,nil]
let array_map = array.map { $0 }
//[Optional(1), Optional(2), Optional(5), Optional(6), Optional(7), nil]
let array_flatmap = array_map.flatMap { $0 }
//[1, 2, 5, 6, 7]

第二种情况可以进行“降维”操作。

let array = [["1", "2"],["3", "4"]]
let array_map = array.map { $0 }
//[["1", "2"], ["3", "4"]]
let array_flatmap = array_map.flatMap { $0 }
//["1", "2", "3", "4"]

21、defer、guard 的作用

defer 语句块中的代码,会在当前作用域结束前调用,无论函数是否会抛出错误。每当一个作用域结束就进行该作用域 defer 执行。如果有多个 defer,那么后加入的先执行。
guard:过滤器/拦截器
guard 和 if 类似,不同的是,guard 总是有一个 else 语句,如果表达式是假或者值绑定失败的时候,会执行 else 语句且在 else 语句中一定要停止函数调用。

22、throws 和 rethrows 的用法与作用

throws 用在函数上,表示这个函数会抛出错误。
有两种情况会抛出错误,一种是直接使用 throw 抛出;另一种是调用其他抛出异常的函数时,直接使用 try XX 没有处理异常。

enum DivideError: Error {
    case EqualZeroError;
}
func divide(_ a: Double, _ b: Double) throws -> Double {
    guard b != Double(0) else {
        throw DivideError.EqualZeroError
    }
    return a / b
}
func split(pieces: Int) throws -> Double {
    return try divide(1, Double(pieces))
}

rethrows 与 throws 类似,不过只适用于参数中有函数且函数会抛出异常的情况,rethrows 可以用 throws 替换,反过来不行。

func processNumber(a: Double, b: Double, function: (Double, Double) throws -> Double) rethrows -> Double {
    return try function(a, b)
}

23、do - catch

有 throw 的方法,要进行 try 处理,在用 try 处理的时候,要用上 do - catch 方法。

do {
       //解析二进制数据
      let objcs = try NSJSONSerialization.JSONObjectWithData(data, options: NSJSONReadingOptions.MutableContainers) as! [[String : AnyObject]]
      for dict in objcs {
          let chilControllerName = dict["vcName"] as? String
          let title = dict["title"] as? String
          let imageName = dict["imageName"] as? String
          addChildViewController(chilControllerName, title: title, imageName: imageName)       
       }
 }catch {
      //如果do里边的代码发生错误,比如,解析不了数据,就会执行catch里边的代码
 }

补充:
在 do 中代码发生错误的时候,才会执行 catch 中代码
try 正常处理异常,也就是要使用 do - catch 方法处理。
try! 告诉系统一定不会有异常,可以不通过 do-catch 方式处理,不建议这么写。
try? 可以不通过 do-catch 方式处理。如果没错,返回给我们一个可选类型的值,如果有错,返回 nil。

24、@discardableResult

取消 Xcode 的警告⚠️

25、Swift 中的 KVC 和 KVO

KVC:要继承 NSObject 并在属性前加上 @objc

class KVCClass :NSObject{
    @objc var someValue: String = "123"
}
let kvc = KVCClass()
kvc.someValue // 123
kvc.setValue("456", forKey: "someValue")
kvc.someValue // 456
KVO:要继承自 NSObject 并将观测的对象标记为 dynamic.

class KVOClass:NSObject {
    dynamic var someValue: String = "123"
    var someOtherValue: String = "abc"
}

class ObserverClass: NSObject {
    func observer() {
        let kvo = KVOClass()
        kvo.addObserver(self, forKeyPath: "someValue", options: .new, context: nil)
        kvo.addObserver(self, forKeyPath: "someOtherValue", options: .new, context: nil)
        kvo.someValue = "456"
        kvo.someOtherValue = "def"
        kvo.removeObserver(self, forKeyPath: "someValue")
        kvo.removeObserver(self, forKeyPath: "someOtherValue")
    }
    override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
        print("/(keyPath!) change to /(change![.newKey] as! String)")
    }
}
ObserverClass().observer()
//someValue change to 456

问&答

1、隐式解包可选什么时候使用?

1、当属性在初始化不能为空的时候。
2、解决强引用循环的问题,当两个实例相互引用,需要一个实例是非空引用。

2、Set 独有的方法有哪些
// 定义一个 set
let setA: Set = [1, 2, 3, 4, 4]// {1, 2, 3, 4}, 顺序可能不一致, 同一个元素只有一个值
let setB: Set = [1, 3, 5, 7, 9]// {1, 3, 5, 7, 9}
// 取并集 A | B
let setUnion = setA.union(setB)// {1, 2, 3, 4, 5, 7, 9}
// 取交集 A & B
let setIntersect = setA.intersection(setB)// {1, 3}
// 取差集 A - B
let setRevers = setA.subtracting(setB) // {2, 4}
// 取对称差集, A XOR B = A - B | B - A
let setXor = setA.symmetricDifference(setB) //{2, 4, 5, 7, 9}
3、什么时候使用 final

不允许 class/函数继承/重写的时候在 class/函数前加上 final。

4、什么时候使用 @objc

1、@objc 用途是为了在 Objective-C 和 Swift 混编的时候,能够正常调用 Swift 代码。可以用于修饰类、协议、方法、属性。
2、常用的地方是在定义 delegate 协议中,会将协议中的部分方法声明为可选方法,需要用到 @objc。

5、dynamic 的作用

由于 swift 是一个静态语言,没有 Objective-C 中的消息发送这些动态机制,dynamic 的作用就是让 swift 代码也能有 Objective-C 中的动态机制,常用的地方就是 KVO 了,如果要监控一个属性,则必须要标记为 dynamic。

6、dynamic framework 和 static framework 的区别是什么?

静态库和动态库。静态库是每一个程序单独打包一份,而动态库则是多个程序之间共享。
静态库和动态库是相对编译期和运行期的:静态库在程序编译时会被链接到目标代码中,程序运行时将不再更改静态库;动态库在程序编译时并不会被链接到目标代码中,只是在程序运行时才被载入。
不同点:
静态库在链接时,会被完整的复制到可执行文件中,如果多个 App 都使用了同一个静态库,那么每个 App 都会拷贝一份,缺点是浪费内存。
动态库不会复制,只有一份,程序运行时动态加载到内存中,系统只会加载一次,多个程序共用一份,节约了内存。
共同点:
静态库和动态库都是闭源库,只能拿来满足某个功能的使用,不会暴露内部具体的代码信息。

静态库和动态库

7、set 和 array 的区别

set 存储的内容是无序的,在内存中存储的方式是不连续的;array 是有序的,在内存中存储的位置是连续的。
set 搜索元素的速率比 array 的要高,主要是 set 用了一个 hash 算法。
set 和 array 中都只能添加对象。

8、Error 如果要兼容 NSError 需要做什么操作

Error 是一个协议,swift 中的 Error 都是 enum,可以转 NSError。如果需要 Error 有 NSError 的功能,需要实现 LocalizedError 和 CustomNSError 协议。

9、String 与 NSString 的关系与区别

String 是值类型,NSString 是引用类型。

10、定义静态方法时关键字 static 和 class 有什么区别

static 定义的方法不可以被子类继承,class 则可以。

11、Swift 中 self 和 Self

Self 是表示特定类型,并且只能用在协议中或者作为某个类的方法的返回值类型。
self 在实例方法中代指当前实例,在类方法中则代指当前类。

12、下面的功能特性都包含在 Swift 中吗

1、泛型类
2、泛型结构体
3、泛型协议
答案:Swift 包含1和2特性。泛型可以在类、结构体、枚举、全局函数或者方法中使用。
3是通过 typealias 部分实现的。typealias 不是一个泛型类型,它只是一个占位符的名字。它通常是作为关联类型被引用,只有协议被一个类型引用的时候它才被定义。

13、如何自定义下标获取

实现 subscript 即可,如

extension AnyList {
    subscript(index: Int) -> T{
        return self.list[index]
    }
    subscript(indexString: String) -> T?{
        guard let index = Int(indexString) else {
            return nil
        }
        return self.list[index]
    }
}

索引除了数字之外,其他类型也是可以的。

14、为什么数组索引越界会崩溃,而字典用下标取值时 key 没有对应值的话返回的是 nil 不会崩溃。
struct Array {
    subscript(index: Int) -> Element
}

struct Dictionary {
    subscript(key: Key) -> Value?
}

1、数组索引访问的是一段连续地址,越界访问也能访问到内存,但这段内存不一定可用,所以会引起Crash。
2、字典的 key 并没有对应确定的内存地址,所以是安全的。

15、给集合中元素是字符串的类型增加一个扩展方法,应该怎么声明?
使用 where 子句, 限制 Element 为 String
extension Array where Element == String {
    var isStringElement:Bool {
        return true
    }
}
["1", "2"].isStringElement
//[1, 2].isStringElement// error
16、一个函数的参数类型只要是数字(Int、Float)都可以,要怎么表示?
Int、Float 都有一个协议
func myMethod(_ value: T) where T: Numeric {
    print(value + 1)
} 
或者 ExpressibleByIntegerLiteral 协议也行
17、一个类型表示选项,可以同时表示有几个选项选中(类似 UIViewAnimationOptions ),用什么类型表示?

需要实现自 OptionSet,一般使用 struct 实现。由于 OptionSet 要求有一个不可失败的 init(rawValue:) 构造器,而枚举无法做到这一点(枚举的原始值构造器是可失败的且有些组合值是没办法用一个枚举值表示的)。

struct SomeOption: OptionSet {
    let rawValue: Int
    static let option1 = SomeOption(rawValue: 1 << 0)
    static let option2 =  SomeOption(rawValue:1 << 1)
    static let option3 =  SomeOption(rawValue:1 << 2)
}
let options: SomeOption = [.option1, .option2]
18、下面枚举可以定义吗?
enum Edges : (Double, Double) {
  case TopLeft = (0.0, 0.0)
  case TopRight = (1.0, 0.0)
  case BottomLeft = (0.0, 1.0)
  case BottomRight = (1.0, 1.0)
}

不能。原始数值类型是元组类型,即使元组中的数值满足条件,也是不兼容的。
原始值类型必须满足:实现 Equatable 协议;是 Int、String、Character 转换的类型。

19、如何解决引用循环?

1、转换为值类型,只有类会存在引用循环,所以如果能不用类,是可以解引用循环的。
2、delegate 使用 weak 属性。
3、闭包中,对有可能发生循环引用的对象,使用 weak 或者 unowned 修饰。

20、在一个 HTTPS 连接的网站里,输入账号密码点击登录后,到服务器返回这个请求前中间经历了什么?
HTTPS 加密

1、客户端打包请求。包括 url,端口号,账号密码等等。账号密码登陆应该用的是 Post 方式,所以相关的用户信息会被加载到 body 里面。这个请求应该包含三个方面:网络地址,协议,资源路径。注意,这里是 HTTPS,就是HTTP + SSL / TLS,在 HTTP 上又加了一层处理加密信息的模块(相当于是个锁)。这个过程相当于是客户端请求钥匙。
2、服务器接受请求。一般客户端的请求会先发送到 DNS 服务器。 DNS 服务器负责将你的网络地址解析成IP地址,这个IP地址对应网上一台机器。这其中可能发生 Hosts Hijack 和 ISP failure 的问题。过了 DNS 这一关,信息就到了服务器端,此时客户端会和服务器的端口之间建立一个 socket 连接,socket 一般都是以 file descriptor 的方式解析请求。这个过程相当于是服务器端分析是否要向客户端发送钥匙模板。
3、服务器端返回数字证书。服务器端会有一套数字证书(相当于是个钥匙模板),这个证书会先发送给客户端。这个过程相当于是服务器端向客户端发送钥匙模板。
4、客户端生成加密信息。根据收到的数字证书(钥匙模板),客户端会生成钥匙,并把内容锁上,此时信息已经加密。这个过程相当于客户端生成钥匙并锁上请求。
5、客户端发送加密信息。服务器端会收到由自己发送出去的数字证书加锁的信息。 这个时候生成的钥匙也一并被发送到服务器端。这个过程是相当于客户端发送请求。
6、服务器端解锁加密信息。服务器端收到加密信息后,会根据得到的钥匙进行解密,并把要返回的数据进行对称加密。这个过程相当于服务器端解锁请求、生成、加锁回应信息。
7、服务器端向客户端返回信息。客户端会收到相应的加密信息。这个过程相当于服务器端向客户端发送回应。
8、客户端解锁返回信息。客户端会用刚刚生成的钥匙进行解密,将内容显示在浏览器上。

21、以下有什么区别?
const int number = 0;
let number = 0

const 是一个变量在编译期间被初始化值或者在编译期间表达式的值.
let 关键字创建常量是在 Runtime 时初始化的,它能够用静态的或者动态表达式的结果初始化.注意它的值只能被初始化一次.
22、如何声明一个只能被类 conform 的 protocol ?
protocol OnlyClassProtocol : class {

}
23、自定义运算符

自定义的运算符可以在全局使用。需要使用 operator 关键字。使用 prefix、infix、postfix 标记运算符使用的位置。

prefix operator +++ {}
prefix func +++ (inout vector: Vector2D) -> Vector2D {
    vector += vector
    return vector
}

var toBeDoubled = Vector2D(x: 1.0, y: 4.0)
let afterDoubling = +++toBeDoubled
// toBeDoubled now has values of (2.0, 8.0)
// afterDoubling also has values of (2.0, 8.0)
24、自定义操作符

可以定义操作符的关联性 associativity 和优先级 precedence。associativity 有三个值:left、right、none,默认是 none。precedence 默认值是:100。

infix operator +- { associativity left precedence 140 }
func +- (left: Vector2D, right: Vector2D) -> Vector2D {
    return Vector2D(x: left.x + right.x, y: left.y - right.y)
}
let firstVector = Vector2D(x: 1.0, y: 2.0)
let secondVector = Vector2D(x: 3.0, y: 4.0)
let plusMinusVector = firstVector +- secondVector
// plusMinusVector is a Vector2D instance with values of (4.0, -2.0)
25、nil 和 .None
var optional1: String? = nil
var optional2: String? = .None
nil 和 .None 有什么不同?变量 optional1 和 optional2 有什么不同?

答:nil和.None是一样的。当可选变量没有值时,Optional.None(.None for short)是一般初始化可选变量的方式,而nil则是另一种写法。
事实上,下面条件语句输出是true:
nil == .None
注: enumeration 结构
enum Optional {
  case None
  case Some(T)
}

示例

1、下面代码输出什么?
protocol Chef {
    func makeFood()
}

extension Chef {
    func makeFood() {
        print("make food")
    }
}

struct SeafoodChef: Chef {
    func makeFood() {
        print("make seafood")
    }
}

let chefOne: Chef = SeafoodChef()
let chefTwo: SeafoodChef = SeafoodChef()
chefOne.makeFood()
chefTwo.makeFood()

结果:
make seafood
make seafood

分析:
在 Swift 中,协议中方法是动态派发,扩展中方法是静态派发。
如果协议中有方法声明,则会根据对象的实际类型进行调用。
如果协议中没有方法声明,则会根据对象的声明类型进行调用。

上面 makeFood() 方法在 Chef 协议中已经声明了,而 chefOne 虽然声明为 Chef,但实际实现为 SeafoodChef。所以,根据实际情况,makeFood() 会调用 SeafoodChef 中的实现。chefTwo 也是同样的道理。

如果 protocol 中没有声明 makeFood() 方法,代码又会输出什么?

make food
make seafood

因为协议中没有声明 makeFood() 方法,所以,此时只会按照扩展中的声明类型进行静态派发。也就是说,会根据对象的声明类型进行调用。chefone 被声明为 Chef,所以会调用扩展的实现,chefTwo 被声明为 SeafoodChef,则会调用 SeafoodChef 中的实现。

2、下面代码有什么问题
public class Node {
  public var value: Int
  public var prev: Node?
  public var post: Node?

  public init(_ value: Int) {
    self.value = value
  }
}

答案:应该在 prev 或者 post 前面加上 weak。
原因:表面上看,以上代码毫无问题。但是我这样一写,问题就来了:

let head = Node(0)
let tail = Node(1)
head.post = tail
tail.prev = head

此时,head 和 tail 互相指向,形成循环引用(retain cycle)。
3、实现一个函数,输入是任一整数,输出要返回输入的整数 + 2
//利用 Swift 的 Currying 特性:
func add(_ num: Int) -> (Int) -> Int {
  return { val in
    return num + val
  }
}
4、下面代码有什么问题?
protocol SomeProtocol {
    func doSomething()
}

class Person {
    weak var delegate: SomeProtocol?
}

解析:声明 delegate 属性的时候错误,编译器会报错。
Swift 中协议不仅可以被 class 这样的引用类型实现,也可以被 struct 和 enum 这样的值类型实现(这是和 OC 的一大区别)。weak 关键词是 ARC 环境下,为引用类型提供引用计数这样的内存管理,它是不能被用来修饰值类型的。
修改方案如下:

1)在 protocol 前面加上 @objc。在 OC 中,协议只能由 class 来实现,这样一来,weak 修饰的对象与 OC 一样,只不过是 class 类型。如下:
@objc protocol SomeProtocol {
    func doSomething()
}

2)在 SomeProtocol 之后添加 class 关键词。如此一来就声明该协议只能由类(class)来实现。如下:
protocol SomeProtocol: class {
    func doSomething()
}
5、编程
public struct Thermometer {
  public var temperature: Double
  public init(temperature: Double) {
    self.temperature = temperature
  }
}

可以通过以下方式使用

var t: Thermometer = Thermometer(temperature:56.8)

能实现以下方法调用吗?要怎么实现?

var thermometer: Thermometer = 56.8

解决:swift 提供以下协议,通过使用赋值操作符,用字面值直接初始化.
NilLiteralConvertible , BooleanLiteralConvertible , IntegerLiteralConvertible , FloatLiteralConvertible , UnicodeScalarLiteralConvertible , ExtendedGraphemeClusterLiteralConvertible , StringLiteralConvertible , ArrayLiteralConvertible , DictionaryLiteralConvertible.
采用相对应的协议,提供一个公有的构造器,允许字面值方式初始化它.
在这个温度计这个例子中,需要实现FloatLiteralConvertible协议.

extension Thermometer : FloatLiteralConvertible {
  public init(floatLiteral value: FloatLiteralType) {
    self.init(temperature: value)
  }
}

可以实现

var thermometer: Thermometer = 56.8

示例二:

func countUniques(array: Array) -> Int {
  let sorted = array.sort(<)
  let initial: (T?, Int) = (.None, 0)
  let reduced = sorted.reduce(initial) { ($1, $0.0 == $1 ? $0.1 : $0.1 + 1) }
  return reduced.1
}
调用:
countUniques([1, 2, 3, 3]) // result is 3
重写以上代码,使其可以这样调用
[1, 2, 3, 3].countUniques() // should print 3

泛型是可以被扩展的,但需要类型约束限制,如果泛型不满足约束,那么扩展也是不可见或者不可访问的。因此,全局函数 countUniques 可以被重写为数组 Array 的扩展:
extension Array where Element: Comparable {
  func countUniques() -> Int {
    let sorted = sort(<)
    let initial: (Element?, Int) = (.None, 0)
    let reduced = sorted.reduce(initial) { ($1, $0.0 == $1 ? $0.1 : $0.1 + 1) }
    return reduced.1
  }
}
注意:这个被重写的方法只有当泛型的类型 Element 实现了 Comparable 协议才是有效的。
6、给一个数组,要求写一个函数,交换数组中的两个元素
func swap(_ nums: inout [T], _ p: Int, _ q: Int) {
    (nums[p], nums[q]) = (nums[q], nums[p])
}
7、以下函数会打印出什么?
var car = "Benz" 
let closure = { [car] in 
  print("I drive /(car)")
} 
car = "Tesla" 
closure()

答案:"I drive Benz

当声明闭包的时候,捕获列表会创建一份变量的 copy,被捕获到的值是不会改变的,即使外界变量的值发生了改变。所以会打印出"I drive Benz"。

//稍作修改:

var car = "Benz" 
let closure = {
  print("I drive /(car)")
} 
car = "Tesla" 
closure()

答案:I drive Tesla

如果去掉闭包中的捕获列表,编译器会使用引用代替 copy。在这种情况下,当闭包被调用时,变量的值是可以改变的。所以 clousre 用的还是全局的 car 变量,此时将会打印出 "I drive Tesla"

8、以下代码有什么问题?
public class ThermometerClass {
  private(set) var temperature: Double = 0.0
  public func registerTemperature(temperature: Double) {
    self.temperature = temperature
  }
}
 
let thermometerClass = ThermometerClass()
thermometerClass.registerTemperature(56.0)
 
public struct ThermometerStruct {
  private(set) var temperature: Double = 0.0
  public mutating func registerTemperature(temperature: Double) {
    self.temperature = temperature
  }
}
 
let thermometerStruct = ThermometerStruct()
thermometerStruct.registerTemperature(56.0)

在最后一行编译器会提示错误,结构体 ThermometerStruct 声明一个可变的函数修改内部变量 temperature,但是 registerTemperature 却被一个用 let 创建的实例所调用,用 let 定义的变量是不可变的,所以编译通不过。
在结构体中,改变内部状态的方法必须用 mutating 声明,而且不允许用不可变的实例调用它。

9、简化以下代码
let animals = ["fish", "cat", "chicken", "dog"]
let sortedAnimals = animals.sort { (one: String, two: String) -> Bool in
  return one < two
}


1 可以简化闭包的参数,因为在闭包中,系统是可以通过类型推断方式推算出参数的类型。所以你可以去掉参数的类型:
let sortedAnimals = animals.sort { (one, two) -> Bool in return one < two }

2 返回类型也可以推算出来,所以可以去掉闭包的返回类型:
let sortedAnimals = animals.sort { (one, two) in return one < two }

3 可以用$i符号替换掉参数的名字,代码然后就变成这样:
let sortedAnimals = animals.sort { return $0 < $1 }

4 在单语句的闭包中,关键字return也可以省略。最后一条语句的返回值就变成闭包的返回值:
let sortedAnimals = animals.sort { $0 < $1 }

5 终极奥义
let sortedAnimals = animals.sort(<)

版权声明:
作者:lichengxin
链接:https://www.techfm.club/p/47243.html
来源:TechFM
文章版权归作者所有,未经允许请勿转载。

THE END
分享
二维码
< <上一篇
下一篇>>