Swift 3.0 编码规范

花了周末的时间翻译raywenderlich.com的Swift编码规范(传送门), 原文是针对写作时Swift代码的规范,在整理过程中删除了部分和实际项目无关的条目。

编码规范的目标是保证代码的简洁性,可读性和可维护性。

正确性

把警告当做错误处理。这条规则从根本禁止了一些文法使用,如推荐使用#selector文而不是用字符串(更多请阅读Swift 3为什么推荐使用#selector)。 


命名

使用具有描述性的名称,并结合驼峰式命名规则给类方法和变量等命名。类别名称(类,结构体,枚举和协议)首字母大写,而方法或者变量的首字母小写。更多命名规范请查阅Apple Design Guidelines

正例

private let maximumWidgetCount = 100

class WidgetContainer {
    var widgetButton: UIButton  
    let  widgetHeightPercentage = 0.85
}

反例

let MAX_WIDGET_COUNT = 100
class app_widgetContainer {
   var wBut: UIButton
   let  wHeightPct = 0.85
}

缩写和简写应该要尽量避免,遵守苹果命名规范,缩写和简写中的所有字符大小写要一致。

正例

let urlString:  URLString
let userID:  UserID

反例

let uRLString:  UrlString
let userId:  UserId

对于函数和初始化方法,建议给所有参数命名,除非上下文非常清晰,如果结合外部参数命名可以让函数调用更易读,要结合起来。

func dateFromString(dateString: String) -> NSDate
func convertPointAt(column column:Int, row:Int) > CGPoint
func timedAction(afterDelay delay: NSTime Interval,performaction: SKAction) ->SKAction!

//调用上述函数时如下:
dateFromString("2014-03-14")
convertPointAt(column: 42, row: 13)
timedAction(afterDelay: 1.0, perform: someOtherAction)

方法命名的时候要考虑方法体中的第一个参数的名称。

class Counter {
  //combineWith + otherCounter
  func combineWith(otherCounter:  Counter, options: Dictionary?) { ... }
 //incrementBy + amount
  func incrementBy(amount: Int) { ... }
}

协议

根据苹果接口设计指导准则,协议名称用来描述一些东西是什么的时候是名词,例如:Collection,WidgetFactory。若协议名称用来描述能力应该以-ing, -able, �或 -ible结尾,例如:Equatable,Resizing.

枚举

根据Swift 3苹果接口设计指导文档,枚举中的值使用“小骆驼拼写法”命名规则

enum Shape {
    case rectangle
    case square
    case  triangle
    case  circle
}

类前缀 

Swift中类别(类,结构体)在编译时会把模块设置为默认的命名空间,所以不用为了区分类别而添加前缀,比如RW。如果担心来自不同模块的两个名称发生冲突,可以在使用时添加模块名称来区分,注意不要滥用模块名称,仅在有可能发生冲突或疑惑的场景下使用。

import SomeModule
let myClass = MyModule.UsefulClass()

委托 - Delegate

在定义委托方法时,第一个未命名参数应是委托数据源。 (为了保持参数声明的一致性在Swift3引入的)

正例

func namePickerView(_ namePickerView: NamePickerView, didSelectName name: String)

func namePickerViewShouldReload(_ namePickerView: NamePickerView) -> Bool

反例

func didSelectName(namePicker: NamePickerViewController, name: String)

func namePickerShouldReload() -> Bool

泛型

泛型类参数应具有描述性,遵守“大骆驼命名法”。如果一个参数名没有具体的含义,可以使用传统单大写字符,如T,  U, 或V等。

正例

struct Stack<Element>{ ... }
func writeTo <Target: OutputStream>(to Target: inout Target)
func swap(_ a: inout T, _ b: inout T) 

反例

struct Stack<T>{ ... }
func write<target: OutputStream> (to target: inout target)
func swap<Thing>(_ a: inout Thing, _ b: inout Thing)

语言

使用美式英语来定义接口。

正例

let color = "red"

反例

let colour = "red"


代码组织结构

协议一致性

当一个对象要实现协议一致性时,推荐使用 extension 隔离协议中的方法集,这样让相关方法和协议集中显示在一起,�也简化了类支持一个协议和实现相关方法的流程。

正例

class MyViewcontroller: UIViewController {
      // �方法
}

extension MyViewcontroller: UITableViewDataSource {
      // UITableViewDataSource 方法
}

extension MyViewcontroller: UIScrollViewDelegate {
      // UIScrollViewDelegate 方法

反例

class MyViewcontroller: UIViewController, UITableViewDataSource, UIScrollViewDelegate {
       // 所有的方法
}

对于UIKit view controllers, 建议用extensions定义不同的类,按照生命周期,自定义方法,IBAction分组。

无用代码

无用的代码,包括Xcode生成的模板代码和占位符注释应该删除,除非是有目的性的保留这些代码。

一些方法内只是简单地调用了父类里面的方法也需要删除,包括UIApplicationDelegate内的空方法和无用方法。

正例

override func tableView(tableView: UITableView,numberOfRowsInSectionsection: Int) -> Int{
  return Database.contacts.count
}

反例

override func didReceiveMemoryWarning() {
  super.didReceiveMemoryWarning()
}
override func numberOfSectionsInTableView(tableView: UITableView) -> Int {
  return 1
}

override func tableView(tableView: UITableView,numberOfRowsInSectionsection: Int) -> Int {
  return Database.contacts.count
}

最少引入

减少不必要的引入,例如引入Foundation能满足的情况下不用引入UIKit。

间隔

缩进使用2个空格. 在Xcode的偏好设置和项目设置都可以配置。

Xcode偏好设置
项目设置

方法体的大括号和其他大括号 (if/else/switch/while等) 首括号和首行语句在同一行,尾括号新起一行。

正例

if user.isHappy {
  // xxoo
} else {
  // ooxx
}

反例

if user.isHappy
{
  // xxoo
}
else{
  // ooxx
}

方法体内最多只有一个空行,这样可以保持整齐和简洁,如果需要多个空行,说明代码需要重构。

: 左面不要空格,而右面需要保留空格,例外的是三元操作符 ? : 和空数组 [:]

正例

class TestDatabase: Database {
   var data: [String: CGFloat] = ["A": 1.2,"B": 3.2]
}

反例

class TestDatabase: Database {
   var data :[String:CGFloat] = ["A" : 1.2,"B" : 3.2]
}


注释

当需要的时候,使用注释来解释阐明特定一块代码的用途,注释必须保持最新,过期的注释要及时删除。

避免代码中出现注释块,代码本身应尽量起到注释的作用,如果注释是为了生成文档可以例外。


类和结构体

应该使用哪一个

结构体是值类型。结构体在使用中没有标识。一个数组包含[a, b, c]和另外一个数组包含[a, b, c]是完全一样的,它们完全可以�互相替换,使用第一个还是使用第二个都一样,因为它们代表的是同一个东西。这就是为什么数组是结构体。

类是引用类型。类使用的场景是需要一个标识或者需要一个特定的生命周期。假设你需要对人�抽象为一个类,因为两个人,是两个不同的东西。即使两个人有同样的名字和生日,也不能确定这两个人是一样的。但是人的生日是一个结构体,因为日期1950/03/03和另外一个日期1950/03/03是相同的,日期是结构体没有唯一标示。

有时,一些事物应该定义为结构体,但是还需要兼容AnyObject或者已经在以前的历史版本中定义为类(NSDate,NSSet),所以尽可能的注意�类和结构体之间的区别。

类定义

下面是一个设计良好的类定义:

class Circle: Shape {
  var x: Int, y: Int
  var radius: Double
  var diameter: Double{
      get {
        returnradius * 2
     }
     set {     
        radius=newValue/2
    } 
}
   init(x: Int, y: Int, radius: Double) {
      self.x = x
      self.y = y
      self.radius = radius 
  }

 convenience init(x: Int, y: Int, diameter: Double) {
     self.init(x: x, y: y, radius: diameter/2) 
}

func describe() -> String{
    return"I am a circle at\(centerString())with an area of\(computeArea())"
}
override func computeArea() -> Double{
   return M_PI * radius * radius 
}

private func centerString()->String{
    return "(\(x),\(y))"
  }
}

上面的例子展示了下面的设计准则:

属性,变量,常量和参数等在声明定义时,其中: 符号后有空格,而: 符号前没有空格,比如x: Int, 和Circle: Shape

如果定义多个变量/数据结构时出于相同的目的和上下文,可以定义在同一行。

缩进getter,setter的定义和属性观察器的定义。

不需要添加internal这样的默认的修饰符。也不需要在重写一个方法时添加访问修饰符。

self的使用

为了保持简洁,可以避免使用 self 关键词,因为Swift 在访问对象属性和调用对象方法不需要使用 self。

不过当在构造器中需要区分属性名和参数名时需要使用 self,还有当在在闭包表达式中引用属性值。

class BoardLocation  {
    let row: Int, column: Int
    init(row: Int, column: Int)  {
        self.row = row
        self.column = column
        let closure = {
            print(self.row)   
         } 
    }
}

计算属性

为了保持简洁,如果一个属性是只读的,请忽略掉get语句。因为只有在需要定义set语句的时候,才需要get语句。

正例

var diameter:  Double{
    return radius  *  2
}

反例

var diameter:  Double{
     get {
        return radius * 2
     }
}

�Final

给那些不打算被继承的类使用final 修饰符, �例如:

final class Box<T> {
     let value: T
     init(_ value: T) {
         self.value = value 
     }
}


�函数声明

在定义短函数声明时保持在一行,一行内包括头括号:

func reticulateSplines(spline: [Double]) -> Bool {
     // xxoo
}

对于声明较长的函数时,在适当的位置换行并在第二行多添加一个缩进:

func reticulateSplines(spline: [Double], adjustmentFactor: Double,    translateConstant:Int,          comment:String) -> Bool {
      // ooxx
}


闭包表达式

仅在闭包表达式参数在参数列表中最后一个时,使用尾随闭包表达式。闭包参数命名要有描述性。

正例

UIView.animateWithDuration(1.0) {
     self.myView.alpha=0
}

UIView.animateWithDuration(1.0, 
      animations: {
            self.myView.alpha = 0
       }, 
        completion: { finish in
             self.myView.removeFromSuperview() 
   }
)

反例

UIView.animateWithDuration(1.0, animations: {
      self.myView.alpha=0
})

UIView.animateWithDuration(1.0, 
        animations: {
              self.myView.alpha = 0
           }) { finish in
          self.myView.removeFromSuperview()
}

当单个闭包表达式上下文清晰时,使用隐式的返回值:

attendeeList.sort { a, b in
     a > b
}

链式方法使用尾随闭包会更清晰易读,�至于如何使用空格,换行,还是使用命名和匿名参数不做具体要求。

let value = numbers.map{ $0 * 2  }.filter{  $0 % 3 == 0 }.index(of: 90)

let value=numbers
          .map{ $0 * 2 }
          .filter{ $0 > 50 }
          .map{$0 + 10}


类型

优先使用Swift原生类型,可以根据需要使用Objective-C提供的方法,因为Swift提供了到Objective-C的桥接 。

正例

let width = 120.0  // Double
let widthString = (width as NSNumber).stringValue // String

反例

let width: NSNumber = 120.0 // NSNumber
let widthString: NSString=width.stringValue  // NSString

�在 Sprite Kit �代码中, 使用CGFloat可以避免数据多次转换,让代码更简洁。

常量

定义常量使用 let 关键字,定义变量使用 var 关键字, 如果变量的值未来不会发生变化要使用常量。

�建议: 一个好的技巧是,使用 let 定义任何东西,只有在编译器提出警告需要改变的时候才修改为 var 定义。

你可以使用类型属性来定义类型常量而不是实例常量,使用static let 可以定义类型属性常量。 这样方式定义类别属性整体上优于全局常量,因为更容易区分于实例属性. �比如:

正例

enum Math {
   static let e = 2.718281828459045235360287  
   static let pi = 3.141592653589793238462643
}
radius * Math.pi * 2// �周长

Note: 使用枚举的好处是变量不会被无意初始化,且全局有效。

反例

let e=2.718281828459045235360287// �污染全局命名空间
let pi=3.141592653589793238462643

radius * pi * 2// pi是实例数据还是全局常量?

静态方法和变量类型属性

静态方法和类别属性的工作原理类似于全局方法和全局属性,应该节制使用。它们的使用场景在于如果某些功能局限于特别的类型或和Objective-C 互相调用。

可选类型

可以变量和函数返回值声明为可选类型(?),如果nil值可以接受。

当你确认实例变量会稍晚在使用前初始化,可以在声明时使用!来隐式的拆包类型,比如在viewDidLoad中会初始化的子视图。

当你访问一个可选类型值时,如果只需要访问一次或者在可选值链中有多个可选类型值时,请使用可选类型值链:

self.textContainer?.textLabel?.setNeedsDisplay()

如果可以方便的一次性拆包或者�执行多次性操作时,使用可选类型绑定:

if let textContainer =self.textContainer {
      // 对textContainer多次操作
}

当我们命名一个可选值变量和属性时,避免使用如optionalString 或 maybeView名称,因为可选值的已经体现在类型定义中。

在可选值绑定时,直接映射原始的命名比使用诸如unwrappedView 或 actualLabel好。

正例

var subview: UIView?
var volume: Double?

if let subview=subview, volume=volume {
      // xxoo
}

反例

var optionalSubview: UIView?
var volume: Double?
if let unwrappedSubview = optionalSubview {
    if let realVolume = volume {
          // ooxx
    }
}

结构体构造器

使用Swift原生的结构体构造器而不是传统的的CGGeometry构造器。

正例

let bounds = CGRect(x: 40, y: 20, width: 120, height: 80)
let centerPoint = CGPoint(x: 96, y: 42)

反例

let bounds = CGRectMake(40,20,120,80)
let centerPoint = CGPointMake(96,42)

推荐使用结构体内部的常量CGRect.infiniteRect,CGRect.nullRect等,来替代全局常量CGRectInfinite,CGRectNull等。对于已经存在的变量值,可以简写成 .zero。

延迟初始化

延迟初始化用来细致地控制对象的生命周期,这对于想实现延迟加载视图的UIViewController特别有用,你可以使用即时被调用闭包或私有构造方法:

lazy var locationManager: CLLocationManager=self.makeLocationManager()

private func makeLocationManager() -> CLLocationManager {
    let manager=CLLocationManager()                                manager.desiredAccuracy=kCLLocationAccuracyBest 
    manager.delegate=self
    manager.requestAlwaysAuthorization()
    return manager
}

注意:Location manager 的负面效果会弹出对话框要求用户提供权限,这是做延时加载的原因。

类型推断

推荐使用更加紧凑的代码,让编译器能够推断出每个常量和变量的类型。类型推断也适用于小的数组和字典,如果需要可以指定特定的类型,如 CGFloat 或 Int16 。

正例

let message = "Click the button"
let currentBounds = computeViewBounds()
var names = ["Mic","Sam","Christine"]
let maximumWidth: CGFloat =106.5

反例

let message:String="Click the button"
let currentBounds: CGRect=computeViewBounds()
let names=[String]()

类型注解对空的数组和字典

对空的数据和字典,使用类型注解。

正例

var names:  [String] = []
var lookup:  [String: Int] = [:]

反例

var names = [String]()
var lookup = [String: Int]()

注意:这意味着选择描述性名称更加重要。

语法糖

使用简洁的类型定义语法,而不是全称语法。

正例

var deviceModels: [String]
var employees: [Int:String]
var faxNumber: Int?

反例

var deviceModels: Array<String>
var employees: Dictionary<Int, String>
var faxNumber: Optional<Int>


函数 VS 方法

自由函数不依附于任何类或类型,应该节制地使用。如果可能优先使用方法而不是自由函数,这有助于代码的可读性和易发现性。

自由函数使用的场景是当不确定它和具体的类别或实例相关。

正例

let sorted = items.mergeSort() // �易发现性
rocket.launch()  // �可读性

反例

let sorted = mergeSort(items)// �不易被发现
launch(&rocket)

自由函数

let tuples = zip(a, b) 
let value = max(x,y,z)// �天然自由函数!


内存管理

代码应避免指针循环引用,分析对象图谱,使用弱引用�和未知引用避免强引用循环. 另外使用值类型(结构体和枚举)可以避免循环引用。

延长对象生命周期

延长对象生命周期习惯上使用[weak self] 和 guard let strongSelf = self else { return }. [weak self]� 优于 [unowned self] 因为前者更更能明显地self 生命周期长于闭包块。 显式延长�生命周期优先于�可选性拆包.

正例

resource.request().onComplete { [weak self] response in
  guard let strongSelf = self else {return}
  let model = strongSelf.updateModel(response) 
  strongSelf.updateUI(model)
}

反例

// 如果self在response之前销毁会�崩溃
resource.request().onComplete { [unowned self]  response in
    let model = self.updateModel(response)
    self.updateUI(model)
}

反例

//�self可能在updateModel和updateUI被释放
resource.request().onComplete { [weak self] response in
  let model = self?.updateModel(response)
  self?.updateUI(model)
}


访问控制

合理的使用private 和  fileprivate, 推荐使用private,在使用extension时可使用fileprivate。

访问控制符一般放在属性修饰符的最前面. 除非需要使用 static 修饰符 ,@IBAction,  @IBOutlet 或 @discardableResult 。

正例

class TimeMachine {
   private dynamic lazy var fluxCapacitor = FluxCapacitor()
}

private let message = "Great Scott!"

反例

class TimeMachine {
   lazy dynamic private var fluxCapacitor=FluxCapacitor()
}

fileprivate let message = "Great Scott!"


控制流程

循环使用for-in表达式,而不使用 while 表达式。

正例

for _ in 0..<3 {
  print("Hello three times")
}

for(index, person) in attendeeList.enumerate() {
    print("\(person)is at position #\(index)")
}

for index in 0.stride(from: 0, to: items.count, by: 2) {
  print(index)
}
for index in (0...3).reverse() {
    print(index)
}

反例

var i=0
while i<3 {
  print("Hello three times")
  i+=1
}

var i=0
while i<3 {
  let person = attendeeList[i]
  print("\(person)is at position #\(i)") 
  i+=1
}


黄金路径

当编码遇到条件判断时,左边的距离是黄金路径或幸福路径,因为路径越短,速度越快。不要嵌套if循环,多个返回语句是可以的。guard 就为此而生的。

正例

func computeFFT(context: Context?, inputData: InputData?)  throws -> Frequencies {
    guard let context = context else {throwFFTError.NoContext }

   guard let inputData = inputData else { throwFFTError.NoInputData }

  //�计算frequencies
  return frequencies
}

反例

func computeFFT(context: Context?, inputData: InputData?) throws -> Frequencies {
if let context = context {
    if let inputData = inputData {
           // 计算frequencies
          return frequencies   
    } else {
         throwFFTError.NoInputData   
     } 
   } else {
      throwFFTError.NoContext 
   }
}

当有多个条件需要用 guard 或 if let 解包,可用复合语句避免嵌套。

正例

guard let number1=number1, number2=number2, number3=number3 else{
    fatalError("impossible")
}
// 处理number

反例

if let number1=number1 {
     if let number2=number2 {
           if let number3=number3 {
               // 处理number
             } else {     
              fatalError("impossible")   
             } 
         } else {   
            fatalError("impossible") 
          }
      } else {
   fatalError("impossible")
}

�失败防护

防护语句的退出有很多方式,一般都是单行语句,如 return,throw,break,continue 和 fatalError()等。 避免出现大的代码块,�如果清理代码需要多个退出点,可以用 defer 模块避免重复清理代码。


分号

Swift不强制每条语句有分号,当一行内写多个语句的时候是必须的。

不要在一行里写多个语句。

例外的是在构造for递增循环时需要使用分号,但构造for循环时推荐使用 用 for-in �结构。

正例

let swift = "not a scripting language"

反例

let swift = "not a scripting language";


圆括号

条件判断时圆括号不是必须的,建议省略。

正例

if name=="Hello" {
    print("World")
}

反例

if(name=="Hello") {
   print("World")
}


推荐阅读

Swift入门 - 数组


更多

获取更多内容请关注微信公众号豆志昂扬:

+ 直接添加公众号豆志昂扬

+ 微信扫描下图二维码;

推荐阅读更多精彩内容