阅读 4464

Swift开发小记(含面试题)

春节前后一直在忙晋升的事情,整理了下之前记录的Swift知识点,同时在网上看到的一些有意思的面试题我也加了进来,有的题目我稍微变了下,感觉会更有趣,当然老规矩所有代码在整理时都重新跑了一遍确认~ 另外后续文章的更新频率加快,除了iOS开发上的一些文章,也会增加一些跨平台、前端、算法等文章,也算是对自己技术栈的回顾吧

声明


  • let和var

    let用来声明常量,var用来声明变量。了解js的对于这两个应该不陌生,但是区别还是挺大的,尤其是let,在js中是用来声明变量的,const才是用来声明常量的。

  • 类型标注

    声明常量/变量时可以增加类型标注,来说明存储类型,例如

    var message: String
    复制代码

    如果不显示说明,Swift会根据声明时赋值自动推断出对应类型。一般不太需要标注类型,但是之前遇到过在某些情况下需要大量声明时,由于没有标注类型,Xcode直接内存爆了导致Mac死机,后来加上就好了。

  • 命名

    常量与变量名不能包含数学符号,箭头,保留的(或者非法的)Unicode 码位,连线与制表符。也不能以数字开头,但是可以在常量与变量名的其他地方包含数字,除此外你可以用任何你喜欢的字符,例如

    let 🐶 = ""
    let 蜗牛君 = ""
    复制代码

    但是不推荐使用,骚操作太多容易闪着腰,Camel-Case仍是更好的选择。

    另外如果需要使用Swift保留关键字,可以使用反引号`包裹使用,例如

    enum BlogStyle {
        case `default`
        case colors
    }
    复制代码

元组(Tuple)


Swift支持把多个值组合成一个复合值,称为元组。元组内的值可以是任意类型,并不要求是相同类型,例如

let size = (width: 10, height: 10)
print("\(size.0)")
print("\(size.width)")

// 也可以不对元素命名
let size = (10, 10)
复制代码

在函数需要返回多个值时,元组非常有用。但它并不适合复杂的数据表达,并且尽量只在临时需要时使用

之前有同事通过元组返回多个值,且没有对元素命名,然后不少地方都使用了该元组,导致后面的同事接手时无法快速理解数据含义,并且在需要改动返回数据时,必须通过代码逻辑去查找哪些地方使用了该元组,耗费了不少时间。
复制代码

可选类型


可选类型是Swift区别与ObjC的另一个新特性,它用于处理值可能缺失的情况,可以通过在类型标注后面加上一个?来表示这是一个可选类型,例如

// 可选类型没有明确赋值时,默认为nil
var message: String?
// 要么有确切值
message = "Hello"
// 要么没有值,为nil
message = nil
复制代码

Optional实际上是一个泛型枚举,大致源码如下:

public enum Optional<Wrapped> : ExpressibleByNilLiteral {

    /// The absence of a value.
    ///
    /// In code, the absence of a value is typically written using the `nil`
    /// literal rather than the explicit `.none` enumeration case.
    case none

    /// The presence of a value, stored as `Wrapped`.
    case some(Wrapped)
  
    ....
}
复制代码

所以上面的初始化还可以这么写:

str = .none
复制代码
Swift 的 nil 和 ObjC 中的 nil 并不一样。在 ObjC 中,nil 是一个指向不存在对象的指针。在 Swift 中,nil 不是指针——它是一个确定的值,用来表示值缺失。任何类型的可选状态都可以被设置为 nil,不只是对象类型。
复制代码
  • 可选绑定

    但是在日常开发过程中,常常我们需要判断可选类型是否有值,并且获取该值,这时候我们就要用到可选绑定

    if let message = message {
      ...
    }
    
    guard let message = message else {
      return
    }
    ...
    复制代码

    上述代码可以理解为:如果 message 返回的可选 String 包含一个值,创建一个叫做 message 的新常量并将可选包含的值赋给它。

  • 隐式解析可选类型

    虽然我们可以通过可选绑定来获取可选类型的值,但是对于一些除了初始化时为nil,后续不再为nil的值来说,使用可选绑定会显的臃肿.

    隐式解析可选类型支持在可选类型后面加!来直接获取有效值, 例如

    let message: String = message!
    复制代码

    如果你在隐式解析可选类型没有值的时候尝试取值,会触发运行时错误。因此Apple建议如果一个变量之后可能变成nil的话请不要使用隐式解析可选类型,而是使用普通可选类型。

    但事实是靠人为去判别是否能使用隐式解析可选类型是非常危险的,尤其是团队合作,一旦出问题就会造成崩溃,因此在我们团队不允许使用隐式解析可选类型。
    复制代码

运算符


  • 空合运算符

    空合运算符a ?? b)将对可选类型 a 进行空判断,如果 a 包含一个值就进行解封,否则就返回一个默认值 b。表达式 a 必须是 Optional 类型。默认值 b 的类型必须要和 a 存储值的类型保持一致。

    let message: String = message ?? "Hello"
    // 实际上等同于三目运算符
    message != nil ? message! : "Hello"
    复制代码
  • 区间运算符

    Swift提供了几种方便表达一个区间的值的区间运算符,I like it😀

    let array = [1, 2, 3, 4, 5]
    // 闭区间运算符,表示截取下标0~2的数组元素
    array[0...2]
    // 半开区间运算符,表示截取下标0~1的数组元素
    array[0..<2]
    // 单侧区间运算符,表示截取开始到下标2的数组元素
    array[...2]
    // 单侧区间运算符,表示截取从下标2到结束的数组元素
    array[2...]
    复制代码

    除此之外,还可以通过 ... 或者 ..< 来连接两个字符串。一个常见的使用场景就是检查某个字符是否是合法的字符。

    // 判断是否包含大写字母,并打印
    let str = "Hello"
    let test = "A"..."Z"
    for c in str {
        if test.contains(String(c)) {
            print("\(c)是大写字母")
        }
    }
    // 打印 H是大写字母
    复制代码
  • 恒等运算符

    有时候我们需要判定两个常量或者变量是否引用同一个类实例。为了达到这个目的,Swift 内建了两个恒等运算符:

    • 等价于(===
    • 不等价于(!==

    运用这两个运算符检测两个常量或者变量是否引用同一个实例。

  • ++和--

    Swift不支持这种写法,ObjC还用得蛮多的。

闭包


闭包是自包含的函数代码块,可以在代码中被传递和使用。Swift 中的闭包与 C 和 ObjC 中的代码块(blocks)比较相似。

Swift 的闭包表达式拥有简洁的风格,并鼓励在常见场景中进行语法优化,主要优化如下:

-利用上下文推断参数和返回值类型

-隐式返回单表达式闭包,即单表达式闭包可以省略 return 关键字

-参数名称缩写

-尾随闭包语法

闭包表达式语法有如下的一般形式:

{ (parameters) -> returnType in
    statements
}
复制代码
  • 尾随闭包

    当函数的最后一个参数是闭包时,可以使用尾随闭包来增强函数的可读性。在使用尾随闭包时,你不用写出它的参数标签:

    func test(closure: () -> Void) {
        ...
    }
    
    // 不使用尾随闭包
    test(closure: {
        ...
    })
    
    // 使用尾随闭包
    test() {
        ...
    }
    复制代码
  • 逃逸闭包

    当一个闭包作为参数传到一个函数中,但是这个闭包在函数返回之后还可能被使用,我们称该闭包从函数中逃逸。例如

    var completions: [() -> Void] = []
    func testClosure(completion: () -> Void) {
        completions.append(completion)
    }
    复制代码

    此时编译器会报错,提示你这是一个逃逸闭包,我们可以在参数名之前标注 @escaping,用来指明这个闭包是允许“逃逸”出这个函数的。

    var completions: [() -> Void] = []
    func testEscapingClosure(completion: @escaping () -> Void) {
        completions.append(completion)
    }
    复制代码

    另外,将一个闭包标记为 @escaping 意味着你必须在闭包中显式地引用 self,而非逃逸闭包则不用。这提醒你可能会一不小心就捕获了self,注意循环引用。

  • 自动闭包

    自动闭包是一种自动创建的闭包,用于包装传递给函数作为参数的表达式。这种闭包不接受任何参数,让你能够省略闭包的花括号,用一个普通的表达式来代替显式的闭包。

    并且自动闭包让你能够延迟求值,因为直到你调用这个闭包,代码段才会被执行。要标注一个闭包是自动闭包,需要使用@autoclosure

    // 未使用自动闭包,需要显示用花括号说明这个参数是一个闭包
    func test(closure: () -> Bool) {
    }
    test(closure: { 1 < 2 } )
    
    // 使用自动闭包,只需要传递表达式
    func test(closure: @autoclosure () -> String) {
    }
    test(customer: 1 < 2)
    复制代码

递归枚举


可能有这样一个场景,定义一个Food的枚举,包含了一些食物,同时还支持基于这些食物可以两两混合成新食物

enum Food {
    case beef
    case potato
    case mix(Food, Food)
}
复制代码

此时编译器会提示你Recursive enum 'Food' is not marked 'indirect',原因是因为枚举成员里出现了递归调用。因此我们需要在枚举成员前加上indirect来表示该成员可递归。

// 标记整个枚举是递归枚举
indirect enum Food {
    case beef
    case potato
    case mix(Food, Food)
}

// 仅标记存在递归的枚举成员
enum Food {
    case beef
    case potato
    indirect case mix(Food, Food)
}
复制代码

更推荐第二种写法,因为使用递归枚举时,编译器会插入一个间接层。仅标记枚举成员,能够减少不必要的消耗。

属性


  • 存储属性

    简单来说,一个存储属性就是存储在特定类或结构体实例里的一个常量或变量。存储属性可以是变量存储属性(用关键字 var 定义),也可以是常量存储属性(用关键字 let 定义)。

    struct Person {
        var name: String
        var height: CGFloat
    }
    复制代码

    同时我们还可以通过Lazy来标示该属性为延迟存储属性,类似于ObjC常说的的懒加载。

    lazy var fileName: String = "data.txt"
    复制代码
    如果一个被标记为 lazy 的属性在没有初始化时就同时被多个线程访问,则无法保证该属性只会被初始化一次,也就是说它是非线程安全的。
    复制代码
  • 计算属性

    除存储属性外,类、结构体和枚举可以定义计算属性。计算属性不直接存储值,而是提供一个 getter 和一个可选的 setter,来间接获取和设置其他属性或变量的值。

    struct Rect {
        var origin = CGPoint.zero
        var size = CGSize.zero
        var center: CGPoint {
            get {
                let centerX = origin.x + (size.width / 2)
                let centerY = origin.y + (size.height / 2)
                return Point(x: centerX, y: centerY)
            }
            set {
                origin.x = newValue.x - (size.width / 2)
                origin.y = newValue.y - (size.height / 2)
            }
        }
    }
    复制代码

    如果我们只希望可读而不可写时,setter方法不提供即可,可以简写为

    var center: CGPoint {
        let centerX = origin.x + (size.width / 2)
        let centerY = origin.y + (size.height / 2)
        return Point(x: centerX, y: centerY)
    }
    复制代码
  • 观察器

    Swift提供了非常方便的观察属性变化的方法,每次属性被设置值的时候都会调用属性观察器,即使新值和当前值相同的时候也不例外。

    var origin: CGPoint {
        willSet {
            print("\(newValue)")
        }
        didSet {
            print("\(oldValue)")
        }
    }
    复制代码
  • 调用时序

    调用 numberset 方法可以看到工作的顺序

    let b = B()
    b.number = 0
    
    // 输出
    // get
    // willSet
    // set
    // didSet
    复制代码

    为什么有个get

    这是因为我们实现了 didSetdidSet 中会用到 oldValue,而这个值需要在整个 set 动作之前进行获取并存储待用,否则将无法确保正确性。如果我们不实现 didSet 的话,这次 get 操作也将不存在。

unowned


你可以在声明属性或者变量时,在前面加上关键unowned`表示这是一个无主引用。使用无主引用,你必须确保引用始终指向一个未销毁的实例。

和weak类似,unowned不会牢牢保持住引用的实例。它也被用于解决可能存在循环引用,且对象是非可选类型的场景。

例如在这样的设计中:一个客户可能有或者没有信用卡,但是一张信用卡总是关联着一个客户。

class Customer {
    let name: String
    var card: CreditCard?
}

class CreditCard {
    let number: UInt64
    unowned let customer: Customer // 由于始终有值,无法使用weak
}
复制代码

is和as


  • is

is 在功能上相当于ObjC的 isKindOfClass,可以检查一个对象是否属于某类型或其子类型。is 和原来的区别主要在于亮点,首先它不仅可以用于 class 类型上,也可以对 Swift 的其他像是 structenum 类型进行判断。

class ClassA { }
class ClassB: ClassA { }

let obj: AnyObject = ClassB()

if (obj is ClassA) {
    print("属于 ClassA")
}

if (obj is ClassB) {
    print("属于 ClassB")
}
复制代码
  • as

某类型的一个常量或变量可能在幕后实际上属于一个子类。当确定是这种情况时,你可以尝试向下转到它的子类型,用类型转换操作符as?as!)。

class Media {}
class Movie: Media {}
class Song: Media {}

for item in medias {
    if let movie = item as? Movie { 
        print("It's Movie")
    } else if let song = item as? Song {
        print("It's Song")
    }
}
复制代码

as? 返回一个你试图向下转成的类型的可选值。 as! 把试图向下转型和强制解包转换结果结合为一个操作。只有你可以确定向下转型一定会成功时,才使用as!

如同隐式解析可选类型一样,as!同样具有崩溃的高风险,我们一般不允许使用。
复制代码

扩展下标


Swift支持通过Extension为已有类型添加新下标,例如

extension Int {
    subscript(digitIndex: Int) -> Int {
        var decimalBase = 1
        for _ in 0..<digitIndex {
            decimalBase *= 10
        }
        return (self / decimalBase) % 10
    }
}
746381295[0]
// 返回 5
746381295[1]
// 返回 9
746381295[2]
// 返回 2
746381295[8]
// 返回 7
复制代码

如果该 Int 值没有足够的位数,即下标越界,那么上述下标实现会返回 0,犹如在数字左边自动补 0

746381295[9]
// 返回 0,即等同于:
0746381295[9]
复制代码

mutating


结构体和枚举类型中修改 self 或其属性的方法必须将该实例方法标注为 mutating,否则无法在方法里改变自己的变量。

struct MyCar {
    var color = UIColor.blue
    mutating func changeColor() {
        color = UIColor.red
    }
}
复制代码

由于Swift 的 protocol 不仅可以被 class 类型实现,也适用于 structenum,因此我们在写给别人用的接口时需要多考虑是否使用 mutating 来修饰方法。

实现协议中的 mutating 方法时,若是类类型,则不用写 mutating 关键字。而对于结构体和枚举,则必须写 mutating 关键字。
复制代码

协议合成


有时候需要同时遵循多个协议,例如一个函数希望某个参数同时满足ProtocolA和ProtocolB,我们可以采用 ProtocolA & ProtocolB 这样的格式进行组合,称为 协议合成(protocol composition)

func testComposition(protocols: ProtocolA & ProtocolB) {
}
复制代码

selector和@objc

在开发中常常有面临这样的代码

btn.addTarget(self, action: #selector(onClick(_:)), for: .touchUpInside)

@objc func onClick(_ sender: UIButton) {
}
复制代码

为什么要使用@objc?

因为Swift 中的 #selector 是从暴露给 ObjC 的代码中获取一个 selector,所以它仍然是 ObjC runtime 的概念,如果你的 selector 对应的方法只在 Swift 中可见的话 (也就是说它是一个 Swift 中的 private 方法),在调用这个 selector 时你会遇到一个 unrecognized selector 错误。

inout


有些时候我们会希望在方法内部直接修改输入的值,这时候我们可以使用 inout 来对参数进行修饰:

func addOne(_ variable: inout Int) {
    variable += 1
}
复制代码

因为在函数内部就更改了值,所以也不需要返回了。调用也要改变为相应的形式,在前面加上 & 符号:

incrementor(&luckyNumber)
复制代码

单例


在 ObjC 中单例一般写成:

@implementation MyManager
+ (id)sharedManager {
    static MyManager *staticInstance = nil;
    static dispatch_once_t onceToken;

    dispatch_once(&onceToken, ^{
        staticInstance = [[self alloc] init];
    });
    return staticInstance;
}
@end
复制代码

但在Swift中变得非常简洁:

static let sharedInstance = MyManager()
复制代码

随机数


我们常常可能使用这样的方式来获取随机数,比如100以内的:

let randomNum: Int = arc4random() % 100
复制代码

此时编译器会提示error,因为arc4random()返回UInt32,需要做类型转换,有时候我们可能就直接:

let randomNum: Int = Int(arc4random()) % 100
复制代码

结果测试的时候发现在有些机型上就崩溃了。

这是因为Int在32位机器上(iPhone5及以下)相当于Int32,64位机器上相当于Int64,表现上与ObjC中的NSInteger一致,而arc4random()始终返回UInt32,所以在32位机器上就可能越界崩溃了。

最快捷的方式可以先取余之后再类型转换:

let randomNum: Int = Int(arc4random() % 100)
复制代码

可变参数函数


如果想要一个可变参数的函数只需要在声明参数时在类型后面加上 ... 就可以了。

func sum(input: Int...) -> Int {
    return input.reduce(0, combine: +)
}

print(sum(1,2,3,4,5))
复制代码

可选协议


Swift protocol本身不允许可选项,要求所有方法都是必须得实现的。但是由于Swift和ObjC可以混编,那么为了方便和ObjC打交道,Swift支持在 protocol中使用 optional 关键字作为前缀来定义可选要求,且协议和可选要求都必须带上@objc属性。

@objc protocol CounterDataSource {
    @objc optional func incrementForCount(count: Int) -> Int
    @objc optional var fixedIncrement: Int { get }
}
复制代码

但是标记 @objc 特性的协议只能被继承自 ObjC 类的类或者 @objc 类遵循,其他类以及结构体和枚举均不能遵循这种协议。这对于Swift protocol是一个很大的限制。

由于 protocol支持可扩展,那么我们可以在声明一个 protocol之后再用extension的方式给出部分方法默认的实现,这样这些方法在实际的类中就是可选实现的了。

protocol CounterDataSource {
    func incrementForCount(count: Int) -> Int
    var fixedIncrement: Int { get }
}

extension CounterDataSource {
    func incrementForCount(count: Int) -> Int {
        if count == 0 {
            return 0
        } else if count < 0 {
            return 1
        } else {
            return -1
        }
    }
}

class Counter: CounterDataSource {
    var fixedIncrement: Int = 0
}
复制代码

协议扩展


有这么一个例子:

protocol A2 {
    func method1()
}

extension A2 {
    func method1() {
        return print("hi")
    }

    func method2() {
        return print("hi")
    }
}

struct B2: A2 {
    func method1() {
        return print("hello")
    }

    func method2() {
        return print("hello")
    }
}

let b2 = B2()
b2.method1()
b2.method2()
复制代码

打印的结果如下:

hello
hello
复制代码

结果看起来在意料之中,那如果我们稍作改动:

let a2 = b2 as A2
a2.method1() 
a2.method2() 
复制代码

此时结果是什么呢?还与之前一样么?打印结果如下:

hello
hi
复制代码

对于 method1,因为它在 protocol 中被定义了,因此对于一个被声明为遵守接口的类型的实例 (也就是对于 a2) 来说,可以确定实例必然实现了 method1,我们可以放心大胆地用动态派发的方式使用最终的实现 (不论它是在类型中的具体实现,还是在接口扩展中的默认实现);但是对于 method2 来说,我们只是在接口扩展中进行了定义,没有任何规定说它必须在最终的类型中被实现。在使用时,因为 a2 只是一个符合 A2 接口的实例,编译器对 method2 唯一能确定的只是在接口扩展中有一个默认实现,因此在调用时,无法确定安全,也就不会去进行动态派发,而是转而编译期间就确定的默认实现。

值类型和引用类型


Swift 的类型分为值类型和引用类型两种,值类型在传递和赋值时将进行复制,而引用类型则只会使用引用对象的一个 "指向"。

  • 值类型有哪些?

    Swift 中的 structenum 定义的类型是值类型,使用 class 定义的为引用类型。很有意思的是,Swift 中的所有的内建类型都是值类型,不仅包括了传统意义像 IntBool这些,甚至连 StringArray 以及 Dictionary 都是值类型的。

  • 值类型的好处?

    相较于传统的引用类型来说,一个很显而易见的优势就是减少了堆上内存分配和回收的次数。值类型的一个特点是在传递和赋值时进行复制,每次复制肯定会产生额外开销,但是在 Swift 中这个消耗被控制在了最小范围内,在没有必要复制的时候,值类型的复制都是不会发生的。

    var a = [1,2,3]
    var b = a
    let c = b
    b.append(5) // 此时 a,c 和 b 的内存地址不再相同
    复制代码

    只有当值类型的内容发生改变时,值类型被才会复制。

  • 值类型的弊端?

    在少数情况下,我们显然也可能会在数组或者字典中存储非常多的东西,并且还要对其中的内容进行添加或者删除。在这时,Swift 内建的值类型的容器类型在每次操作时都需要复制一遍,即使是存储的都是引用类型,在复制时我们还是需要存储大量的引用,这个开销就变得不容忽视了。

  • 最佳实践

    针对上述问题,我们可以通过 Cocoa 中的引用类型的容器类来对应这种情况,那就是 NSMutableArrayNSMutableDictionary

    所以,在使用数组合字典时的最佳实践应该是,按照具体的数据规模和操作特点来决定到时是使用值类型的容器还是引用类型的容器:在需要处理大量数据并且频繁操作 (增减) 其中元素时,选择 NSMutableArrayNSMutableDictionary 会更好,而对于容器内条目小而容器本身数目多的情况,应该使用 Swift 语言内建的 ArrayDictionary

获取对象类型


let str = "Hello"
print("\(type(of: str))")
print("\(String(describing: object_getClass(str)))")

// String
// Optional(NSTaggedPointerString)
复制代码

KVO


KVO在Cocoa中是非常强大的特性,在ObjC中有非常多的应用,之前在《iOS开发小记-基础篇》中有相关介绍,感兴趣的同学可以移步这篇文章。

在 Swift 中我们也是可以使用 KVO 的,但是仅限于在 NSObject 的子类中。这是可以理解的,因为 KVO 是基于 KVC (Key-Value Coding) 以及动态派发技术实现的,而这些东西都是 ObjC 运行时的概念。另外由于 Swift 为了效率,默认禁用了动态派发,因此想用 Swift 来实现 KVO,我们还需要做额外的工作,那就是将想要观测的对象标记为 @objc dynamic`。

例如,按照ObjC的使用习惯,我们往往会这么实现:

class Person: NSObject {
    @objc dynamic var isHealth = true
}

private var familyContext = 0
class Family: NSObject {

    var grandpa: Person

    override init() {
        grandpa = Person()
        super.init()
        print("爷爷身体状况: \(grandpa.isHealth ? "健康" : "不健康")")
        grandpa.addObserver(self, forKeyPath: "isHealth", options: [.new], context: &familyContext)
        DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 3) {
            self.grandpa.isHealth = false
        }
    }

    override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
        if let isHealth = change?[.newKey] as? Bool,
            context == &familyContext {
            print("爷爷身体状况发生了变化:\(isHealth ? "健康" : "不健康")")
        }
    }
}
复制代码

但实际上Swift 4通过闭包优化了KVO的实现,我们可以将上述例子改为:

class Person: NSObject {
    @objc dynamic var isHealth = true
}

class Family: NSObject {

    var grandpa: Person
    var observation: NSKeyValueObservation?

    override init() {
        grandpa = Person()
        super.init()
        print("爷爷身体状况: \(grandpa.isHealth ? "健康" : "不健康")")

        observation = grandpa.observe(\.isHealth, options: .new) { (object, change) in
            if let isHealth = change.newValue {
                print("爷爷身体状况发生了变化:\(isHealth ? "健康" : "不健康")")
            }
        }

        DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 3) {
            self.grandpa.isHealth = false
        }
    }
}
复制代码

这样看上去是不是友好了许多?

《WWDC-What's New in Foundation》专题上介绍 KVO 时,提到了observe会返回一个 NSKeyValueObservation对象,开发者只需要管理它的生命周期,而不再需要移除观察者,因此不用担心忘记移除而导致crash。

你可以试试将observation = grandpa.observe改为let observation = grandpa.observe,看看有什么不同?

  • 开发上的弊端

    在 ObjC 中我们几乎可以没有限制地对所有满足 KVC 的属性进行监听,而现在我们需要属性有 @objc dynamic 进行修饰。但在很多情况下,监听的类属性并不满足这个条件且无法修改。目前可行的一个方案是通过属性观察器来实现一套自己的类似替代。

    喵神在关于这一节的讲述,由于Swift版本较早,提出“一个可能可行的方案是继承这个类并且将需要观察的属性使用 `dynamic` 进行重写。”
    但实际上仅使用`dynamic`修饰是不够的,Swift 4开始还得配合`@objc`使用,但是继承后再添加`@objc`是无法编译通过的。(这一点也容易理解,因为父类对于ObjC来说,已经不是`NSObject`的子类了)
    复制代码

lazy


前面提到过lazy可以用来标示属性延迟加载,它还可以配合像 map 或是 filter 这类接受闭包并进行运行的方法一起,让整个行为变成延时进行的。在某些情况下这么做也对性能会有不小的帮助。

例如,直接使用 map 时:

let data = 1...3
let result = data.map {
    (i: Int) -> Int in
    print("正在处理 \(i)")
    return i * 2
}

print("准备访问结果")
for i in result {
    print("操作后结果为 \(i)")
}

print("操作完毕")
复制代码

其输出为:

// 正在处理 1
// 正在处理 2
// 正在处理 3
// 准备访问结果
// 操作后结果为 2
// 操作后结果为 4
// 操作后结果为 6
// 操作完毕
复制代码

而如果我们先进行一次 lazy 操作的话,我们就能得到延时运行版本的容器:

let data = 1...3
let result = data.lazy.map {
    (i: Int) -> Int in
    print("正在处理 \(i)")
    return i * 2
}

print("准备访问结果")
for i in result {
    print("操作后结果为 \(i)")
}

print("操作完毕")
复制代码

此时的运行结果:

// 准备访问结果
// 正在处理 1
// 操作后结果为 2
// 正在处理 2
// 操作后结果为 4
// 正在处理 3
// 操作后结果为 6
// 操作完毕
复制代码

对于那些不需要完全运行,可能提前退出的情况,使用 lazy 来进行性能优化效果会非常有效。

Log与编译符号


有时候我们会想要将当前的文件名字和那些必要的信息作为参数一起打印出来,Swift 为我们准备了几个很有用的编译符号,用来处理类似这样的需求,它们分别是:

符号类型描述
#fileString包含这个符号的文件的路径
#lineInt符号出现处的行号
#columnInt符号出现处的列
#functionString包含这个符号的方法名字

这样,我们可以通过使用这些符号来写一个好一些的 Log 输出方法:

override func viewDidLoad() {
    super.viewDidLoad()

    detailLog(message: "嘿,这里有问题")
}

func detailLog<T>(message: T,
                  file: String = #file,
                  method: String = #function,
                  line: Int = #line) {
    #if DEBUG
    print("\((file as NSString).lastPathComponent)[\(line)], \(method): \(message)")
    #endif
}
复制代码

Optional Map


我们常常会对数组使用 map 方法,这个方法能对数组中的所有元素应用某个规则,然后返回一个新的数组。

例如希望将数组中的所有数字乘2:

let nums = [1, 2, 3]
let result = nums.map{ $0 * 2 }
print("\(result)")

// 输出:[2, 4, 6]
复制代码

但如果改成对某个Int?乘2呢?期望如果这个 Int? 有值的话,就取出值进行乘 2 的操作;如果是 nil 的话就直接将 nil 赋给结果。

let num: Int? = 3
// let num: Int? = nil

var result: Int?
if let num = num {
    result = num * 2
}
print("\(String(describing: result))")

// num = 3时,打印Optional(6)
// num = nil时,打印nil
复制代码

但其实有更优雅简洁的写法,那就是Optional Map。不仅在 Array 或者说 CollectionType 里可以用 map,在 Optional 的声明的话,会发现它也有一个 map 方法:

/// Evaluates the given closure when this `Optional` instance is not `nil`,
/// passing the unwrapped value as a parameter.
///
/// Use the `map` method with a closure that returns a non-optional value.
/// This example performs an arithmetic operation on an
/// optional integer.
///
///     let possibleNumber: Int? = Int("42")
///     let possibleSquare = possibleNumber.map { $0 * $0 }
///     print(possibleSquare)
///     // Prints "Optional(1764)"
///
///     let noNumber: Int? = nil
///     let noSquare = noNumber.map { $0 * $0 }
///     print(noSquare)
///     // Prints "nil"
///
/// - Parameter transform: A closure that takes the unwrapped value
///   of the instance.
/// - Returns: The result of the given closure. If this instance is `nil`,
///   returns `nil`.
@inlinable public func map<U>(_ transform: (Wrapped) throws -> U) rethrows -> U?
复制代码

如同函数说明描述,这个方法能让我们很方便地对一个 Optional 值做变化和操作,而不必进行手动的解包工作。输入会被自动用类似 Optinal Binding 的方式进行判断,如果有值,则进入 transform的闭包进行变换,并返回一个 U?;如果输入就是 nil 的话,则直接返回值为 nilU?

因此,刚才的例子可以改为:

let num: Int? = 3
// let num: Int? = nil

let result = num.map { $0 * 2 }
print("\(String(describing: result))")

// num = 3时,打印Optional(6)
// num = nil时,打印nil
复制代码

Delegate


在一开始在Swift写代理时,我们可能会这么写:

protocol MyProyocol {
    func method()
}

class MyClass: NSObject {
    weak var delegate: MyProyocol?
}
复制代码

然后就会发现编译器会提示错误'weak' must not be applied to non-class-bound 'MyProyocol'; consider adding a protocol conformance that has a class bound

这是因为 Swift 的 protocol 是可以被除了 class 以外的其他类型遵守的,而对于像 struct 或是 enum 这样的类型,本身就不通过引用计数来管理内存,所以也不可能用 weak 这样的 ARC 的概念来进行修饰。

因此想要在 Swift 中使用 weak delegate,我们就需要将 protocol 限制在 class 内,例如

protocol MyProyocol: class {
    func method()
}

protocol MyProyocol: NSObjectProtocol {
    func method()
}
复制代码

class限制协议只用在class中,NSObjectProtocol限制只用在NSObject中,明显class的范围更广,日常开发中都可以使用。

@synchronized


在ObjC日常开发中, @synchronized接触会比较多,这个关键字可以用来修饰一个变量,并为其自动加上和解除互斥锁,用以保证变量在作用范围内不会被其他线程改变。

但不幸的是Swift 中它已经不存在了。其实 @synchronized 在幕后做的事情是调用了 objc_sync 中的 objc_sync_enterobjc_sync_exit 方法,并且加入了一些异常判断。因此,在 Swift 中,如果我们忽略掉那些异常的话,我们想要 lock 一个变量的话,可以这样写:

private var isResponse: Bool {
    get {
        objc_sync_enter(lockObj)
        let result = _isResponse
        objc_sync_exit(lockObj)
        return result
    }

    set {
        objc_sync_enter(lockObj)
        _isResponse = newValue
        objc_sync_exit(lockObj)
    }
}
复制代码

字面量


所谓字面量,就是指像特定的数字,字符串或者是布尔值这样,能够直截了当地指出自己的类型并为变量进行赋值的值。比如在下面:

let aNumber = 3
let aString = "Hello"
let aBool = true
复制代码

在开发中我们可能会遇到下面这种情况:

public struct Thermometer {
    var temperature: Double
    public init(temperature: Double) {
        self.temperature = temperature
    }
}
复制代码

想要创建一个Thermometer对象,可以使用如下代码:

let t: Thermometer = Thermometer(temperature: 20.0)
复制代码

但是实际上Thermometer的初始化仅仅只需要一个Double类型的基础数据,如果能通过字面量来赋值该多好,比如:

let t: Thermometer = 20.0
复制代码

其实Swift 为我们提供了一组非常有意思的接口,用来将字面量转换为特定的类型。对于那些实现了字面量转换接口的类型,在提供字面量赋值的时候,就可以简单地按照接口方法中定义的规则“无缝对应”地通过赋值的方式将值转换为对应类型。这些接口包括了各个原生的字面量,在实际开发中我们经常可能用到的有:

  • ExpressibleByNilLiteral
  • ExpressibleByIntegerLiteral
  • ExpressibleByFloatLiteral
  • ExpressibleByBooleanLiteral
  • ExpressibleByStringLiteral
  • ExpressibleByArrayLiteral
  • ExpressibleByDictionaryLiteral

这样,我们就可以实现刚才的设想啦:

extension Thermometer: ExpressibleByFloatLiteral {
    public typealias FloatLiteralType = Double

    public init(floatLiteral value: Self.FloatLiteralType) {
        self.temperature = value
    }
}

let t: Thermometer = 20.0
复制代码

struct与class


  • 共同点
  1. 定义属性用于存储值
  2. 定义方法用于提供功能
  3. 定义下标操作使得可以通过下标语法来访问实例所包含的值
  4. 定义构造器用于生成初始化值
  5. 通过扩展以增加默认实现的功能
  6. 实现协议以提供某种标准功能
  • 类更强大
  1. 继承允许一个类继承另一个类的特征
  2. 类型转换允许在运行时检查和解释一个类实例的类型
  3. 析构器允许一个类实例释放任何其所被分配的资源
  4. 引用计数允许对一个类的多次引用
  • 两者的区别
  1. struct是值类型,class是引用类型。
  2. struct有一个自动生成的成员逐一构造器,用于初始化新结构体实例中成员的属性;而class没有。
  3. struct中修改 self 或其属性的方法必须将该实例方法标注为 mutating;而class并不需要。
  4. struct不可以继承,class可以继承。
  5. struct赋值是值拷贝,拷贝的是内容;class是引用拷贝,拷贝的是指针。
  6. struct是自动线程安全的;而class不是。
  7. struct存储在stack中,class存储在heap中,struct更快。
  • 如何选择?

一般的建议是使用最小的工具来完成你的目标,如果struct能够完全满足你的预期要求,可以多使用struct

柯里化 (Currying)


Currying就是把接受多个参数的方法进行一些变形,使其更加灵活的方法。函数式的编程思想贯穿于 Swift 中,而函数的柯里化正是这门语言函数式特点的重要表现。

例如有这样的一个题目:实现一个函数,输入是任一整数,输出要返回输入的整数 + 2。一般的实现为:

func addTwo(_ num: Int) -> Int {
    return num + 2
}
复制代码

如果实现+3,+4,+5呢?是否需要将上面的函数依次增加一遍?我们其实可以定义一个通用的函数,它将接受需要与输入数字相加的数,并返回一个函数:

func add(_ num: Int) -> (Int) -> Int {
    return { (val) in
        return val + num
    }
}

let addTwo = add(2)
let addThree = add(3)
print("\(addTwo(1))  \(addThree(1))")
复制代码

这样我们就可以通过Curring来输出模版来避免写重复方法,从而达到量产相似方法的目的。

Swift 中定义常量和 Objective-C 中定义常量有什么区别?


Swift中使用let关键字来定义常量,let只是标识这是一个常量,它是在runtime时确定的,此后无法再修改;ObjC中使用const关键字来定义常量,在编译时或者编译解析时就需要确定值。

不通过继承,代码复用(共享)的方式有哪些?


  • 全局函数
  • 扩展

实现一个 min 函数,返回两个元素较小的元素


func min<T : Comparable>(_ a : T , b : T) -> T {
    return a < b ? a : b
}
复制代码

两个元素交换


常见的一种写法是:

func swap<T>(_ a: inout T, _ b: inout T) {
    let tempA = a
    a = b
    b = tempA
}
复制代码

但如果使用多元组的话,我们可以这么写:

func swap<T>(_ a: inout T, _ b: inout T) {
    (a, b) = (b, a)
}
复制代码

这样一下变得简洁起来,并且没有显示的增加额外空间

为什么要说没有显示的增加额外空间呢?
喵神在说多元组交换时,提到了没有使用额外空间。但有一些开发者认为多元组交换将会复制两个值,导致多余内存消耗,这种说法有些道理,但蜗牛并没有找到实质性的证据,如果有同学了解,可以评论补充。
另外蜗牛在[Leetcode-交换数字](https://leetcode-cn.com/problems/swap-numbers-lcci/)中测试了两种写法的执行耗时和内存消耗,基本上多元组交换执行速度上要优于普通交换,但前者的内存消耗要更高些,感兴趣的同学可以试试。
复制代码

map与flatmap


都会对数组中的每一个元素调用一次闭包函数,并返回该元素所映射的值,最终返回一个新数组。但flatmap更进一步,多做了一些事情:

  1. 返回的结果中会去除nil,并且会解包Optional类型。
  2. 会将N维数组变成1维数组返回。

defer


defer 所声明的 block 会在当前代码执行退出后被调用。正因为它提供了一种延时调用的方式,所以一般会被用来做资源释放或者销毁,这在某个函数有多个返回出口的时候特别有用。

func testDefer() {
    print("开始持有资源")
    defer {
        print("结束持有资源")
    }

    print("程序运行ing")
}

// 开始持有资源
// 程序运行ing
// 结束持有资源
复制代码

使用defer会方便的将前后必要逻辑放在一起,增强可读性和维护,但是不正确的使用也会导致问题。例如上面的例子,在持有之前先判断是否资源已经被其他持有:

func testDefer(isLock: Bool) {
    if !isLock {
        print("开始持有资源")
        defer {
            print("结束持有资源")
        }
    }

    print("程序运行ing")
}

// 开始持有资源
// 结束持有资源
// 程序运行ing
复制代码

我们要注意到defer的作用域不是整个函数,而是当前的scope。那如果有多份defer呢?

func testDefer() {
    print("开始持有资源")

    defer {
        print("结束持有资源A")
    }

    defer {
        print("结束持有资源B")
    }

    print("程序运行ing")
}

// 开始持有资源
// 程序运行ing
// 结束持有资源B
// 结束持有资源A
复制代码

当有多个defer时,后加入的先执行,可以猜测Swift使用了stack来管理defer

String和NSString 的关系与区别


String是Swift类型,NSStringFoundation中的类,两者可以无缝转换。String是值类型,NSString是引用类型,前者更切合字符串的 "不变" 这一特性,并且值类型是自动多线程安全的,在使用上性能也有提升。

除非需要一些NSString特有的方法,否则使用String即可。

怎么获取一个String的长度?


仅仅是获取字符串的字符个数,可以使用count直接获取:

let str = "Hello你好"
print("\(str.count)") // 7
复制代码

如果想获取字符串占用的字节数,可以根据具体的编码环境来获取:

print("\(str.lengthOfBytes(using: .utf8))")    // 11
print("\(str.lengthOfBytes(using: .unicode))") // 14
复制代码

### [1, 2, 3].map{ $0 * 2 }都用了哪些语法糖


  1. [1, 2, 3]使用了字面量初始化,Array实现了ExpressibleByArrayLiteral协议。
  2. 使用了尾随闭包。
  3. 未显式声明参数列表和返回值,使用了闭包类型的自动推断。
  4. 闭包只有一句代码时,可省略return,自动将这一句的结果作为返回值。
  5. $0在未显式声明参数列表时,代表第一个参数,以此类推。

下面的代码能否正常运行?结果是什么?


var mutableArray = [1,2,3]
for i in mutableArray {
    mutableArray.removeAll()
    print("\(i)")
}
print(mutableArray)
复制代码

可以正常运行,结果如下:

1
2
3
[]
复制代码

为什么会调用三次?

因为Array是个值类型,它是写时赋值的,循环中mutableArray值一旦改变,for in上的mutableArray会产生拷贝,后者的值仍然是[1, 2, 3],因此会循环三次。

原创不易,文章有任何错误,欢迎批(feng)评(kuang)指(diao)教(wo),顺手点个赞👍,不甚感激!