前段时间阅读了王巍的书<<Swift必备tips>>,根据自己
笔者关于Swift的笔记(持续更新): https://www.gitbook.com/book/hell03w/swift-note #1. 柯里化 在计算机科学中,柯里化(Currying)是把接受多个参数的函数变换成接受一个单一参数(最初函数的第一个参数)的函数,并且返回接受余下的参数且返回结果的新函数的技术。 2. 将 protocol 的方法声明为 mutating“Swift 的 protocol 不仅可以被 class 类型实现,也适用于 struct 和 enum。因为这个原因,我们在写给别人用的协议时需要多考虑是否使用 mutating 来修饰方法,比如定义为 mutating func myMethod()。Swift 的 mutating 关键字修饰方法是为了能在该方法中修改 struct 或是 enum 的变量,所以如果你没在协议方法里写 mutating 的话,别人如果用 struct 或者 enum 来实现这个协议的话,就不能在方法里改变自己的变量了。比如下面的代码” “另外,在使用 class 来实现带有 mutating 的方法的协议时,具体实现的前面是不需要加 mutating 修饰的,因为 class 可以随意更改自己的成员变量。所以说在协议里用 mutating 修饰方法,对于 class 的实现是完全透明,可以当作不存在的。” 3. Sequence“Swift 的 for...in 可以用在所有实现了 Sequence 的类型上,而为了实现 Sequence 你首先需要实现一个 IteratorProtocol。” 4. @autoclosure 和 ??<div> func logIfTrue(_ predicate: @autoclosure () -> Bool) { if predicate() { print("True") } } //这时候我们就可以直接写: logIfTrue(2 > 1) //??操作符就是通过@autoclosure实现的 func ??<T>(optional: T?,defaultValue: @autoclosure () -> T?) -> T? func ??<T>(optional: T?,defaultValue: @autoclosure () -> T) -> T`</pre></div> 5. @escapingSwift非常适合函数式编程,而闭包closure正是函数式编程的核心概念之一. 在swift中我们可以定义一个接受函数作为参数的函数,而在调用的时候,使用闭包的方式来传递这个参数是常见手段. 这种简单形式的闭包其实包含一种假设. 那就是参数中的block的内容会在dowork返回之前完成. 也就是说,对于block中的调用是同步的. 如果我们改变一下代码,将block放到Dispatch中去. 让它在dowork之后被调用,这时候我们就需要在block的类型前面加上 6. 操作符swift相比Objective-C支持了运算符的重载. 比如两个向量相加的重载: //向量`+`运算的重载,`-`,`*`,`/`运算符的重载和`+`类似,方法里定义自己需要的操作即可. func +(left: Vector2D,right: Vector2D) -> Vector2D { return Vector2D(x: left.x + right.x,y: left.y + right.y) } swift中支持我们自定义全新的运算符类型,但是可能复杂一点. 如果是系统定义好的运算符的重载,我们可以像上面所示那样实现. 但是系统未定义的运算符需要我们先对其声明,告诉编译器这个符号其实是一个操作符,添加如下代码: precedencegroup DotProductPrecedence { associativity: none higherThan: MultiplicationPrecedence } infix operator +*: DotProductPrecedence > precedencegroup: 定义一个操作符的优先级. > associativity: 定义结合律,多个运算同时出现,按照从左到右还是从右到左. > higherThan: 运算符的优先级. 也可以用lowerThan > infix: 表示要定义的是一个中位操作符,即前后都是输入;其他的修饰子还包括 prefix 和 postfix,不再赘述; 7. typealiastypealias用来给已经存在的类型定义别名,例如在一个计算距离的方法中,可能参数类型是CGPoint类型,从程序上没任何问题,但是程序的阅读性比较差,这时候可以将类型重命名成Distance类型,这样程序可读性增强. 用法: //定义CGPoint和Double的别名 typealias Location = CGPoint typealias Distance = Double //泛型的别名 typealias Worker<T> = Person<T> 8.可变参数类型可变参数函数指的是可以接受任意多个参数的函数,我们最熟悉的可能就是 NSString 的 -stringWithFormat: 方法了. swift中可变参数的用法: func sum(input: Int...) -> Int { //... } //输入的 input 在函数体内部将被作为数组 [Int] 来使用,让我们来完成上面的方法吧。当然你可以用传统的 for...in 做累加,但是这里我们选择了一种看起来更 Swift 的方式: //定义是巨简单的,在一个参数后写`...`,这个参数就变成了可变参数,在swift中,每个参数的名字和声明是在一起的,所以swift中可变参数不必须放在最后一位. func sum(input: Int...) -> Int { return input.reduce(0,+) } swift中多参数函数的限制: 同一个方法中只能有一个可变参数; 可变参数必须是同一类型的. 但是 extension NSString { convenience init(format: NSString,_ args: CVarArgType...) //... } 9.初始化方法顺序Swift的初始化方法需要保证类型的所有属性都别初始化. 所以初始化方法的调用顺序是很有讲究的. 在某个子类中,初始化方法的语句顺序并不是随意的,我们需要保证在当前子类的实例成员初始化完成后才能调用父类的初始化方法. class Cat { var name: String init() { name = "cat" } } class Tiger: Cat { let power: Int override init() { power = 10 super.init() name = "tiger" } } 一般来说,子类的初始化顺序是:
如果第三步不需要,则可以省略, class Cat { var name: String init() { name = "cat" } } class Tiger: Cat { let power: Int override init() { power = 10 // 如果我们不需要打改变 name 的话, // 虽然我们没有显式地对 super.init() 进行调用 // 不过由于这是初始化的最后了,Swift 替我们自动完成了 } } 10. Designated,Convenience,RequiredSwift有相比Objective-C有着超级严格的初始化方法. 目的就是为了: 安全! 在 Objective-C 中,init 方法是非常不安全的:没有人能保证 init 只被调用一次,也没有人保证在初始化方法调用以后实例的各个变量都完成初始化,甚至如果在初始化里使用属性进行设置的话,还可能会造成各种问题,虽然 Apple 也明确说明了不应该在 init 中使用属性来访问,但是这并不是编译器强制的,因此还是会有很多开发者犯这样的错误。 所以 Swift 有了超级严格的初始化方法。一方面,Swift 强化了 designated 初始化方法的地位。Swift 中不加修饰的 init 方法都需要在方法中保证所有非 Optional 的实例变量被赋值初始化,而在子类中也强制 (显式或者隐式地) 调用 super 版本的 designated 初始化,所以无论如何走何种路径,被初始化的对象总是可以完成完整的初始化. 在init中我们可以对let类型的实例变量进行赋值,这是初始化方法的特点. Swift中init只会被调用一次,因此在init中我们可以为常量进行赋值,不会引起任何线程安全的问题. 与designated初始化方法相对应的是在init前面加上 初始化方法永远遵循的两个原则:
可返回?的convenience方法: //convenience是可以返回你了值的 convenience init?(string URLString: String) > 对于某些我们希望子类中一定要实现的designated初始化方法,我们可以通过添加required关键字进行限制,强制子类对这个方法重写实现. 这样可以保证依赖某个designated初始化方法的convenience一直可以被使用. 其实不仅仅是对 designated 初始化方法,对于 convenience 的初始化方法,我们也可以加上 required 以确保子类对其进行实现。 11. static 和 class 关键字的用法Swift 中表示 “类型范围作用域” 这一概念有两个不同的关键字,它们分别是 static 和 class。 相同点:
不同点:
func printLog<T>(_ message: T,file: String = #file,method: String = #function,line: Int = #line) { #if DEBUG print("((file as NSString).lastPathComponent)[(line)],(method): (message)") #endif } 新版本的 LLVM 编译器在遇到这个空方法时,甚至会直接将这个方法整个去掉,完全不去调用它,从而实现零成本。 13. 溢出对于mac,早已进入64位时代,但是对于iphone64位才刚刚开始,所以在开发中一般要兼容32位和64位系统. 在 Swift 中我们一般简单地使用 Int 来表示整数,在 iPhone 5 和以下的设备中,这个类型其实等同于 Int32,而在 64 位设备中表示的是 Int64 (这点和 Objective-C 中的 NSInteger 表现是完全一样的,事实上,在 Swift 中 NSInteger 只是一个 Int 的 typealias。这就意味着,我们在开发的时候必须考虑同样的代码在不同平台上的表现差异,比如下面的这段计算在 32 位设备上和 64 位设备上的表现就完全不同. 在swift中溢出崩溃也是swift更加安全的一种体现,这样在开发中我们就会去避免溢出. 最后,如果我们想要其他编程语言那样的对溢出处理温柔一些,不是让程序崩溃,而是简单地从高位截断的话,可以使用溢出处理的运算符,在 Swift 中,我们可以使用以下这五个带有 & 的操作符,这样 Swift 就会忽略掉溢出的错误:
14. 都类型和容器Swift中常用的原生容器类型有三种,它们分别是Array,Dictoonary,Set. 它们都是泛型的,也就是说我们在一个集合中只能放同一个类型的元素. 问题来了,怎么才能放不同的元素呢?? 第一种方法: 把值类型隐式转换成Any类型,但是这样的转换会造成信息损失,我们从容器中取值时候只能得到信息完全丢失后的结果,在使用的时候还需要进行一次类型转换. 我们可以添加任意类型的值,也可以转换成任意类型的值,编译器也不会给我们任何警告信息,这是非常危险的事情. // Any 类型可以隐式转换 let mixed: [Any] = [1,"two",3] // 转换为 [NSObject] let objectArray = [1 as NSObject,"two" as NSObject,3 as NSObject] 第二种方法: 我们要添加到容器中的值肯定符合某些特定的特征 这些容易也支持添加实现了同一协议的类型的对象. 比如上面的例子如果我们希望的是打印出容器内的元素的 description,可能我们更倾向于将数组声明为 [CustomStringConvertible] 的: import Foundation let mixed: [CustomStringConvertible] = [1,3] for obj in mixed { print(obj.description) } 这种方法也会损失一部分类型信息,但是对于Any或者AnyObject类型还是改善了很多的. 第三种方法: 使用enum可以带有值的特点,将类型信息封装到特定的enum中,例如: enum IntOrString { case IntValue(Int) case StringValue(String) } let mixed = [IntOrString.IntValue(1),IntOrString.StringValue("two"),IntOrString.IntValue(3)] for value in mixed { switch value { case let .IntValue(i): print(i * 2) case let .StringValue(s): print(s.capitalized) } } 这种方法我们可以在编译时保留不同的类型信息. 15. 字面量表达字面量时而有意思的东西,哎Swift中Array和Dictionary在使用简单的描述赋值时候也是使用的字面量: let anArray = [1,2,3] let aDictionary = ["key1": "value1","key2": "value2"] Swift为我么提供了一组非常有意思的协议,使用字面量来表达特定的类型. 对于那些实现了字面量表达协议的类型,在提供字面量赋值的时候,就可以简单的按照协议中的规则无缝对应的通过赋值的方式将值类型表达为对应的类型,这些类型包含了原生的字面量,在实际开发中常用的有:
如果想通过String赋值来生成Person对象,而已这样写这个类,如下所示.首先要实现ExpressibleByStringLiteral协议,而这个协议还要求ExpressibleByExtendedGraphemeClusterLiteral和ExpressibleByUnicodeScalarLiteral这两个协议,因此Person要实现这三个协议. 因为是designated初始化方法,因此需要使用 class Person: NSObject,ExpressibleByStringLiteral { let name: String init(name: String) { self.name = name } required convenience init(stringLiteral value: String) { self.init(name: value) } required convenience init(extendedGraphemeClusterLiteral value: String) { self.init(name: value) } required convenience init(unicodeScalarLiteral value: String) { self.init(name: value) } } //使用字面量初始化一个类型 let person: Person = "walden" 下面是使用enum包装不同数据类型的例子,直接使用不同类型的字面量给enum赋值,可以放方便的放在容器中,需要的时候可以取出enum的关联值. enum Gather: ExpressibleByStringLiteral,ExpressibleByIntegerLiteral { case strValue(String) case intValue(Int) // 两个初始化方法,统一赋值 init(_ intValue: Int) { self = .intValue(intValue) } init(_ strValue: String) { self = .strValue(strValue) } // ExpressibleByStringLiteral init(stringLiteral value: String) { self.init(value) } init(extendedGraphemeClusterLiteral value: String) { self.init(value) } public init(unicodeScalarLiteral value: String) { self.init(value) } // ExpressibleByIntegerLiteral public init(integerLiteral value: Int) { self.init(value) } } let gather1: Gather = "qweaaw" let gather2: Gather = 1234 print(gather1) print(gather2) switch gather2 { case .strValue(let value): print(value) case .intValue(let value): print(value) } “总结一下,字面量表达是一个很强大的特性,使用得当的话对缩短代码和清晰表意都很有帮助;但是这同时又是一个比较隐蔽的特性:因为你的代码并没有显式的赋值或者初始化,所以可能会给人造成迷惑:比如上面例子中为什么一个字符串能被赋值为 Person?你的同事在阅读代码的时候可能不得不去寻找这些负责字面量表达的代码进行查看 (而如果代码库很大的话,这不是一件容易的事情,因为你没有办法对字面量赋值进行 Cmd + 单击跳转)。 和其他 Swift 的新鲜特性一样,我们究竟如何使用字面量表达,它的最佳实践到底是什么,都还是在研究及讨论中的。因此在使用这样的新特性时,必须力求表意清晰,没有误解,代码才能经受得住历史考验。” 摘录来自: 王巍 (onevcat). “Swifter - Swift 必备 Tips (第三版)”。 iBooks. 16. 正则表达式我们可以使用自定义的 precedencegroup MatchPrecedence { associativity: none higherThan: DefaultPrecedence } infix operator =~: MatchPrecedence func =~(lhs: String,rhs: String) -> Bool { //判断左边的字符串是不是标准的正则表达式. } 17. ...和..<这两个操作符不但能够生成Int或者Double的集合,还能遍历ASCII码表,如下所以例子. let test = "helLo" let interval = "a"..."z" for c in test.characters { if !interval.contains(String(c)) { print("(c) 不是小写字母") } } 17. Any,AnyObject,AnyClass区别和联系17.1 Any与AnyObjectAnyObject 可以代表任何 class 类型的实例 Any 可以表示任意类型,甚至包括方法 (func) 类型 /// The protocol to which all types implicitly conform. public typealias Any = protocol<> /// The protocol to which all classes implicitly conform. @objc public protocol AnyObject { } 从以上可以看出Any和AnyObject都是协议而且,并且从Apple提供的注释中可以看出所有的类型都隱式实现了Any协议,所有的class都隱式实现了AnyObject协议。`</pre></div> 可以总结为:
AnyObject是一个协议,Any是零个协议!AnyObject用于任何类实例,而Any用于任何变量。 17.2 AnyClassAnyClass是AnyObject.Type的别名而已。 看一下AnyClass的定义: AnyClass乍一看没什么用处,对于单独的类型我们完全没必要关心它的元类型,但是元类型或者元编程的概念可以变得非常多的灵活和强大,我们在编写框架代码时候会非常方便. 在下面的这个例子中虽然我们是用代码声明的方式获取了 MusicViewController 和 AlbumViewController 的元类型,但是其实这一步骤完全可以通过读入配置文件之类的方式来完成的。而在将这些元类型存入数组并且传递给别的方法来进行配置这一点上,元类型编程就很难被替代了: class MusicViewController: UIViewController { } class AlbumViewController: UIViewController { } let usingVCTypes: [AnyClass] = [MusicViewController.self,AlbumViewController.self] func setupViewControllers(_ vcTypes: [AnyClass]) { for vcType in vcTypes { if vcType is UIViewController.Type { let vc = (vcType as! UIViewController.Type).init() print(vc) } } } setupViewControllers(usingVCTypes) > .Type 表示的是某个类型的元类型,而在 Swift 中,除了 class,struct 和 enum 这三个类型外,我们还可以定义 protocol。对于 protocol 来说,有时候我们也会想取得协议的元类型。这时我们可以在某个 protocol 的名字后面使用 18. 动态类型和多方法swift100tipes中,这一节所述已经过时,实验证明是可以动态查找到的. //code class Pet: NSObject { func desc() { print("Pet Type") } } class Cat: Pet { override func desc() { print("Cat Type") } } class Dog: Pet { override func desc() { print("Dog Type") } } class PrintPet { func printPet(_ cat: Cat) { cat.desc() } func printPet(_ dog: Dog) { dog.desc() } func printPet(_ pet: Pet) { pet.desc() } func printPet(_ pet1: Pet,pet2: Pet) { printPet(pet1) printPet(pet2) } } let pets = PrintPet() pets.printPet(Cat(),pet2: Dog()) //输出 Cat Type Dog Type 19.属性观察属性管擦汗是swift中很特殊的特性,利用属性观察我们可以在当前类型内监视对于属性的设定,并做一些响应. Swift中为我们提供两个属性观察的方法,他们分别是willSet和didSet. 在 willSet 和 didSet 中我们分别可以使用 newValue 和 oldValue 来获取将要设定的和已经设定的值。属性观察的一个重要用处是作为设置值的验证,比如上面的例子中我们不希望 date 超过当前时间的一年以上的话,我们可以将 didSet 修改. > 初始化方法对于属性的设定,以及在willSet和didSet中对属性的再次设定都不会触发属观察的调用,一般来说这会是我们需要的. 我们知道,在 Swift 中所声明的属性包括存储属性和计算属性两种。其中存储属性将会在内存中实际分配地址对属性进行存储,而计算属性则不包括背后的存储,只是提供 set 和 get 两种方法。在同一个类型中,属性观察和计算属性是不能同时共存的。也就是说,想在一个属性定义中同时出现 set 和 willSet 或 didSet 是一件办不到的事情。计算属性中我们可以通过改写 set 中的内容来达到和 willSet 及 didSet 同样的属性观察的目的。如果我们无法改动这个类,又想要通过属性观察做一些事情的话,可能就需要子类化这个类,并且重写它的属性了。重写的属性并不知道父类属性的具体实现情况,而只从父类属性中继承名字和类型,因此在子类的重载属性中我们是可以对父类的属性任意地添加属性观察的,而不用在意父类中到底是存储属性还是计算属性: class A { var number :Int { get { print("get") return 1 } set {print("set")} } } class B: A { override var number: Int { willSet {print("willSet")} didSet {print("didSet")} } } 调用 number 的 set 方法可以看到工作的顺序 let b = B() b.number = 0 // 输出 // get // willSet // set // didSet set 和对应的属性观察的调用都在我们的预想之中。这里要注意的是 get 首先被调用了一次。这是因为我们实现了 didSet,didSet 中会用到 oldValue,而这个值需要在整个set 动作之前进行获取并存储待用,否则将无法确保正确性。如果我们不实现 didSet 的话,这次 get 操作也将不存在。 20.final关键字final关键字可以用在class,func,var前面进行修饰,表示不允许对该内容进行继承或者修改操作. 权限控制: 给一段代码加上final关键字,意味着编辑器向你保证,这段代码不会再被修改,也意味着这段代码已经完备没有再被进行继承或者重写的必要,因此需要深思熟虑使用此关键字. 一般来说,不希望被继承或者重写会有以下几种情况: 1.类或者方法确实已经非常完备,不需要再被修改比如md5. 2.子类的继承和修改是危险的事情在子类继承或者重写某些方法后,可能做一些破坏性的事情,导致子类或者父类部分无法正常工作. 这时候需要使用final关键字来禁止子类修改. 3.为了父类中某些代码一定会被执行有时候需要父类中一些关键代码在被继承重写后必须执行(比如配置状态,认证等),否则可能导致运行错误. 而在一般的方法中,我们无法强制子类必须调用父类方法,此时可以通过final关键字实现(attribute((objc_requires_super)) oc中可通过此方法声明,但是swift中目前尚未有方法实现此功能): class Parent { final func method() { print("开始配置") // ..必要的代码 methodImpl() // ..必要的代码 print("结束配置") } func methodImpl() { fatalError("子类必须实现这个方法") // 或者也可以给出默认实现 } } class Child: Parent { override func methodImpl() { //..子类的业务逻辑 } } 21. lazy修饰符和lazy方法延时加载或者说延时初始化是很常用的优化方法,在构建和生成新的对象的时候,内存分配会在运行时耗费不少时间,如果有一些对象的属性和内容非常复杂的话,这个时间是不可忽略的. 一个基本的lazy属性 如下所示,需要声明变量类型(不声明的话就需要在闭包中声明返回值) lazy var str: String = { let str = "Hello" print("只在首次访问输出") return str }() 22. swift数组中map,Filter. Reduce的使用1. map> map函数能够被数组调用,它接收一个闭包作为参数,作用于数组中的每个元素. 闭包返回一个变换后的元素,最后将所有这些变换后的元素组成一个新的数组. 比如一个将数组中所有元素自身相加的需求,不使用map函数可能这么写: let numbers = [1,3] var sumNumbers = [Int]() for var number in numbers { number += number sumNumbers.append(number) } // [2,4,6] print(sumNumbers) 使用map函数可以这么写 // 1.可以看到我们甚至可以不再定义可变的数组直接用不可变的就可以 let numbers = [1,3] let sumNumbers = numbers.map { (number: Int) -> Int in return number + number } // 2.下面介绍简便写法 因为map闭包里面的类型可以自动推断所以可以省略 let sumNumbers1 = numbers.map { number in return number + number } // 3.可以省了return let sumNumbers2 = numbers.map { number in number + number } print(sumNumbers2) // [2,6] // 4.最终简化写法 let sumNumbers3 = numbers.map { $0 + $0 } //$0表示取第一个参数 > 注意 > map函数返回数组的元素类型不一定要与原数组相同,例如int类型的数组可以返回String的值. filter> filter可以取出数组中符合条件的元素,重新组成一个新的数组 fliter主要可以用来从数组中筛选中符合某些条件的值 let numbers = [1,3,5,6] let evens = numbers.filter { $0 % 2 == 0 } // [2,6] print(evens) reduce> map,filter,flatmap方法都是通过一个已存在的数组,生成一个新的,经过修改的数组. 然后有时候我们需要根据数组中所有的元素进行计算,返回一个新的值,这时候就需要用到 let numbers = [1,1,8,8] // reduce 函数第一个参数是返回值的初始化值 let tel = numbers.reduce("") { "($0)" + "($1)" } // 15188888888 print(tel) 23. protocol extension如题,swift2.0之后,我们可以直接给protocol添加extension. 这样我们可以给某些必须实现的协议在协议的层次上添加实现,这样在层次化结构的类中会非常有用. 整理一下扩展中的协议和类型中协议的调用规则:
24. where和模式匹配where关键字在swift中非常强大,但是往往容易被忽略. 这节整理一下where的使用场合. 1. case1用在switch的case语句中,判断某个值是否符合某个或者某些条件! name.forEach { switch $0 { case let x where x.hasPrefix("王"): print("(x)是笔者本家") default: print("你好,($0)") } } 2. case2用在普通的for循环中,判定某些符合条件的护具才能进入for循环. let num: [Int?] = [48,99,nil] let n = num.flatMap {$0} for score in n where score > 60 { print("及格啦 - (score)") } // 输出: // 及格啦 - Optional(99) 25. 多元组(Tuple)元组是swift中的新特性,还是能够带来很多方便的. 交换两个变量的值,使用tuple我们可以优雅的写: var a = 20 var b = 30 (a,b) = (b,a) 26. Optional Chaining使用Optional Chaining可以让我们摆脱很多不必要的判断和取值,但是在使用的时候需要格外小心陷阱!!! 陷阱如下!! extension Toy { func play() { //... } } //错误代码示例 let playClosure = {(child: Child) -> () in child.pet?.toy?.play() } 如上所示,这样的代码是没有意义的. 问题在于play()的调用上. 定义的时候我们没有写play()的返回,那么根据闭包的性质,闭包的返回结果就是()?或者说是Void?. 27. 值类型和引用类型swift的类型分为值类型和引用类型两种,值类型在传递和赋值时候将进行复制,而引用类型则只会使用引用对象的一个'指向'. Swift中的struct和enum定义的类型是值类型,使用class定义的为引用类型. 有意思的是,swift中所有的内建类型都是值类型: Int,Bool,String,Array,Dictionary等都是值类型. 值类型的优点是减少了堆上内存分配和回收的次数. Swift 的值类型,特别是数组和字典这样的容器,在内存管理上经过了精心的设计。值类型的一个特点是在传递和赋值时进行复制,每次复制肯定会产生额外开销,但是在 Swift 中这个消耗被控制在了最小范围内,在没有必要复制的时候,值类型的复制都是不会发生的。也就是说,简单的赋值,参数的传递等等普通操作,虽然我们可能用不同的名字来回设置和传递值类型,但是在内存上它们都是同一块内容。 如下: func test(_ arr: [Int]) { for i in arr { print(i) } } var a = [1,3] var b = a let c = b test(a) 如下的代码将会造成值类型的赋复制,a和b的地址将会不同. var a = [1,3] var b = a b.append(5) 虽然将数组和字典设计为值类型最大的考虑是为了线程安全,但是这样的设计在存储的元素或条目数量较少时,给我们带来了另一个优点,那就是非常高效,因为 "一旦赋值就不太会变化" 这种使用情景在 Cocoa 框架中是占有绝大多数的,这有效减少了内存的分配和回收。但是在少数情况下,我们显然也可能会在数组或者字典中存储非常多的东西,并且还要对其中的内容进行添加或者删除。在这时,Swift 内建的值类型的容器类型在每次操作时都需要复制一遍,即使是存储的都是引用类型,在复制时我们还是需要存储大量的引用,这个开销就变得不容忽视了。幸好我们还有 Cocoa 中的引用类型的容器类来对应这种情况,那就是 NSMutableArray 和 NSMutableDictionary。 所以,在使用数组合字典时的最佳实践应该是,按照具体的数据规模和操作特点来决定到时是使用值类型的容器还是引用类型的容器:在需要处理大量数据并且频繁操作 (增减) 其中元素时,选择 NSMutableArray 和 NSMutableDictionary 会更好,而对于容器内条目小而容器本身数目多的情况,应该使用 Swift 语言内建的 Array 和 Dictionary。 28. func的参数修饰符在声明一个swift方法的时候,我们一般不去指定参数前面的修饰符. 默认情况下参数修饰符是 方法一: 如下所示,定义新的变量接收参数,然后可以去修改参数带过来的值. func incrementor2(variable: Int) -> Int { var num = variable num += 1 return num } 方法二: inout修饰,如果想要在方法内部直接修改参数的值并回传到方法调用的变量,则可以使用 func incrementor(variable: inout Int) { variable += 1 } //方法调用 var luckyNumber = 7 incrementor(variable: &luckyNumber) print(luckyNumber) // luckyNumber = 8 > 方法调用使用的 29. 方法嵌套在swift中,方法嵌套成了一等公民. 我们可以将方法当做变量或者参数来传递了. 更甚至我们可以在一个方法中定义新的方法,这将给代码的层次结构和访问级别带来新的选择. 举两个方法嵌套的用途: 例子1: //如下代码所示,有的方法可能旨在这一个方法中能够用到,在oc中我们也不得不放在类的结构中,但是在swift中,我们可以将这样的方法放在方法中,嵌套方法的实现. 给代码结构带来了新的选择. 这样也会让代码更加的安全. func appendQuery(url: String,key: String,value: AnyObject) -> String { func appendQueryDictionary(url: String,value: [String: AnyObject]) -> String { //... return result } func appendQueryArray(url: String,value: [AnyObject]) -> String { //... return result } func appendQuerySingle(url: String,value: AnyObject) -> String { //... return result } if let dictionary = value as? [String: AnyObject] { return appendQueryDictionary(url,key,dictionary) } else if let array = value as? [AnyObject] { return appendQueryArray(url,array) } else { return appendQuerySingle(url,value) } } 例子2: //如下代码所示,swift虽然给我们提供了访问权限,但是有些方法我们不希望在其它地方直接使用. 这时候可以使用乔涛方法,通过传入不同的参数,返回不通过的方法模板. func makeIncrementor(addNumber: Int) -> ((inout Int) -> Void { func incrementor(inout variable: Int) -> Void { variable += addNumber; } return incrementor; } 30. Selectorselector是objective-c中的概念,swift中也支持类似的写法. func callMe() { //... } func callMeWithParam(obj: AnyObject!) { //... } let someMethod = #selector(callMe) let anotherMethod = #selector(callMeWithParam(obj:)) 需要注意的是,selector是oc runtime的概念,如果你的selector对应的方法只是私有的话(swift中的private关键字),在调用的时候会遇到一个 unreconized selector错误. 31. 单例单例算是在iOS开发中算是比较常用的一种设计模式. oc中我们都是这么写的: @implementation MyManager + (id)sharedManager { static MyManager * staticInstance = nil; static dispatch_once_t onceToken; dispatch_once(&onceToken,^{ staticInstance = [[self alloc] init]; }); return staticInstance; } @end 但是在swift中会有所不同. swift中推荐的写法如下: //使用static保证静态变量可以使用类名直接调用; 使用let保证变量只能被创建一次. class MyManager { static let shared = MyManager() private init() {} } 这种写法不仅简洁,而且保证了单例的独一无二。在初始化类变量的时候,Apple 将会把这个初始化包装在一次 swift_once_block_invoke 中,以保证它的唯一性。不仅如此,对于所有的全局变量,Apple 都会在底层使用这个类似 dispatch_once 的方式来确保只以 lazy 的方式初始化一次。 另外,我们在这个类型中加入了一个私有的初始化方法,来覆盖默认的公开初始化方法,这让项目中的其他地方不能够通过 init 来生成自己的 MyManager 实例,也保证了类型单例的唯一性。如果你需要的是类似 default 的形式的单例 (也就是说这个类的使用者可以创建自己的实例) 的话,可以去掉这个私有的 init 方法。 32. 条件编译在C系语言中,可以使用#if或者#ifdef之类的编译条件分支控制那些代码需要编译,哪些不需要. 但是swift中没有宏定义的概念,因此不能使用#ifdef这样的宏定义. 但是为了流程控制,Swift还是提供了几种简单的机制来根据需求定制编译内容的. //#if这一套内容实际上还是存在的,使用语法也和原来的相同. //但是这里的condition并不是任意的类型,苹果帮助我们内建了一些类型 #if <condition> #elseif <condition> #else #endif 33. 编译标记oc中使用 //MARK: 简单的标记可以加 `-` //TODO: 待完善的部分 //FIXME: 有问题的部分,需要修复 //WARNING: 警告,警示信息. #34. @UIApplicationMain 在oc中,程序从main.c函数开始,当我们创建一个iOS app项目时候,xcode会帮我们自动创建好这些代码,main是整个程序的入口,读起来也是简单明了. 但是在swift中,抛弃了main函数,唯一和main相关的是appdelegate中的 其实刚才swift中的app也是需要main函数的,只不过默认情况下@ApplicationMain帮我们自动生成而已. swift中也可以创建一个 //注释掉@ApplicationMain,main.swift中添加如下代码,编译是可以通过的. UIApplicationMain(Process.argc,Process.unsafeArgv,nil,NSStringFromClass(AppDelegate)) //我们也可以通过第三个参数将UIApplication替换成自己的子类. diamante如下: class MyApplication: UIApplication { override func sendEvent(event: UIEvent!) { super.sendEvent(event) print("Event sent: (event)"); } } UIApplicationMain(Process.argc,NSStringFromClass(MyApplication),NSStringFromClass(AppDelegate)) 这样每次发送事件,我们都可以监听到这个事件了. 35. 可选协议和协议扩展Objective-C中的 //不能像oc中那样使用一个@objc定义多个方法可选,swift中必须为每个方法单独定义. @objc protocol OptionalProtocol { @objc optional func optionalMethod() // 可选 func necessaryMethod() // 必须 @objc optional func anotherOptionalMethod() // 可选 } 一个不可避免的限制是,使用 @objc 修饰的 protocol 就只能被 class 实现了,也就是说,对于 struct 和 enum 类型,我们是无法令它们所实现的协议中含有可选方法或者属性的。另外,实现它的 class 中的方法还必须也被标注为 @objc,或者整个类就是继承自 NSObject。这对我们写代码来说是一种很让人郁闷的限制。 在swift2.0之后,我们有了另一种选择. 那就是使用protocol extension. 我们可以在声明一个procotol之后再用extension的方式给出部分方法默认实现. 这样这些方法在实际的类中就是可选实现的了. 36. 内存管理weak,unownd这部分还是非常重要的,但是解决方法也很简单. 代理使用weak声明,闭包使用[unowned self]包含. 可以解决绝大部分的问题. 37. @autoreleasepoolswift中的内存管理使用的是自动引用计数的那一套方法. 虽然在ARC下我们不需要手动的写retain,release,autorelease这样的代码,但是这些方法还是会被调用--只不过是编译器在编译时在合适的地方帮我们加入的. 相比在oc中使用关键字声明autoreleasepool,在swift中使用闭包来声明的. func autoreleasepool(code: () -> ()) swift中使用自动释放池的方法: func loadBigData() { if let path = NSBundle.mainBundle() .pathForResource("big",ofType: "jpg") { for i in 1...10000 { autoreleasepool { //此方法在swift.0中已经废弃. let data = NSData.dataWithContentsOfFile( path,options: nil,error: nil) NSThread.sleepForTimeInterval(0.5) } } } } 其实对于特定的例子,可以不需要加入自动释放. 在swift中更提倡的是使用初始化方法而不是像上面那样的类方法来生成对象. let data = NSData(contentsOfFile: path) 使用初始化方法的话,就不需要面临自动释放的问题了,每次在超过作用域之后,自动内存管理都将为我们处理好内存相关的事情. 38. 获取对象类型oc中可以使用 39. 自省程序设计时候会遇到一个问题,就是"我是谁",在程序设计中,这个问题涉及到自省. 向一个对象发出询问,以确定它是不是属于某个类,这样的操作称为自省. //在oc中通过如下代码判断 [obj1 isKindOfClass:[classA class]]; [obj2 isKindOfClass:[classB class]]; //同样的方法在oc中使用方法如下: obj1.isKind(of: ClassA.self) //true obj2.isKind(of: ClassB.self) //false 纯swift代码也是可以这样写的,使用oc的方法也支持自省判断,但是不支持struct的判断. class ClassA { } class ClassB: ClassA { } let obj1: AnyObject = ClassB() let obj2: AnyObject = ClassB() obj1.isKind(of: ClassA.self) // true obj2.isMember(of: ClassA.self) // false 为了快速确定类型,Swift 提供了一个简洁的写法:对于一个不确定的类型,我们现在可以使用 is 来进行判断。is 在功能上相当于原来的 isKindOfClass,可以检查一个对象是否属于某类型或其子类型。is 和原来的区别主要在于亮点,首先它不仅可以用于 class 类型上,也可以对 Swift 的其他像是 struct 或 enum 类型进行判断。使用起来是这个样子的: class ClassA { } class ClassB: ClassA { } let obj: AnyObject = ClassB() if (obj is ClassA) { print("属于 ClassA") } if (obj is ClassB) { print("属于 ClassB") } 另外,编译器将对这种检查进行必要性的判断:如果编译器能够唯一确定类型,那么 is 的判断就没有必要,编译器将会抛出一个警告,来提示你并没有转换的必要。 let string = "String" if string is String { // Do something } // 'is' test is always true 40. 输出格式化//swift中的输出,终于拜托了C语言输出的包袱,变成了这种样子. print("int:(a) double:(b) string:(c)") //但是有时候也是需要格式化输出的,let format = String(format:"%.2lf",b) 如果嫌这样写太麻烦,也可以为Double写一个扩展: extension Double{ func format(_ f: String) -> Stirng{ retrn String(format: "%(f)",self) } } 41. swift中的Options学习之后发现,Options真是个神奇的东西. 在 Objective-C中,我们可以这样定义一个enum,然后我们可以通过 &,| 等符号将多个合并在一起形成一个集合Options: typedef NS_OPTIONS(NSUInteger,UIViewAnimationOptions) { UIViewAnimationOptionLayoutSubviews = 1 << 0,UIViewAnimationOptionAllowUserInteraction = 1 << 1,UIViewAnimationOptionBeginFromCurrentState = 1 << 2,//... UIViewAnimationOptionTransitionFlipFromBottom = 7 << 20,} 在swift中相同的功能被映射为满足 public struct UIViewAnimationOptions : OptionSetType { public init(rawValue: UInt) static var layoutSubviews: UIViewAnimationOptions { get } static var allowUserInteraction: UIViewAnimationOptions { get } //... static var transitionFlipFromBottom: UIViewAnimationOptions { get } } 同时我们也可以参考已有的语法实现自己的 struct YourOption: OptionSet { let rawValue: UInt static let none = YourOption(rawValue: 0) static let option1 = YourOption(rawValue: 1) static let option2 = YourOption(rawValue: 1 << 1) //... } let op1:YourOption = [YourOption.option2,YourOption.option1] let op2:YourOption = [YourOption.option1,YourOption.option2] let op3 = YourOption.option2 op1.contains(YourOption.option1) //true op3.contains(YourOption.option2) //true op1 == op2 //true 42. Associated ObjectObjective-C中,经常会出现给已有的类添加扩展的属性,这个需求在swift中也是需要的,并且swift中也实现了相关的方法,并且限制更加的严格了. 在swift中我们可以这么写: class MyClass { } // MyClassExtension.swift private var key: Void? extension MyClass { var title: String? { get { return objc_getAssociatedObject(self,&key) as? String } set { objc_setAssociatedObject(self,&key,newValue,.OBJC_ASSOCIATION_RETAIN_NONATOMIC) } } } key 的类型在这里声明为了 Void?,并且通过 & 操作符取地址并作为 UnsafePointer<Void> 类型被传入。这在 Swift 与 C 协作和指针操作时是一种很常见的用法。关于 C 的指针操作和这些 unsafe 开头的类型的用法. 43. Lock只要说到多线程或者并发编程的代码,我们就很难绕开对于锁的讨论. Objective-C中使用这种方法加锁: - (void)myMethod:(id)anObj { @synchronized(anObj) { // 在括号内 anObj 不会被其他线程改变 } } 但是在swift中 func myMethod(anObj: AnyObject!) { objc_sync_enter(anObj) // 在 enter 和 exit 之间 anObj 不会被其他线程改变 objc_sync_exit(anObj) } 当然,如果比较喜欢以前的书写方式,也可以封装起来,这样写: func synchronized(_ lock: AnyObject,closure: () -> ()) { objc_sync_enter(lock) closure() objc_sync_exit(lock) } //这样调用 func myMethodLocked(anObj: AnyObject!) { synchronized(anObj) { // 在括号内 anObj 不会被其他线程改变 } } 44. 随机数的生成随机数的生成一直是程序猿要面临的大问题之一,在CPU时钟,进程线程所构成的世界中,是没有真正随机的. 而 //这是一个错误的示范 let diceFaceCount = 6 let randomRoll = Int(arc4random()) % diceFaceCount + 1 以上代码在iPhone5s上是完全没问题的,但是在iPhone5以下,有时候会崩溃. (程序猿最郁闷的就是有时候) 其实 Swift 的 Int 是和 CPU 架构有关的:在 32 位的 CPU 上 (也就是 iPhone 5 和前任们),实际上它是 Int32,而在 64 位 CPU (iPhone 5s 及以后的机型) 上是 Int64。arc4random 所返回的值不论在什么平台上都是一个 UInt32,于是在 32 位的平台上就有一半几率在进行 Int 转换时越界,时不时的崩溃也就不足为奇了。 一种安全的做法就是使用arc4random的升级版 let diceFaceCount: UInt32 = 6 let randomRoll = Int(arc4random_uniform(diceFaceCount)) + 1 print(randomRoll) 有时候我们需要返回某个范围中的整数,这时候可以封装一下: func random(in range: Range<Int>) -> Int { let count = UInt32(range.endIndex - range.startIndex) return Int(arc4random_uniform(count)) + range.startIndex } 45. 断言断言是在开发中一般用来检查输入参数是否满足一定条件,并对其进行论断.一般情况下我们可以使用if来做判断,但是这样会增加运行时的开销. 对于像判定输入是否满足某种特殊条件的运用情景,其实我们有更好的选择,那就是断言. swift中定义了一系列的断言,我们最常用的是: func assert(@autoclosure condition: () -> Bool,@autoclosure _ message: () -> String = default,file: StaticString = default,line: UInt = default) 断言的另一个优点是它是一个开发时的特性,只有在debug编译时才有效. 而在运行时是不会被编译执行的,因此断言并不会消耗运行时的性能呢. 这些特点使得断言成为面向程序员的在调试开发阶段非常合适的调试判断,而在代码发布的时候,我们也不需要刻意去将这些断言手动清理掉,非常方便。 46 fatalErrorfatalError和asset非常类似,都可以让程序崩溃 但是asset只能在debug环境下使用,release环境下asset经将会被禁用. 但是在晕倒确实因为错误输入导致无法使程序继续运行的时候,我们一般可以考虑以生成致命错误的方式来终止程序. //如下,当switch遇到无法继续执行的错误的时候,可以使用fatalError来生成一个致命错误. //需要注意的是,即使函数需要返回一个String结构体,但是如果执行fatalError之后,则无需再返回任何值. func check(someValue: MyEnum) -> String { switch someValue { case .Value1: return "OK" case .Value2: return "Maybe OK" default: // 这个分支没有返回 String,也能编译通过 fatalError("Should not show!") } } 在开发接口的时候,如果我们希望子类一定要重写某个方法,那么可以在父类中使用 class MyClass { func methodMustBeImplementedInSubclass() { fatalError("这个方法必须在子类中被重写") } } class YourClass: MyClass { override func methodMustBeImplementedInSubclass() { print("YourClass 实现了该方法") } } 47. 安全的资源组织方式iOS开发中,有很多使用字符串来指定某个资源的用法,比如图片,字体,nib,string等等,当我们在项目中直接使用string字符串来查找资源的时候很多时候会出错,如果资源名字错了一个字符就会导致程序错误甚至崩溃,这是很不好的体验. 在oc中我们可以通过宏定义统一命名的方式来解决,在swift中也可以通过将名字定义在统一的enum中缓解这个问题. 但是这并没有彻底解决名字变更带来的问题. 不过在swift中,根据项目内容自动化生成像是 48. Log输出log输出是程序开发中的重要部分,虽然它不直接涉及业务代码,但是却可以忠实的反应我们的程序是怎么工作的,以及记录程序运行过程中发生了什么.swift中最简单的方法就是 当时更多的时候,我们会希望在debug时候输出log而在release环境下禁止输出,因为print是非常消耗程序性能的. 这时候我们可以将输出封装在一个特定的方法中去. func printLog<T>(_ message: T,(method): (message)") #endif } 在新版本的LLVM编译器在遇到空方法的时候,甚至会直接将这整个方法去掉,完全不去调用它,从而实现零成本.
49.溢出64位和32位的区别:
iOS开发中32位机器上Int类型最大支持 50.属性访问控制Swift中由低到高提供了 private 让代码只能在当前作用域中被使用,fileprivate 表示代码只能在当前文件中被访问。但是对于一个严格的项目来说,精确的最小化访问控制级别对于代码的维护来说还是相当重要的。我们想让同一 module (或者说是 target) 中的其他代码访问的话,保持默认的 internal 就可以了。如果我们在为其他开发者开发库的话,可能会希望用一些 public 甚至 open,因为在 target 外只能调用到 public 和 open 的代码。public 和 open 的区别在于,只有被 open 标记的内容才能在别的框架中被继承或者重写。因此,如果你只希望框架的用户使用某个类型和方法,而不希望他们继承或者重写的话,应该将其限定为 public 而非 open。 51.泛型扩展Swift 对于泛型的支持使得我们可以避免为类似的功能多次书写重复的代码,这是一种很好的简化。而对于泛型类型,我们也可以使用 extension 为泛型类型添加新的方法。 与为普通的类型添加扩展不同的是,泛型类型在类型定义时就引入了类型标志,我们可以直接使用。 需要了解的是: 泛型是支持扩展的. (编辑:李大同) 【声明】本站内容均来自网络,其相关言论仅代表作者个人观点,不代表本站立场。若无意侵犯到您的权利,请及时与联系站长删除相关内容! |