Swift开发小记(含面试题)

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

声明


  • let和var前端

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

  • 类型标注面试

    声明常量/变量时能够增长类型标注,来讲明存储类型,例如算法

    var message: String
    复制代码

    若是不显示说明,Swift会根据声明时赋值自动推断出对应类型。通常不太须要标注类型,可是以前遇到过在某些状况下须要大量声明时,因为没有标注类型,Xcode直接内存爆了致使Mac死机,后来加上就行了。编程

  • 命名swift

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

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

    可是不推荐使用,骚操做太多容易闪着腰,Camel-Case还是更好的选择。api

    另外若是须要使用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(inout variable: 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 为咱们准备了几个颇有用的编译符号,用来处理相似这样的需求,它们分别是:

符号 类型 描述
#file String 包含这个符号的文件的路径
#line Int 符号出现处的行号
#column Int 符号出现处的列
#function String 包含这个符号的方法名字

这样,咱们能够经过使用这些符号来写一个好一些的 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
}
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),顺手点个赞👍,不甚感激!
相关文章
相关标签/搜索