技术分享|技术分享-swift防御编程


技术分享-swift防御编程

  • 1 前言
  • 2 防御性编程的习惯
  • 3. swift中一些需要注意的点
    • 3.1 可选类型 Optional Type
    • 3.2 OC默认非空声明对Swift的影响
    • 3.3 多个运算符结合时,请使用括号显式进行结合
    • 3.4 闭包中调用self要避免循环引用
    • 3.5 Swift与OC混编时反射问题
    • 3.6 Swift中String.count与OC中NSString.length不总是相同
  • 4 swift中的一些规范

公司每两周会组织一次团队内的技术学习会议,大家轮流分享自己开发中,一些遇到的问题,或则一些好的技术,会议的宗旨希望团队的大家共同进步。 前面开了很多次,但是我都没有做一个记录,主要是当时听懂了,觉得没必要记录,但是后面的开发中发现有些东西虽然当时听懂了,但是过后会比较容易忘记。所以决定后面的每一次技术交流会,我都会记录下来,方便后面忘记可以有资料找。
今天技术交流会的主题是swift防御编程
1 前言 为了开发可靠的软件,我们需要设计系统中的每个组件,已其尽可能的保护自己。防御性编程使我们能尽早的发现较小的问题,而不是等到它发展成为大灾难的时候才发现?
由于没有人能保证自己的程序是百分百没有bug,所以适度的防御会降低调试bug的时间。
  • 方法一:一旦碰到约定的异常,程序必须兼容处理,一定不能让程序Crash
  • 方法二: 一旦碰到预定异常,就抛出去,如果上层没有处理,则Crash
2 防御性编程的习惯
  • 奥卡姆剃刀原理:**如无必须,勿增实体。**不要浪费更多的东西去做,你可以用更少的东西做同样的事情,更多的变量,需要更多的精力维护,也更容易出问题。
  • 不要仓促的编写代码:写每一行代码需要三思而后行,考虑可能会出现什么样的错误?考虑所有可能出现的逻辑分支
  • 尽可能处理掉所有编译警告: 处理编译警告是一种优秀的习惯,编译警告可能隐含着某种错误
  • 检查所有返回值:检查所有的返回值,包括自己写的代码,不要一些异常污染正常执行
  • 审慎地进行强制转换:强制转换需要谨慎处理,不要盲目转换,对转换尽可能的说明清楚
  • 检查数值的边界:对数值的范围要做周密的判断,有多少历史教训是由边界造成的
3. swift中一些需要注意的点 目前swift的趋势已经很大了, 公司的项目已经在一年前,开始转向swift编程。所以对于swift编程,有一些需要注意的点。
3.1 可选类型 Optional Type
  1. 尽量避免声明隐式可选类型,除非能确定其使用时一定有值,例如:
// 结合@IBOutlet使用时可以用隐式可选类型 @IBOutlet weak var tableView: UITableView! // 能确定其使用时一定有值,可以用隐式可选类型 func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { let identifier = "cell" var cell: UITableViewCell! = tableView.dequeueReusableCell(withIdentifier: identifier) if cell == nil { cell = UITableViewCell(style: .default, reuseIdentifier: identifier) } cell.textLabel?.text = "Row \(indexPath.row)" return cell }

  1. 避免使用as!try!,如果对象没有值,使用,会导致程序直接Crash
  2. 慎对可选变量,使用强制解包,也就是谨慎使用
可选变量的值,可能是nil,如果对可选变量使用强制解包,需要明确上下文环境,是否会出现nil.特别可能变量是类的储存属性,并且在多线程环境下使用,请务必不要使用强制解包。
var test : String? print(test)//nil print(test!) //error

更好的使用方式,是使用前判定或则赋值给let变量(显示解包)
//使用前判定 if test == nil {} else{}//赋值给let变量 if let constant = optionVariable{} //多个可选变量 if let constant = optionaVar1 , constant2 = optionVar2{}

3.2 OC默认非空声明对Swift的影响 很多的公司原先可能都是OC的代码,后面慢慢转到swift变成,在这个过程中就有一个混编的过程。那么在混编的过程中我们对一些变量的使用也需要注意
【技术分享|技术分享-swift防御编程】OC如果整个文件都没有显示指定nullable或者nonnull,则所有属性、参数和返回值在swift中都默认为隐式可选类型,其值可能为nil,如果直接使用可能会知道程序直接崩溃(注意:公司项目发生过一起该问题的生产事件)
@interface tztStockInfo : NSObject @property(nonatomic, strong) NSString *stockCode; @property(nonatomic, strong) NSString *stockName; @property(nonatomic, assign) int stockType; @property(nonatomic, strong) NSString *stockProp; - (NSMutableDictionary*)GetStockDict; @end

如下面代码,在stockName为nil时会崩溃
extension tztStockInfo { /// 股票完整性检查 func isIntegral() -> Bool { return self.stockType != 0 && self.stockName.count > 0 && self.stockProp.count > 0; } }

OC转换成隐式可选类型,也需要判断非nil,或则使用if let来进行可选绑定来使用
// OC代码未使用nullable、nonnull声明,在Swift中会自动转化为隐式可选类型(OC使用nullable、nonnull,在Swift分别会转化为`可选类型`、`非可选类型`) @property(nonatomic, strong) NSString *stockCode; // OC转换的隐式可选类型,必须判断非nil if stockInfo.stockCode != nil { params = [ "stockCode": stockInfo.stockCode, ] }// OC转换的隐式可选类型,通过可选绑定使用 if let stockCode = stockInfo.stockCode { params = [ "stockCode": stockCode, ] }

新开发的OC代码建议通过宏NS_ASSUME_NONNULL_BEGIN配合nullable使用严格规范可返回值为nil的情况
#ifndef NS_ASSUME_NONNULL_BEGIN #define NS_ASSUME_NONNULL_BEGIN _Pragma("clang assume_nonnull begin") #endif #ifndef NS_ASSUME_NONNULL_END #define NS_ASSUME_NONNULL_END_Pragma("clang assume_nonnull end") #endif

如果需要每个属性或每个方法都去指定nonnull和nullable,是一件非常繁琐的事。苹果为了减轻我们的工作量,专门提供了两个宏:NS_ASSUME_NONNULL_BEGINNS_ASSUME_NONNULL_END。在这两个宏之间的代码,所有简单指针对象都被假定为nonnull,因此我们只需要去指定那些nullable的指针
3.3 多个运算符结合时,请使用括号显式进行结合
print(true|| false && false) print((true || false) && false) print(true|| (false && false))

第一行:看不出谁的运算级谁高
第二行:把||用括号括起来,增强或优先级
第三行:把&&用括号括起来,增强与优先级
打印结果:
true false true

可以看出&&的优先级高
再来看另一个案例:
public func stockKey(stockCode: String?, stockType: Int32) -> String { return stockCode ?? "" + "|\(stockType)" } let key0 = stockKey(stockCode: nil, stockType: 1)//|1 let key1 = stockKey(stockCode: "600061", stockType: 1)//600061 不符合预期

正确的代码应该是:
public func stockKey(stockCode: String?, stockType: Int32) -> String { return (stockCode ?? "") + "|\(stockType)" } let key0 = stockKey(stockCode: nil, stockType: 1)//|1 let key1 = stockKey(stockCode: "600061", stockType: 1)//600061|1 符合预期

3.4 闭包中调用self要避免循环引用 将捕获列表[weak self]放在闭包参数前
myFunctionWithEscapingClosure() { [weak self] error in // 可以这样使用 self?.doSomething() // 或者这样使用 guard let strongSelf = self else { return } strongSelf.doSomething() }

PS: Swift4.2之后可选绑定中的self不再作为保留关键字,可以光明正大这么写 guard let self = self else { return }
swift中还有一个字段[unowned self]也可以解决引用循环,但是不建议使用,因为swift中unowned子段相当于OC中的unsafe_unretained,如果对象被设置为nil,unowned表示的指针不会清空, 但是如果你继续访问的话,就会报野指针错误。
3.5 Swift与OC混编时反射问题
@objc public class TestClass: NSObject { }

技术分享|技术分享-swift防御编程
文章图片

OC代码中通过反射获取这个TestClass这个类对象第一种方式输出的cls是为空的,第二种方式是在类名前期需求拼接上Module名称我们看到拿到cls实例
在Swift的类中@ojbc后面加上自定义的类名,代码如下:
@objc(TestClass) public class TestClass: NSObject { }

加上@objc(TestClass)之后,OC中反射时获取Swift的Class我们就不需要关心Module名,底层处理方式和OC一样,这样就抹平了底层的差异性。
我们来验证下:
技术分享|技术分享-swift防御编程
文章图片

但是这样就有一个问题, swift支持在不同模块下面,创建名称一样的 文件,如果你这样修改之后,就不能这样做了。相当于放弃了swift这个特性
3.6 Swift中String.count与OC中NSString.length不总是相同 Swift中String的count属性返回的字符计数并不总是与包含相同字符的NSString的length属性相同。 虽然我们目前没有碰到过这个问题,但是我们需要有这个意思,知道这两种获取的值不一定相同
在Swift中如果希望得到NSString.length相同的长度,可以使用String.utf16.count
// é可以用单一的Unicode的标量U+00E9来表示,也可以通过e(U+0065)与 ?(U+0301)的Unicode的标量组合来表示。 let eAcute = "\u{E9}"// é let combinedEAcute = "\u{65}\u{301}"// e后面拼接上 ? print(combinedEAcute.count) // 1 print((combinedEAcute as NSString).length)// 2 print(combinedEAcute.utf16.count) // 2

4 swift中的一些规范
  1. 尽可能使用let(能使用let就不要使用var
  2. 在声明类方法和属性时,优先使用static而非calss,仅当需要在子类中覆盖该函数或属性的功能时才使用 class
  3. 如果该函数没有参数,没有副作用,只返回一些对象或则值,建议使用计算属性
struct Square { var side: CGFloat = 1.0 // 推荐 var girth: CGFloat { return side * 4 } // 不推荐 //func girth() -> CGFloat { //return side * 4 //} // 推荐 func halfGirth() -> CGFloat { self.side = side * 0.5 return side * 4 } // 不推荐 //var halfGirth: CGFloat { //self.side = side * 0.5 //return side * 4 //} }

  1. 使用有限集合(enum)的switch语句时,不要包含default。反之,switch语句的末尾用default涵盖其他情况。
    • 因为swift的枚举支持很多类型,比如字符串,我们在使用字符串的作为type时,字符串可以表示无限的情况,这时候我们应该加上default来涵盖其它情况
    • 如果type表示的是一个有限的枚举类型,我们可以不用加上default在末尾,这样如果你在拓展新功能,修改了枚举,而忘记修改switch语句,那么系统会报错,引导我们去修改对应的代码
  2. 展开可选项时,推荐使用guard 语句,而不是if语句,已减少代码中的嵌套缩进的数据,这样也可以增加我们代码的可读性
// 推荐 guard let monkeyIsland = monkeyIsland else { return } bookVacation(on: monkeyIsland) // 不推荐 if let monkeyIsland = monkeyIsland { bookVacation(on: monkeyIsland) } // 极其不推荐 if monkeyIsland == nil { return } bookVacation(on: monkeyIsland!)

总结:虽然讲的内容比较浅,但是大多都是我们开发中经常遇到的,一个优秀的程序员需要从细节做起。

    推荐阅读