Swift 开发必备 tips 阅读笔记 第一篇

Swift新元素

Posted by poos on February 26, 2019

tips

全3篇,如下,这是第一篇。

Swift 开发必备 tips 阅读笔记 第一篇,Swift 语言的新特性。

Swift 开发必备 tips 阅读笔记 第二篇,Objective-C 与 Swift 的一些特性过渡。

Swift 开发必备 tips 阅读笔记 三 最后一篇,Swift与开发环境及一些实践。

Swift新元素

柯里化(Currying)

Swift里可以将方法进行柯里化(Currying),把接受多个参数的方法进行一些变形,使其更加灵活。

之前的博客已经介绍过,不再赘述。

1
2
3
4
func addTo(_ adder: Int) -> (Int) -> Int {
    return { num in
        return num + adder
    }

将protocol的方法声明为mutating

Swift 的 mutating 关键字修饰方法是为了能在该方法中修改 struct 或是 enum 的变量,所以如果你 没在协议方法里写 mutating 的话,别人如果用 struct 或 enum 来实现这个协议的话,就不能在方法里改变自己的变量了。

1
2
3
protocol AnyProtocol {
    mutating func printName()
}

Sequence 序列

你使用某个类型遵守 IteratorProtocol 协议,那么这个类型即可使用 for … in 方法了;同时这个类型也可以使用 map,filter,reduce 这些高阶函数。

究其根本的话 for in 是这样的

1
2
3
4
var iterator = arr.makeIterator( )
while let obj = iterator .next() {
    print(obj)
}

tuple 元组

元组的使用,例如交换两个值,例如返回两个值

1
2
3
4
5
//1
(a, b) = (b, a)

//2
func calc() -> (Int, Int)

@autoclosure 和 ??

例如一个函数接受一个闭包,虽然可以使用尾随闭包的特性:

logIfTrue{2 > 1}

但是,要么是书写起来十分麻烦,要么是表达上不太清晰,看起来都让人生气。于 是@autoclosure登场了。我们可以改换方法参数,在参数名前面加上@autoclosure 关键字:

1
2
3
4
5
func logIfTrue(_ predicate: @autoclosure () -> Bool) {
    if predicate() {
        print("True" )
    }
}

这时候我们就可以直接写:

logIfTrue(2 > 1)

@escaping

逃逸闭包的用法。注意适时使用 [weak self]。

Optional Chaining

注意一点,连续可选值调用时候,判断最终调用是否成功容易发生错误:

这是错误代码

let playClosure = {(child: Child) -> () in child.pet?.toy?.play()}

这样的代码是没有意义的! 问题在于对于 play() 的调用上。定义的时候我们没有写play() 的返回,这表示这个方法返回 Void (或者写作一对小括号(), 它们是等价的)。但是,经过 Optional Chaining以后 我们得到的是一个Optional的结果。也就是说,我们最后得到的应该是这样一个 closure :

let playClosure = {(child: Child) -> ()? in child. pet? . toy?. play()}

这样调用的返回将是一-个()? (或者写成Void? 会更清楚- -些), 虽然看起来挺奇怪的,但这就是 事实。使用的时候我们可以通过Optional Binding来判定方法是否调用成功:

1
2
3
4
5
if let result: () = playClosure(xiaoming) {
    print("好开心~")
} else {
    print("没有玩具可以玩:(")
}

操作符

可以自定义操作符。可能跟 柯里化 擦出不一样的火花。

注意 尽量明了,避免歧义和可能的误解。因为一个不被公认的操作符是存在冲突风险和理解难度的, 所以我们不应该滥用这个特性。

func的参数修饰

1
2
3
func increaseIt(_ number: inout Int) {
    number += 1
}

字面量表达

除了参数初始化时候可以省略类型初始化。

另一种方式 通过实现协议即可使用 字面量初始化 自定义类型,例如用字符串初始化 enum 等。

下标

作为一门代表了先进生产力的语言,Swift 是允许我们自定义下标的。这不仅包含了对自己写的类 型进行下标自定义,也包括了对那些已经支持下标访问的类型(没错就是Array 和 Dictionay)进 行扩展。

subscript (index: Int) -> T

subscript ( subRange: Range) -> Slice

函数嵌套

如果一个函数只是被另一个函数调用,不妨尝试一下。单一功能的代码块会整合到一起。

命名空间

在 Swift 中,由于可以使用命名空间了,即使是名字相同的类型,只要是来自不同的命名空间的 话,都是可以和平共处的。和 C# 这样的显式在文件中指定命名空间的做法不同,Swift 的命名空 间是基于 module 而不是在代码中显式地指明,每个 module 代表了 Swift 中的一个命名空间。也 就是说,同一个 target 里的类型名称还是不能相同的。在我们进行 app 开发时,默认添加到 app 的主 target 的内容都是处于同一个命名空间中的,我们可以通过创建 Cocoa (Touch) Framework 的 target 的方法来新建一个 module,这样我们就可以在两个不同的 target 中添加同样名字的类型 了。

typealias

1
2
3
4
5
6
7
8
9
10
11
12
13
// This is OK
typealias Worker<T> = Person<T>


class Person<T> {}
typealias WorkId = String
typealias Worker = Person<WorkId>


protocol Cat { ... }
protocol Dog { ... }
typealias Pat = Cat & Dog

associatedtype

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 相当于一个抽象父协议,不能被直接遵守实现
protocol Animal {
    associatedtype F: Food
    func eat(_ food: F)
}

struct Tiger: Animal {
    func eat(_ food: Meat) {
        print("eat (meat)")
    }
}

struct Sheep: Animal {
    func eat(_ food: Grass) {
        print("eat (food)")
    }
}

可变参数函数

不必像 OC 一样,只能将可变参数设置在最后一位。

1
2
3
4
5
6
7
8
func myFunc (numbers: Int..., string: String) {
    numbers.forEach {
        for i in 0..<$0 {
        print("'(i + 1): (string)")
        }
    }
}
myFunc (numbers: 1, 2, 3, string: "hello" )

初始化方法顺序

  • 先初始化子类的成员变量常量
  • 调用 super.init
  • 视情况修改父类的成员变量

Designated, Convenience 和Required

  • designated,不加修饰的初始化方法,必须对非可选的属性进行设置,并且会显式或者隐式调用父类的方法。
  • convenience,便利的初始化方法,必须调用当前类的 designated 方法。如果在子类重写了当前调用的 designated 方法,那么子类就可以使用父类中相关的 convenience 方法了
  • 对于某些我们希望子类中一定实现的 designated 初始化方法,我们可以通过添加 required 关键字进行限制,强制子类对这个方法重写实现。这样做的最大的好处是 可以保证依赖于某个 designated 初始化方法的 convenience 一直可以被使用。

初始化返回nil

convenience init?(string URLString: String)

在新版本的Swift中,对于可能初始化失败的情况,我们应该始终使用可返回 nil 的初始化方法,而不是类型工厂方法。

static 和 class

很早版本(Swift 1.2)时候 class 也可被用于修饰全局静态的方法,现在已经只能用 static 来修饰。

class 仅用于声明一个类

多类型和容器

Array、Dictionary、Set

default参数

现在已经不能使用 = default,而需要指定具体的值。

1
2
3
func increasedNum(num: Int, add: Int = 1) -> Int {
    return num + add
}

正则表达式

使用 NSRegularExpression 做一个包装来使用。

模式匹配 switch case

switch case 支持的模式匹配使得可以写出如下的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
// - mark  ex1
let num: Int? = nil
switch num {
    case nil: print("nil")
    default:
    print("(num!)")
}

// - mark  ex2
let x=0.5
switch x {
    case -1.0...1.0: print("get")
    default: print("error")
}

// - mark  ex3
let contact = ("http://onevcat.com", "onev@onevcat. com")
let mailRegex: NSRegularExpression
let siteRegex: NSRegularExpression
mailRegex =
    try ~/"^([a-z0-9_ \\.-]+)@([\\da-z\\.-]+)\\.([a-z\\.]{2,6})$"
siteRegex =
    try ~/"^(https?:\\/\\V/)?([\\da-z\\.-]+)\\.([a-z\\.]{2,6})([V\\w \\.-]*)*\\/?$"
switch contact {
    case (siteRegex, mailRegex): print ("同时拥有有效的网站和邮箱")
    case (_ , mailRegex): print(" 只拥有有效的邮箱")
    case (siteRegex,_ ): print("只拥有有效的网站")
    default: print("嘛都没有")
}
//输出
//同时拥有网站和邮箱

.. 和 ..< Range 操作符

Range 操作。支持泛型。

1
2
3
4
5
6
7
8
9
let test = "helLo"
let interval = "a"..."z"
for c in test. characters {
    if !interval.contains(String(c)) {
        print("\(c)不是小写字母")
    }
}
//输出
// L不是小写字母

AnyClass, 元类型和 .self

表示任意的概念:Any,AnyObject,AnyClass。

1
2
3
4
5
6
7
8
9
10
11
12
13
//通过 .Type 得到一个元类型(Meta)
typealias AnyClass = Any0bject.Type

//例如
class A {
    static func print(){}
}

let typeA: A.Type = A.self
typeA.print()

let typeAA: AnyClass = A.self
(typeAA as! A.Type).print()

协议和类方法中的 Self

在声明协议时,我们希望在协议中使用的类型就是实现这个协议本身的类型的话,就需要使用返回 Self 进行指代。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
//例如下边
protocol IntervalType (
//..
/// Return rhs clamped to self . The bounds of the result, even
/// if it is empty, are always within the bounds of self
    func clamp(intervalToClamp: Self) -> Self

//...
]

//这是一个个人实现的例子
class MyClass: Copyable {
    var num=1
    func copy() -> Self {
        let result = type(of: self).init()
        result.num = num
        return result
    }
    required init() {

    }
}
//测试 如下
let object = MyClass()
object.num = 100
let new0bject = object.copy()
object.num = 1


print (object.num) // 1
print(new0bject.num) // 100

动态类型和多方法

Swift中我们虽然可以通过 dynamicType 来获取一个对象的动态类型(也就是运行时的实际类型,而非代码指定或编译器看到的类型)。但在使用中,Swift 现在却是不支持多方法的,也就是说,不能根据对象在动态时的类型进行合适的重载方法调用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
//这样实现很方便
class Pet {}
class Cat: Pet {}
class Dog: Pet {}
func printPet(_ pet: Pet) {
    print("Pet")
}
func printPet(_ cat: Cat) {
    print("Cat")
}
func printPet(_ dog: Dog) {
    print("Dog")
}

//但是注意这里的坑
func printThem(_ pet: Pet,_ cat: Cat) {
    printPet(pet)
    printPet(cat)
}
printThem(Dog(), Cat())
//Pet
//Cat

//打印时的Dog() 的类型信息并没有被用来在运行时选择合适的printPet(dog: Dog)版本的方法,
//而是被忽略掉,并采用了编译期间决定的Pet 版本的方法。因为Swift默认情况下是不采用动态
//派发的,因此方法的调用只能在编译时决定。
//要想绕过这个限制,我们可能需要进行通过对输入类型做判断和转换:

func printThem2(_ pet: Pet,_ cat: Cat) {
    if let aCat = pet as? Cat{
        printPet(aCat)
    } else if let aDog = pet as? Dog {
        printPet(aDog)
    }
    printPet(cat)
}

printThem2(Dog(), Cat())
//Dog
//Cat

属性观察

set, get; willSet, didSet

final

final关键字可以用在class, func 或者var 前面进行修饰,表示不允许对该内容进行继承或者重写操作。

通常取决于下边几点:

  • 权限控制
  • 类已经完善了
  • 为了某些方法必须执行
  • 性能考虑

lazy修饰符和lazy方法

延时加载或者说延时初始化是很常用的优化方法,在构建和生成新的对象的时候,内存分配会在运行时耗费不少时间,如果有-些对象的属性和内容非常复杂的话,这个时间更是不可忽略。

Reflection 和 Mirror

swfit 的反射功能相较于运行时要弱很多。且并没有公开官方文档,有可能在未来会被去除。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
struct Person {
    let name: String
    let age: Int
}
let xiaoMing = Person(name: "XiaoMing", age: 16)
let r = Mirror(reflecting: xiaoMing) // r是MirrorType
print("xiaoMing是\(r.displayStyle!)")
print("属性个数:\(r.children.count)")
for child in r.children {
    print("属性名:\(child.label),値: \(child.value)")
}
//輸出:
//xiaoMing是Struct
//属性个数:2
//属性名:name,値:XiaoMing
//属性名:age,値:16

AnyForwardCollection 是遵守 collectionType 协议的,因此我们可以简单地使用count 来获取元素的个数,而对于具体的代表属性的多元组,则使用下标进行访问。在对于我们的例子中,每个 Child 都是具有两个元素的多元组,其中第一个是属性名,第二个是这个属性所存储的值。需要特别注意的是,这个值有可能是多个元素组成嵌套的形式(例如属性值是数组或者字典的话,就是这样的形式)。

如果觉得一个个打印太过于麻烦,我们也可以简单地使用dump 方法来通过获取一个对象的镜像 并进行标准输出的方式将其输出出来。比如对上面的对象 xiaoMing:

1
2
3
4
5
dump(xiaoMing)
/输出:
// V Person
// name: XiaoMing
// age: 16

在这里因为篇幅有限,而且这部分内容很可能随着版本而改变,我们就不再一一介绍Mirror 的更详细的内容了。有兴趣的读者不妨打开Swift的定义文件并找到这个协议,里面对每个属性和方法的作用有非常详细的注释。

在现在的版本中,Swift 的反射特性并不是非常强大,我们只能对属性进行读取,还不能对其设 定,不过我们有希望能在将来的版本中获得更为强大的反射特性。

隐式解包 Optional

!来隐式解包。是因为跟 OC 桥接的历史遗留,所以存在这个东东。

在 Apple 的不断修改 (我相信这是一件消耗大量人月的手工工作) 下,在 Swift 的正式 版本中,已经没有太多的隐式解包的 API 了。最近 Objective-C 中又加入了像是 nonnull 和 nullable 这样的修饰符,这样一来,那些真正有可能为 nil 的返回可以被明确定义为普通的 Optional 值,而那些不会是 Optional 的值,也根据情况转换为了确定的类型。现在比较常见的隐 式解包的 Optional 就只有使用 Interface Builder 时建立的 IBOutlet 了:

@IBOutlet weak var button: UIButton !

多重Optional

会有陷阱。

1
2
3
4
5
6
7
var aNil: String? = nil
var anotherNil: String?? = aNil
var literalNil: String?? = nil

// anotherNil 和 literalNil 不等价
// 因为 anotherNil 是一个 Optional 对象,不是 nil

Optional Map

array 等集合使用 .map 过滤 nil

Protocol Extension

Swift 2 开始引入的。

整理一下相关的规则的话:

  • 如果类型推断得到的是实际的类型。

那么类型中的实现将被调用;如果类型中没有实现的话,那么协议扩展中的默认实现将被使用

  • 如果类型推断得到的是协议,而不是实际类型。

并且方法在协议中进行了定义,那么类型中的实现将被调用; 如果类型中没有实现,那 么协议扩展中的默认实现被使用。否则(也就是方法没有在协议中定义),扩展中的默认实现将被调用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
protocol A2 {
    func method1() -> String
}
extension A2 {
    func method1() -> String {
        return "hi"
    }
    func method2() -> String {
        return "hi"
    }
}

struct B2: A2 {
    func method1() -> String {
        return "hello"
    }
    func method2() -> String {
        return "hello"
    }
}

let b2 = B2()
b2.method1() // hello
b2.method2() // hello

let a2 = b2 as A2
a2.method1() // hello
a2.method2() // hi

where 和模式匹配

早前版本在判断语句中 n > 3 即相当于一个 where 模式匹配。 Swift 3 语法省略了这一部分,不过源码还有一些端倪。

现在多用于 prottocol extension, 限定某些类的,默认协议方法

1
2
3
4
extension Sequence where Self. Iterator . Element : Comparable {
    public func sorted() -> [Self. Iter ator . Element ]
}

indirect 和嵌套 enum

当我们创建一些特殊的数据结构(链表,树,图等),我们可以使用可选值来指定头尾。但是如果用来表示一个空的链表(例如),就不得不再外层包一个 Optional。

这时候我们可以使用枚举来定义这种结构。

1
2
3
4
5
6
7
indirect enum LinkedList<Element: Comparable> {
    case empty
    case node(Element, LinkedList<Element>)
}

let linkedList = LinkedList.node(1, .node(2, .node(3, .node(4, .empty))))

非常帅,就是这样。

为枚举添加方法如下;

1
2
3
4
5
6
7
8
9
10
11
12
func removing(_ element: Element) -> LinkedList<Element> {
    guard case let .node(value, next) = self else {
        return . empty
    }
    return value == element ?
        next : LinkedList.node(value, next .removing(element))
}

let result = linkedList.removing(2)
print(result)
//1->3->4