在这篇文章里我想跟大家谈谈有关我近来从Objective-C过渡到Swift的一些感受。我会尽可能的给大家一些意见,提示一些误区并比较一下在两种语言之间的差异。话不多说,让我们开门见山。
注意:本文讨论的开发环境为Xcode 6 beta 2版本。
单一文件结构 VS 接口-实现
最值得一提的一大改动便是在Objective-C中“[接口].h/[实现].m”这种文件结构被取缔了。
其实我本人是很支持这种文件结构的,因为通过接口文件来获取及共享类特性的方式相当安全而且简单,不过现在不得不面对它不复存在的现实了。
在Swift中并不存在接口与实现分割成两个文件的现象,我们仅需要依靠实现来构建一个类就行了(并且在写的时候甚至不可能添加关于可访问性的修改)。
如果对于这一改动感到无法忍受的话应注意以下事项:
最为明显的:靠直觉。
我们可以借助漂亮的文档来提高类的可读性。举个例子,我们可以把所有想作为public的要素全部挪到文件开头去,也可以采用扩展来区分public和private。
另一个很实用的办法就是在private的方法和变量命名前加一个下划线'_'作为前缀。
下面是混合了以上两种方案的示例:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
// Public
extension DataReader {
var
data { }
func readData(){
var
data = _webserviceInteraction()
}
}
// Private implementation
class DataReader: NSObject {
func _webserviceInteraction()->String{
// ...
}
}
|
虽然我们没办法修改类中各元素的可见性,不过我们可以试着让某些访问变得“困难一些”。
一个特殊的方法就是使用嵌套类来把private部分隐藏起来(至少是自动的隐藏),下面是例子:
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
|
import UIKit
class DataReader: NSObject {
// Public ***********************
var
data:String?{
get{
return
private.internalData}
}
init(){
private = DataReaderPrivate()
}
func publicFunction(){
private.privateFunc()
}
// Private **********************
var
private:DataReaderPrivate
class DataReaderPrivate {
var
internalData:String?
init(){
internalData =
"Private data!"
}
func privateFunc (){}
}
}
|
我们将private的实现放入一个private常量的实例中,然后用“正常”的类的实现来充当接口。不过private部分并非会真正的隐藏起来,只不过在访问的时候需要加上一个private关键字了:
1
2
|
let reader = DataReader()
reader.private.privateFunc()
|
问题来了:仅是为了最后要将这些private的部分隐藏起来要把代码写得这样怪异值得吗?
我的建议是在可见性的修改出来之前(苹果正在忙这个事),我们还是采用详细的文档或者多少用一点扩展来完成这个事。
常量和变量
在写Objective-C的时候我会很少的使用到const关键字,甚至于我知道有的数据时不会变的(好吧不要吐槽我)。然而在Swift中苹果建议开发者们多花点心思在使用常量(let)而不是变量(var)上。所以请注意要弄明白你的变量的具体要做什么。你会使用常量的频繁度将是你从未想象过的。
更加简化的写法
来看一下下面的两行代码并比较有何不同:
1
2
3
|
vs
|
在我最开始接触Swift的前两个星期我强迫自己不要在每一行代码最后都添加分号,现在我感到人生圆满(不过现在写Objective-C的时候我不会加分号了)。
类型推断可以直接根据变量的定义为其指派类型,相比较Objective-C这类冗杂的语言来说,在这里倒是可圈可点。
我们应该使用一致的命名方式,否则其他的开发者(包括你自己)就很难通过极其糟糕的命名来推测其类型:
1
|
let a = something()
|
更加合理的命名是这样的:
1
|
let a = anInt()
|
还有一个改动就是关于括弧号,他们不再需要配对了:
1
2
3
|
if
(a > b){}
vs
if
a > b {}
|
不过请记住,我们在括号中间写入的部分会被认为是一个表达式,在这里不总是代表这样写是对的。在变量绑定时我们不能像下面这样使用括号:
1
2
|
if
(let x = data){}
// Error!
if
let x = data {}
// OK!
|
使用类型判断和删除分号及括号并不完全必要,不过我们可以考虑一下用以上建议的方式来写Swift代码,这样的话会提高代码的可读性并且减少一些输入量。
可选值
有多少次我们困惑与函数的返回值该如何设置?我曾经使用过NSNotFound, -1, 0,自定义的返回值来表示返回为空。
现在有了可选值的出现很好的解决了返回值为空的问题,我们仅需要在数据类型的后面添加一个问号就可以了。
我们可以这样写:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
|
class Person{
let name:String
let car:Car?
// Optional value
init(name:String){
self.name = name
}
}
// ACCESSING THE OPTIONAL VALUE ***********
var
Mark = Person(name:
"mark"
)
// use optional binding
if
let car = Mark.car {
car.accelerate()
}
// unwrap the value
Mark.car?.accelerate()
|
这是个用了可选值来描述“某人有一辆车”的例子,它表示car这一特征可以是没有的,因为这表示某人没有车。
然后我们可以用optional binding或者unwrap来取得它的值。
如果对于一个属性没有设定为可选值,我们又没有为其赋值的话,编译器会立马不爽快的。
一旦初始化了之后便没有设定非可选属性的机会了。
所以我们应该事先考虑一下类的属性与其它部分的关系以及在类进行实例化的时候它们会发生什么变化。
这些改进彻底的改变了构思一个类的方式。
可选值的拆包
你会发现可选值这个东西难以理喻,因为你不会理解为什么编译器会提示你在使用之前对其进行拆包。
Mark.car?
我建议你把可选值当做一个结构体(当做结构体的话会好理解一些),其中包括了一个你所设定的值。不过外面包裹了其他东西(wrap)。如果里面的值有定义,你就可以进行拆包(unwrap)然后得到你所想得到的值。否则你就得到一个空值(nil)。
使用感叹号"!"来进行强制拆包而不管其中的值是否有定义,这样做是有风险的,因为如果里面的值没有定义的话应用会崩掉。
委托模式
经过多年的Objective-C和Cocoa代码编写我想大部分人都对使用委托模式养成了一种嗜好。注意了!我们还是可以继续保留这种嗜好的,下面是一个非常简单的例子:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
@objc protocol DataReaderDelegate{
@optional func DataWillRead()
func DataDidRead()
}
class DataReader: NSObject {
var
delegate:DataReaderDelegate?
var
data:NSData?
func buildData(){
delegate?.DataWillRead?()
// Optional method check
data = _createData()
delegate?.DataDidRead()
// Required method check
}
}
|
这里我们使用了一个简单的@optional来替换了使用respondToSelector检测委托方法是否存在。
1
|
delegate?.DataWillRead?()
|
请注意我们在协议之前必须加@obj前缀,因为后面使用了@optional。同时编译器也会在这里报一个警告的消息以防你没有加上@obj。
要实现协议的话,我们需要构建一个类来实现它然后用曾经在OC上用过的方式来指派。
1
2
3
4
5
6
7
8
9
10
11
12
13
|
class ViewController: UIViewController, DataReaderDelegate {
override func viewDidLoad() {
super
.viewDidLoad()
let reader = DataReader()
reader.delegate = self
}
func DataWillRead() {...}
func DataDidRead() {...}
}
|
目标-动作模式
另一常用的设计模式:目标-动作模式。我们仍然同样可以像在OC中使用它那样在Swift中实现它。
1
2
3
4
5
6
7
8
9
10
11
12
|
class ViewController: UIViewController {
@IBOutlet
var
button:UIButton
override func viewDidLoad() {
super
.viewDidLoad()
button.addTarget(self, action:
"buttonPressed:"
, forControlEvents: UIControlEvents.TouchUpInside)
}
func buttonPressed(sender:UIButton){...}
}
|
这里唯一不同的地方就是如何定义一个selector选择器。我们可以变形使用像下面这样的字符串来写方法原型:
1
|
Selector(
"buttonPressed:"
)
|
单件模式
简直又爱又恨。单件模式依旧是设计模式中最为常用的模式之一。
我们可以用GCD和dispatch_once来实现它,当然还可以用let关键字来实现线程安全。
1
2
3
4
5
6
7
8
9
10
11
12
|
class DataReader: NSObject {
class
var
sharedReader:DataReader {
struct Static{
static let _instance = DataReader()
}
return
Static._instance
}
...
}
|
我们来快速浏览一下这段代码:
1.sharedReader是一个静态的复合属性(我们也可以替换为方法)。
2.静态属性不允许在类被实现的时候重构,所以由于内部类型是被允许的,我们可以再这里加入一个结构体。
3._instance是一个常量,它不会被重写而且保证线程安全。
可以参考下面DataReader单例的用法:
1
|
DataReader.sharedReader
|
结构和枚举
Swift中的结构和枚举简直神乎其神,你根本不会在其他的语言里面找到像它们这样的。
它们支持方法:
1
2
3
4
5
6
7
8
9
10
11
12
13
|
struct User{
// Struct properties
let name:String
let ID:Int
// Method!!!
func sayHello(){
println(
"I'm "
+ self.name +
" my ID is: \(self.ID)"
)
}
}
let pamela = User(name:
"Pamela"
, ID: 123456)
pamela.sayHello()
|
如你所见在这里的结构体使用了初始化,而且这个是Swift自动创建的(我们可以添加一些自定的实现)。
枚举类型的语法比起我们用过的会稍难。
它的定义需要用到关键字case:
1
2
3
4
|
enum Fruit {
case
orange
case
apple
}
|
而且枚举并不局限于int型:
1
2
3
4
|
enum Fruit:String {
case
.orange =
"Orange"
case
.apple =
"Apple"
}
|
而且还可以用的更复杂一些:
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
|
enum Fruit{
// Available Fruits
case
orange
case
apple
// Nested type
struct Vitamin{
var
name:String
}
// Compound property
var
mainVitamin:Vitamin {
switch
self{
case
.orange:
return
Vitamin(name:
"C"
)
case
.apple:
return
Vitamin(name:
"B"
)
}
}
}
let Apple = Fruit.apple
var
Vitamin = Apple.mainVitamin
|
在上面我们为Fruit枚举类添加了一个内部类型Vitamin和一个复合的mainVitamin,并且这样的结构还可以根据枚举的值来进行初始化里面的元素……是不是已经感到眼花缭乱了?
可变与不可变类
在OC中我们总会用到可变以及不可变类,举个例子?NSArray和NSDictionary。在Swift里面我们不在像这样来区分数据了,只需要用常量和变量的定义来替代。
数据变量是可以变的而数组常量的值不可更改。所以请记下这个公式:
“let = immutable. var = mutable”.
块和闭包
我非常喜欢块的语法,因为它相当简单而且好记。
1
|
<br>
|
顺带提一下,因为有多年Cocoa的变成习惯所以有时候我会偏爱于用块来替代简单的委托作业。这是很灵活快捷的方式,而且非常实用。
Swift中与块相对的是闭包。闭包的作用极为强大而且苹果在将其简单化上做得很棒,很容易就可以实现。
官方文档里的示例只能说让我无言以对。
它是这样定义的:
1
2
3
|
reversed = sort(names, { (s1: String, s2: String) -> Bool
in
return
s1 > s2
})
|
然后是这样重构的:
1
|
reversed = sort(names, >)
|
所以,由于类型判断的存在我们能以不同的方式来实现一个闭包、速写参数($0, $1)和直接操作函数(>)。
在这篇文章里我打算遍历一下闭包的用法不过此前我想对如何获取闭包中的值说几句。
在OC里面,我们定义一个变量像_block这样,以方便我们想预备将它压入块。不过在闭包里面这些都没有必要。
我们可以使用和修改周围的值。事实上闭包被设计得非常聪明,足够它获取外部的元素来给内部使用。每个被获取的元素会作为拷贝或者是引用。如果闭包会修改它的值则创建一个引用,否则就生成一份拷贝。
如果闭包引用了一个包含或调用了闭包本身的实例,我们就会进入一个循环强引用。
来看一下例子:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
class Person{
var
age:Int = 0
@lazy
var
agePotion: (Int) -> Void = {
(agex:Int)->Void
in
self.age += agex
}
func modifyAge(agex:Int, modifier:(Int)->Void){
modifier(agex)
}
}
var
Mark:Person? = Person()
Mark!.modifyAge(50, Mark!.agePotion)
Mark = nil
// Memory Leak
|
这个agePotion闭包引用了它本身,而对当前的实例保证了强引用。同时实例保持了一个队闭包的引用。BOOM~~~我们进入了一个循环强引用。
为了避免这种情况我们需要使用获取列表Capture List.这个列表维护了我们想使用的实例的无主弱引用。语法十分简单,只需要在闭包定义前添加 [unowned/strong self] 就行,然后你会得到一个无主的弱引用来替代以前的强引用。
1
2
3
4
|
@lazy
var
agePotion: (Int) -> Void = {
[unowned self](agex:Int)->Void
in
self.age += agex
}
|
无主弱引用
在OC里面我们知道弱引用是怎么运作的。在Swift里面也一样,基本没什么变化。
所以什么是无主引用呢。我仔细的看了看这个关键词的介绍,因为它很好的说明了类间的关系的定义。
让我们来简单的描述一下人Person与银行账户BankAccount间的关系:
1.一个人可以拥有一个银行账户(可选)。
2.一个银行账户属于一个人(必须)。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
We can describe
this
relation
with
code:
class Person{
let name:String
let account:BankAccount!
init(name:String){
self.name = name
self.account = BankAccount(owner: self)
}
}
class BankAccount{
let owner:Person
init(owner:Person){
self.owner = owner
}
}
|
这写关系会创建一个引用循环。第一种解决方案添加了一个弱引用给“BankAccount.owner”属性。不过还用了一个无主引用作为约束:这个属性必须有一个值,不能为空(之前的列表里的第二点令人满意)。
好了,关于无主引用没有更多要说的了。其实它恰好就像一个没有为所指引用增加作用的弱引用,不过为其保证了一个不为空的值。
总结
我不得不承认我偶尔会在编译器报错的时候无能为力的看着它心想:WTF。
我在Swift耗费的实验和测试的次数越多我就会越清晰的明白其价值所在。在我们能舒坦的使用它之前脱离OC去接触Swift开发和训练需要相当大的兴趣的。