1. string拼接
- swift中拼接字符串有两种方式
1.使用+号进行两个字符串的相加,但是要都是非可选类型的字符串,如:let str = "hello" + "world"
2.使用以下方法进行拼接,但是如果拼接的字符串是可选类型,会被默认添加Optional
let str1 = "hello "
let str2:String? = "world"
print("\(str1)\(str2)")
//打印结果:hello Optional("world")
如果不希望被添加Optional,只要让str2是非可选类型就可以了。
let str1 = "hello "
let str2:String? = "world"
//guard let str2 = str2 else { return } 这样写就不需要加 ?? "default"
print("\(str1)\(str2 ?? "default")")
//打印结果:hello world
- ?? : 是当前面的值为nil时,则使用后面的默认值。相当于: str2 ?? "default" -> str2 != nil ? str2 : "default"
这篇文章有讲?? 的实现 @AUTOCLOSURE 和 ??
2.字典
- 在类中声明初始化字典时,尽量不包含中文,不要声明过长。
- iOS浏览器的项目中有几个地方出现过在初始化字典时出现崩溃。
- 像这种初始化有默认值的字典时,可以考虑用plist文件存,然后懒加载plist文件中的数据。
3.dispatch_once
- swift3中dispatch_once方法被废弃了,所以在swift中推荐的单例的写法:
static let shared = MyClass()
fileprivate init() { }
- 或者自己对DispatchQueue进行拓展dispatch_once方法,比如:
public extension DispatchQueue {
private static var _onceTracker = [String]()
/**
Executes a block of code, associated with a unique token, only once. The code is thread safe and will
only execute the code once even in the presence of multithreaded calls.
- parameter token: A unique reverse DNS style name such as com.vectorform.<name> or a GUID
- parameter block: Block to execute once
*/
public class func once(token: String, block:()->Void) {
objc_sync_enter(self)
defer { objc_sync_exit(self) }
if _onceTracker.contains(token) {
return
}
_onceTracker.append(token)
block()
}
}
4. @discardableResult
- 如果一个方法是返回值,但是你调用的时候没有使用其返回值编译时会报警告,
可以在方法前添加@discardableResult对方法进行修饰则可以消除警告。
5.@escaping逃逸闭包
- 在之前的swift版本中,一个函数的参数的闭包的捕捉策略默认就是@escape,所以如果要明确这个闭包是一个非逃逸闭包需要显示的添加声明@noescape。简单的说就是如果这个闭包是在这个函数结束前就会被调用,就是非逃逸的,即noescape。
- 如果这个闭包是在函数执行完后才被调用,调用的地方超过了这函数的范围,所以叫逃逸闭包。
- swift3已经将@noescape废弃了,改成了函数的参数的闭包的捕捉策略默认就是@noescape,如果这个闭包是逃逸的则需要在闭包参数前添加@escape。
- 需要添加逃逸@escaping的情况有:
var myBlock: ()->()?
func test(_ block: @escaping ()->()) {
self.myBlock = block
DispatchQueue.global().async {
block()
}
DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + Double(Int64(0.1 * Double(NSEC_PER_SEC))) / Double(NSEC_PER_SEC)) {
block()
}
}
-
注意点:关于 @escaping 最后还有一点想要说明。如果你在协议或者父类中定义了一个接受 @escaping 为参数方法,那么在实现协议和类型或者是这个父类的子类中,对应的方法也必须被声明为 @escaping,否则两个方法会被认为拥有不同的函数
6.defer延时调用
- 被defer包裹的代码块会在其作用域结束时被调用,可以用在确保退出时,将某些操作结束,常见的是:打开数据库后,将关闭数据库的代码写在defer中,这样可以确保不管什么情况下数据库都能被关闭。
大家可以看看下面这段代码,打印的顺序是什么样的?
class A: NSObject {
class func doSomething() {
defer {
print("firest defer!")
}
defer {
print("second defer!")
}
if true {
defer {
print("third defer!")
}
}
print("will return")
return test()
}
class func test() {
print("test func")
}
}
A.doSomething()
//打印结果:
third defer!
will return
test func
second defer!
firest defer!
从上面的打印可以看出,defer代码块会被压入栈中,待函数结束时弹出栈运行,并且是在return代码执行后执行的。
7.lazy
swift3.0除了给属性进行懒加载外,还可以配合像map或者filter这类接受闭包并进行运行的方法一起, 让整个行为变成延时进行的. 在某些情况下这么做也对性能会有不小的帮助. 例如, 直接使用map时:
let data = [1,2,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,2,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
//操作完毕
如果只取result中其中某一个值,那么只会会要取的值进行处理。
对于那些不需要完全运行, 可能提前退出的情况, 使用lazy来进行性能优化效果会非常有效.
8.class 与 static
这里直接看这篇文章去查看其中的区别。 STATIC 和 CLASS
- 另外补充一点就是:
-static修饰的不能被子类重写,class修饰的才能被子类重写,所以除了当你需要被子类重写的时候外,大多数情况都可以直接使用static
9.mutating
- 默认情况下,(struct、enum)的实例方法中是不可以修改值类型(struct、enum)的属性。
- Swift 的 protocol 不仅可以被 class 类型实现,也适用于 struct 和 enum
- 因此在协议中定义接口的时候最好都加上mutating进行修饰,如果你没在接口方法里写 mutating 的话,别人如果用 struct 或者 enum 来实现这个接口的话,就不能在方法里改变自己的变量了。
- 在使用 class 来实现带有 mutating 的方法的接口时,具体实现的前面是不需要加 mutating 修饰的,因为 class 可以随意更改自己的成员变量。所以说在接口里用 mutating 修饰方法,对于 class 的实现是完全透明,可以当作不存在的。
值类型的实例方法中,也可以直接修改self属性值。
struct Point {
var x: Int
var y: Int
func moveToPoint(point: Point) {
self.x = point.x // 报错:不能对值类型的属性进行修改
self.y = point.y // 报错:不能对值类型的属性进行修改
}
mutating func moveToPoint2(point: Point) {
self.x = point.x // 编译通过
self.y = point.y // 编译通过
}
//可变方法还可以对self进行修改,这个方法和moveToPoint2效果相同
mutating func moveToPoint3(x deltaX: Int, y deltaY: Int) {
self = Point(x:deltaX, y:deltaY)
}
}
10.subscript
- swift 还可以给class、struct、enum添加类似字典和数组的自定义下标访问方法。
- subscript例子看这里。
11.内联序列函数sequence
- 这里也直接看这个网址Swift - 内联序列函数sequence介绍(附样例)
12.weak和unowned
不管在什么语言里, 内存管理的内容都很重要。
Swift是自动管理内存的, 这也就是说, 我们不再需要操心内存的申请和分配. 当我们通过初始化创建一个对象时, Swift会替我们管理和分配内存. 而释放的原则遵循了自动引用计数(ARC)的规则: 当一个对象没有引用的时候, 其内存将会被自动回收. 这套机制从很大程度上简化了我们的编码, 我们只需要保证在合适的时候将引用置空(比如超过作用域, 或者手动设为nil等, 就可以确保内存使用不出现问题.)
但是, 所有的自动引用计数机制都有一个从理论上无法绕过的限制, 那就是循环引用(retain cycle)的情况.
什么是循环引用
虽然我觉得循环引用这样的概念介绍不太因该出现在这本书中, 但是为了更清晰地解释Swift中的循环引用的一般情况, 这里还是简单进行说明. 假设我们有两个类A和B, 它们之中分别有一个存储属性持有对方:
class A: NSObject {
let b: B
override init() {
b = B()
super.init()
b.a = self
}
deinit {
print("A deinit")
}
}
class B: NSObject {
var a: A? = nil
deinit {
print("B deinit")
}
}
在A的初始化方法中, 我们生成了一个B的实例并将其存储在属性中. 然后我们又将A的实例复制给了b.a. 这样a.b和b.a将在初始化的时候形成一个引用循环. 现在当有第三方的调用初始化了A, 然后及时立即将其释放, A和B两个类实例的deinit方法也不会被调用, 说明它们并没有被释放.
var obj: A? = A()
obj = nil
// 内存没有释放
因为即时obj不再持有A的这个对象, b中的b.a依然引用着这个对象, 导致它无法释放. 而进一步, a中也持有着b, 导致b也无法释放. 在将obj设为nil之后, 我们在代码里再也拿不到对于这个对象的引用了, 所以除非是杀掉整个进程, 我们已经永远也无法将它释放了. 多么悲伤的故事啊..
在Swift里防止循环引用
为了防止这种人神共愤的悲剧发生, 我们必须给编译器一点提示, 表明我们不希望它们互相持有. 一般来说我们习惯希望"被动"的一方不要去持有"主动"的一方. 在这里b.a里对A的实例的持有是由A的方法设定的, 我们在之后直接使用的也是A的实例, 因此认为b是被动的一方. 可以将上面的class B的声明改为:
class B: NSObject {
weak var a: A? = nil
deinit {
print("B deinit")
}
}
在var a前面加上了weak, 向编译器说明我们不希望持有a. 这时, 当obj指向nil时, 整个环境中就没有对A的这个实例的持有了, 于是这个实例可以得到释放. 接着, 这个被释放的实例上对b的引用a.b也随着这次释放结束了作用域, 所以b的引用也将归零, 得到释放. 添加weak后的输出:
A deinit
B deinit
可能有心的朋友已经注意到, 在Swift中除了weak以外, 还有另一个冲着编译器叫喊着类似的"不要引用我"的标识符, 那就是unowned. 它们的区别在哪里呢? 如果您一直写Objective-C过来的, 那么从表面的行为上来说unowned更像以前的unsafe_unretained, 而weak就是以前的weak. 用通俗的话说, 就是unowned设置以后即使它原来引用的内容已经被释放了, 它仍然会保持对被已经释放了的对象的一个"无效的"引用, 它不能是Optional值, 也不会被指向nil. 如果你尝试调用这个引用的方法或者访问成员属性的话, 程序就会崩溃. 而weak则友好一些, 在引用的内容被释放后, 标记为weak的成员将自动地变成nil(因此被标记为@weak的变量一定需要时Optional值). 关于两者使用的选择, Apple给我们的建议是如果能够确定在访问时不会已被释放的话, 尽量使用unowned, 如果存在被释放的可能, 那就选择用weak.
我们结合实际编码中的使用来看看选择吧. 日常工作中一般使用弱引用的最常见的场景有两个:
设置delegate时
在self属性存储为闭包时, 其中拥有对self引用时
前者是Cocoa框架的常见设计模式, 比如我们有一个负责网络请求的类, 它实现了发送请求以及接受请求结果的任务, 其中这个结果是通过实现请求类的protocol的方式来实现的, 这种时候我们一般设置delegate为weak:
// RequestManager.swift
class RequestManager: RequestHandler {
@objc func requestFinished() {
print("请求完成")
}
func sendRequest() {
let req = Request()
req.delegate = self
req.send()
}
}
// Request.swift
@objc protocol RequestHandler {
@objc optional func requestFinished()
}
class Request {
weak var delegate: RequestHandler!
func send() {
//发送请求
//一般来说会将req的引用传递给网络框架
}
func gotResponse() {
//请求返回
delegate?.requestFinished?()
}
}
req中以weak的方式持有了delegate, 因为网络请求是一个异步过程, 很可能会遇到用户不愿意等待而选择放弃的情况. 这种情况下一般都会讲RequestManager进行清理, 所以我们其实是无法保证在拿到返回时作为delegate的RequestManager对象是一定存在的. 因此我们使用了weak而非unowned, 并在调用前进行了判断.
闭包和循环引用
另一种闭包的情况稍微复杂一些: 我们首先要知道, 闭包中对任何其他元素的引用都是会被闭包自动是有的. 如果我们在闭包中写了self这样的东西话, 那我们其实也就在闭包内持有了当前的对象. 这里就出现了一个在实际开发中比较隐蔽的陷阱: 如果当前的实例直接或者间接地对这个闭包又有引用的话, 就形成了一个self->闭包->self的循环引用. 最简单的例子是, 我们声明了一个闭包用来以特定的形式打印self中的一个字符串:
class Person {
let name: String
lazy var printName: () -> () = {
print("The name is /(self.name)")
}
init(personName: String) {
name = personName
}
deinit {
print("Person deinit /(self.name)")
}
}
var xiaoMing: Person? = Person(personName: "XiaoMing")
xiaoMing!.printName()
xiaoMing = nil
// 输出:
// The name is XiaoMing. 没有被释放
printName是self的属性, 会被self持有, 而它本身又在闭包内持有self, 这导致了xiaoMing的deinit在自身超过作用域后还是没有被调用, 也就是没有被释放. 为了解决这种闭包内的循环引用, 我们需要在闭包开始的时候添加一个标注, 来表示这个闭包内的某些要素应该以何种特定的方式来使用. 可以将printName修改为这样:
lazy var printName: () -> () = {
[weak self] in
if let strongSelf = self {
print("The name is /(strongSelf.name)")
}
}
现在内存释放就正确了:
// 输出:
// The name is XiaoMing
// Person deinit XiaoMing
如果我们可以确定在整个过程中self不会被释放的话, 我们可以将上面的weak改为unowned, 这样就不再需要strongSelf的判断. 但是如果在过程中self被释放了而printName这个闭包没有被释放的话(比如 生成Person后, 某个外部变量持有了printName, 随后这个Person 对象被释放了, 但是printName已然存在并可能被调用), 使用unowned将造成崩溃. 在这里我们需要根据实际的需求来决定是使用weak还是unowned.
这种在闭包参数的位置进行标注的语法结构是将要标注的内容放在原来参数的前面, 并使用中括号括起来. 如果有多个需要标注的元素的话, 在同一个中括号内用逗号隔开, 举个例子:
//标注前
{ (number: Int) -> Bool in
//...
return true
}
//标注后
{ [unowned self, weak someObject] (number: Int) -> Bool in
//...
return true
}
13.curry特性
其他内容
- 苹果专门给swift开的博客
- swift版本升级的改动 apple/swift-evolution
- 想看更多swift相关的知识点可以在- Swifter-Swift 必备tips 这里看看,文章中很多都能在这里面找到。
- 还有Swift的编写风格可以看这里:Swift Style Guide