Swift编程思想 Part 1:拯救小马


我常看见Swift的新手试着将它们的ObjC代码翻译成Swift。但是开始用Swift写代码的时候最难的事情并不是语法,而是思维方式的转变,去用那些ObjC里并没有的Swift新概念。

在这一系列的文章中,我们会拿一个ObjC代码做例子,然后在把它转成Swift代码的全程中引入越来越多的对新概念的讲解。

本文的第一部分内容:可选类型(optionals),对可选类型的强制拆包,小马,if let,guard和??。

ObjC代码

假设你想创建一个条目列表(比如过会儿要显示在一个TableView里)- 每个条目都有一个图标,标题和网址 - 这些条目都通过一个JSON初始化。下面是ObjC代码看起来的样子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@interface ListItem : NSObject
@property(strong) UIImage* icon;
@property(strong) NSString* title;
@property(strong) NSURL* url;
@end
@implementation ListItem
+(NSArray*)listItemsFromJSONData:(NSData*)jsonData {
     NSArray* itemsDescriptors = [NSJSONSerialization JSONObjectWithData:jsonData options:0 error:nil];
     NSMutableArray* items = [NSMutableArray  new ];
     for  (NSDictionary* itemDesc  in  itemsDescriptors) {
         ListItem* item = [ListItem  new ];
         item.icon = [UIImage imageNamed:itemDesc[@ "icon" ]];
         item.title = itemDesc[@ "title" ];
         item.url = [NSURL URLWithString:itemDesc[@ "title" ]];
         [items addObject:item];
     }
     return  [items copy];
}
@end

直译成Swift

想象一下有多少Swift的新手会把这段代码翻译成这样:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class ListItem {
     var  icon: UIImage?
     var  title: String =  ""
     var  url: NSURL!
     static func listItemsFromJSONData(jsonData: NSData?) -> NSArray {
         let jsonItems: NSArray =  try ! NSJSONSerialization.JSONObjectWithData(jsonData!, options: []) as! NSArray
         let items: NSMutableArray = NSMutableArray()
         for  itemDesc  in  jsonItems {
             let item: ListItem = ListItem()
             item.icon = UIImage(named: itemDesc[ "icon" ] as! String)
             item.title = itemDesc[ "title" ] as! String
             item.url = NSURL(string: itemDesc[ "url" ] as! String)!
             items.addObject(item)
         }
         return  items.copy() as! NSArray
     }
}

对Swift稍有经验的人应该会看出来这里面有很多代码异味。Swift的资深使用者读到这段代码之后就很可能心脏病突发而全部挂掉。

哪里会出错?

上面例子中第一个看起来像代码异味的地方就是一个Swift新手经常犯的坏毛病:到处使用隐式解析可选类型(value!),强制转型(value as! String)和强制使用try(try!)。

可选类型是你的朋友:它们很棒,因为它们能迫使你去思考你的值什么时候是nil,以及在这种情形下你该做什么。比如"如果没有图标的话我该显示什么呢?在我的TableViewCell里我该用一个占位符(placeholder)么?或者用另外一个完全不同的cell模板?"。

这些就是我们在ObjC中经常忘了考虑进去的用例,但是Swift帮助我们去记住它们,所以当值是nil的时候把它们强制拆包导致程序崩溃,把可选类型这个高级特性扔在一边不用,是很可惜的。

你绝不应该对一个值进行强制拆包,除非你真的知道你在干什么。记住,每次你加一个!去安抚编译器的时候,你就屠杀了一匹小马。

很可悲,Xcode是鼓励犯这种错误的,因为error提示到:"value of optional type ‘NSArray?’ not unwrapped. Did you mean to use ! or ?",修改提示建议...你在后面加一个!??。噢,Xcode,你是有多菜。

我们来拯救这些小马吧

那么我们该怎样去避开这些无处不在的糟糕的!呢?这儿有一些技巧:

  • 使用可选绑定(optional binding)if let x = optional { /* 使用 x */ }

  • 用as?替换掉as!,前者在转型失败的时候返回nil;你当然可以把它和if let结合使用

  • 你也可以用try?替换掉try!,前者在表达式失败时返回nil1。

好了,来看看用了这些规则之后我们的代码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
class ListItem {
     var  icon: UIImage?
     var  title: String =  ""
     var  url: NSURL!
     static func listItemsFromJSONData(jsonData: NSData?) -> NSArray {
         if  let nonNilJsonData = jsonData {
             if  let jsonItems: NSArray = ( try ? NSJSONSerialization.JSONObjectWithData(nonNilJsonData, options: [])) as? NSArray {
                 let items: NSMutableArray = NSMutableArray()
                 for  itemDesc  in  jsonItems {
                     let item: ListItem = ListItem()
                     if  let icon = itemDesc[ "icon" ] as? String {
                         item.icon = UIImage(named: icon)
                     }
                     if  let title = itemDesc[ "title" ] as? String {
                         item.title = title
                     }
                     if  let urlString = itemDesc[ "url" ] as? String {
                         if  let url = NSURL(string: urlString) {
                            item.url = url
                         }
                     }
                     items.addObject(item)
                 }
                 return  items.copy() as! NSArray
             }
         }
         return  []  // In case something failed above
     }
}

判决的金字塔

可悲的是,满世界的添加这些if let让我们的代码往右挪了好多,形成了臭名昭著的判决金字塔(此处插段悲情音乐)。

Swift中有些机制能帮我们做简化:

  • 将多个if let语句合并为一个:if let x = opt1, y = opt2

  • 使用guard语句,在某个条件不满足的情况下能让我们尽早的从一个函数中跳出来,避免了再去运行函数体剩下的部分。

当类型能被推断出来的时候,我们再用此代码把这些变量类型去掉来消除冗余 - 比如简单的用let items = NSMutableArray() - 并利用guard语句再确保我们的json确实是一个NSDictionary对象的数组。最后,我们用一个更"Swift化"的返回类型[ListItem]替换掉ObjC的NSArray:

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
class ListItem {
     var  icon: UIImage?
     var  title: String =  ""
     var  url: NSURL!
     static func listItemsFromJSONData(jsonData: NSData?) -> [ListItem] {
         guard let nonNilJsonData = jsonData,
             let json =  try ? NSJSONSerialization.JSONObjectWithData(nonNilJsonData, options: []),
             let jsonItems = json as? Array             else  {
                 * // If we failed to unserialize the JSON*
                 * // or that JSON wasn't an Array of NSDictionaries,*
                 * // then bail early with an empty array*
                 return  []
         }
         var  items = [ListItem]()
         for  itemDesc  in  jsonItems {
             let item = ListItem()
             if  let icon = itemDesc[ "icon" ] as? String {
                 item.icon = UIImage(named: icon)
             }
             if  let title = itemDesc[ "title" ] as? String {
                 item.title = title
             }
             if  let urlString = itemDesc[ "url" ] as? String, let url = NSURL(string: urlString) {
                 item.url = url
             }
             items.append(item)
         }
         return  items
     }
}

guard语句真心很赞,因为它在函数的开始部分就把代码集中在了对输入的有效性检查上,然后在代码剩下的部分中你就不用再为这些检查操心了。如果输入并非所想,我们就尽早跳出,帮助我们专注在事情都如所期的正轨上。

Swift难道不应该比ObjC更简洁么?

the-cake-is-a-lie.png

诱人的蛋糕子虚乌有!

嗯好吧,这代码好像是比它的ObjC版本更复杂。但是别愁,在即将到来的本文第二部分中我们会把它大幅度简化。

但更重要的是,这段代码比它ObjC的版本更加安全。实际上ObjC的代码更短只是因为我们忘了去执行一大堆的安全测试。即使我们的ObjC代码看起来蛮正常,它还是会在一些情况中立即崩溃,比如我们给它一个无效的JSON,或者一个并不是由string类型的dictionary的array构造出来的东西(比如创建JSON的那个人觉得"icon"这个key值对应的就是一个用来提示该条目是否有图标的Boolean,而不是一个String...)。在ObjC中我们仅仅是忘了去处理这些用例,因为ObjC没有引导我们去考虑这些情况,而Swift迫使我们去考虑。

所以ObjC代码当然更短:因为我们就是忘了去处理所有这些事情。如果你去不防止自己程序崩溃的话,把代码写的更短是很轻松的。开车的时候不留意路上的障碍当然轻松,但你就是这样把小马给撞死的。

结论

Swift是为了更高的安全性而设计。不要把所有东西都强制拆包而忽视了可选类型:当你在你的Swift代码中看见了一个!,你就总是要把它看做是一处代码异味,某些事情是要出错的。

在即将到来的本文第二部分中,我们会看到怎么让这个Swift代码更加简洁,并延续Swift的编程思想:将for循环和if-let搬走,替换成map和flatmap。

与此同时,安全驾驶,还有,没错,拯救小马!

  • 注意这个try?默默的将error丢弃了:用它的时候你不会知道更多关于为什么代码出错的原因。所以通常来说如果可能的话用do { try ... } catch { }替换掉try?会更好。但是在我们的例子中,因为我们希望在JSON因某种原因序列化失败时返回一个空数组,这里用try?是OK的。

  • 如你所见,我在代码的最后保留了一个as!(items.copy() as! NSArray)。有时杀死小马强制转型是OK的,如果你真的,真的知道返回的类型不是其他任何东西,就像这里的mutableArray.copy()。可是这种例外十分罕见,只有在你一开始的时候就认真思考过这个用例的情况下才可以接受(当心,如果那匹马死了,你将会受到良心的谴责)。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值