技术分享-swift防御编程

Posted GY-93

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了技术分享-swift防御编程相关的知识,希望对你有一定的参考价值。

技术分享-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变成,在这个过程中就有一个混编的过程。那么在混编的过程中我们对一些变量的使用也需要注意

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 
 

OC代码中通过反射获取这个TestClass这个类对象第一种方式输出的cls是为空的,第二种方式是在类名前期需求拼接上Module名称我们看到拿到cls实例

在Swift的类中@ojbc后面加上自定义的类名,代码如下:

@objc(TestClass)
public class TestClass: NSObject 
 

加上@objc(TestClass)之后,OC中反射时获取Swift的Class我们就不需要关心Module名,底层处理方式和OC一样,这样就抹平了底层的差异性。

我们来验证下:

但是这样就有一个问题, 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 = "\\uE9"   // é
let combinedEAcute = "\\u65\\u301"  // 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!)

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

以上是关于技术分享-swift防御编程的主要内容,如果未能解决你的问题,请参考以下文章

如何将防御性编程技术结合在一起?

技术分享Cookie-Form型CSRF防御机制的不足与反思

技术分享贴浅析CSRF攻击与防御

软件构造第七章第三节 断言和防御性编程

技术分享 | 常见的DDoS攻击类型及防御措施

防御式编程的主要思想