raywenderlich.com官方Swift代码规范指南

本文翻译于The Official raywenderlich.com Swift Style Guide。目前更新到 Swfit4.2。

前言:作为一名开发人员,须知好的代码规范,不仅能够提升代码的可读性、提升开发效率同时也会对让团队间的开发沟通效果得到加强。针对Swift语言raywenderlich.com给出来一份较为完善的开发指南。本文就是基于原文翻译而成。英文水平较好这可点击上方链接,查看原文。

[TOC]

开发警告⚠️

开发者应该努力做到代码没有警告的通过编译,此规则会由许多的代码编写风格来决定,例如使用#selector类型来代替字符串字母量、尽量避免使用标记了DEPRECATED的API等。

命名规范

描述性和一致性的命名会使代码更易于阅读和理解。苹果官方的API设计指南中也描述了Swift的命名约定,一些关键点包括:

  • 命名应当清晰。
  • 命名清晰度的重要性大于简洁性,尽可能的表达清晰而不用害怕命名过长。
  • 使用驼峰法命名。
  • 命名针对类型和协议使用大写字母开头,其它都已小写字母开头。
  • 尽量包含所需要的单词,同时省略不必要的单词。
  • 命名的名称应当基于当前语义,而不是类型。
  • 对信息缺失的命名可以适当补充。
  • 命名尽量使用流畅的语法
  • 工厂方法以make开头
  • 命名方法考虑以下因素
    • 不可变(non-mutating)的动词方法后接 -ed、-ing。
    • 不可变(non-mutating)的名词方法遵守formX
    • boolean 类型的方法阅读应当类似断言。只有是、否两种含义。
    • 描述某些东西的协议应该被理解为名词(例如Collection)。
    • 描述一个协议能力应该使用后缀命名able,ible或ing (例如Equatable,ProgressReporting)。
  • 使用通俗易懂的术语,无论是新手还是专家都能轻松接受。
  • 一般来说避免使用缩写。
  • 使用既定成俗的命名。
  • 首选方法和属性来代替函数。
  • 通常缩略语和首字母缩略词应根据案例惯例统一大写或小写。
  • 为具有相同含义的方法指定相同的基名。
  • 避免方法的返回值过多。
  • 将方法名称命名部分放在第一个参数名中。
  • 选择好的方法参数名称,见文知意,起到好的说明效果。
  • 对闭包和元组类型参数说明
  • 利用好默认参数。

类前缀

Swift类型由包含它们的模块自动命名,并且不应添加类前缀,例如RW。如果来自不同模块的两个名称发生冲突,则可以通过在类型名称前加上模块名称来消除歧义。但是,只有在存在混淆的时才指定模块名称,这在平时开发中应该很少出现。

import SomeModule

let myClass = MyModule.UsefulClass()

Delegates

创建自定义委托方法时,未命名的第一个参数应该是代理源。

推荐:

func  namePickerView(_  namePickerView:NamePickerView,didSelectName  name:String)
func  namePickerViewShouldReload(_  namePickerView:NamePickerView)- >  Bool

不推荐:

func didSelectName(namePicker: NamePickerViewController, name: String)
func namePickerShouldReload() -> Bool

类型推导

使用Swift的类型推导功能,减少代码量,提升代码简洁性
推荐:

let selector = #selector(viewDidLoad)
view.backgroundColor = .red
let toView = context.view(forKey: .to)
let view = UIView(frame: .zero)

不推荐:

let selector = #selector(ViewController.viewDidLoad)
view.backgroundColor = UIColor.red
let toView = context.view(forKey: UITransitionContextViewKey.to)
let view = UIView(frame: CGRect.zero)

泛型

泛型类型的参数应该具有描述性的,按驼峰命名。当一个泛型类名没有一个有意义的名称,可以使用传统的单大写字母代替,例如T,U或V。
推荐:

struct Stack { ... }
func write(to target: inout Target)
func swap(_ a: inout T, _ b: inout T)

不推荐:

struct Stack { ... }
func write(to target: inout Target)
func swap(_ a: inout T, _ b: inout T)

语法

使用美式的语法,美式语法会更加匹配苹果的API。

推荐:

let color = "red"

不推荐:

let colour = "red"

代码结构

使用extensions来组织你的代码结构,每个extension中应该是一整块的相关代码。每个extensions之前都应添加// MARK: -注释。

协议一致性

遵守协议时,针对每个协议下的方法添加单独的扩展,使得协议方法组合在一起。

推荐:

class MyViewController: UIViewController {
  // class stuff here
}

// MARK: - UITableViewDataSource
extension MyViewController: UITableViewDataSource {
  // table view data source methods
}

// MARK: - UIScrollViewDelegate
extension MyViewController: UIScrollViewDelegate {
  // scroll view delegate methods
}

不推荐:

class MyViewController: UIViewController, UITableViewDataSource, UIScrollViewDelegate {
  // all methods
}

在UIKit中的Viewcontroller中可以使用Extension来分离自定义访问器和IBAction。

未使用的代码

应当删除未使用的代码包括Xcode的模板代码和默认的注释、去除没有意义的代码。
推荐:

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

不推荐:

override func didReceiveMemoryWarning() {
  super.didReceiveMemoryWarning()
  // Dispose of any resources that can be recreated.
}

override func numberOfSections(in tableView: UITableView) -> Int {
  // #warning Incomplete implementation, return the number of sections
  return 1
}

override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
  // #warning Incomplete implementation, return the number of rows
  return Database.contacts.count
}

最小import原则

只导入所需的模块,例如当你只需要Foundation时,无需导入UIKit,当你需要UIKit时,无需再重复导入Foundation

推荐:

import UIKit
var view: UIView
var deviceModels: [String]

推荐:

import Foundation
var deviceModels: [String]

不推荐:

import UIKit
import Foundation
var view: UIView
var deviceModels: [String]

不推荐:

import UIKit
var deviceModels: [String]

代码间隔

使用2个空格而不是制表符缩进以节省空间,Xcode中的设置可以按如下配置:

  • 方法括号和其他大括号(if/ else/ switch/while等)始终保持左括号与语法在相同的行上,右括号换行。
  • 您可以通过选择一些代码(或Command-A以选择所有代码)然后再按Control-I来重新缩进。

推荐:

if user.isHappy {
  // Do something
} else {
  // Do something else
}

不推荐:

if user.isHappy
{
  // Do something
}
else {
  // Do something else
}
  • 方法之间应该只有一个空行,以帮助视觉清晰度和组织。
  • 开闭的大括号和代码之间不应该有空行
  • :一般左边没有空格,右边一个空格。除了三目运算符? :、空字典[:]、和#selector的语法如addTarget(_:action:)

推荐:

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]
}
  • 每行代码应该限定在70个字符以内
  • 避免在行尾随意的添加空格
  • 每个文件的末尾添加一个换行符

注释

使用注释来说明代码块的作用,注释应当实时的更新或者删除。

避免代码内联的块注释,代码应该做到自我解释,无需多的注释说明就能看懂。

避免使用C语言的注释风格(/* ... */)。使用 // 或者 ///来添加注释。

类和结构体

类和结构体的选择?

结构体是值语义,当我们定义一个没有特性的事物的时候可以采用Struct类型,一个包含[a,b,c]的数组和另一个包含[a,b,c]的数组是可以互相替换的。无论你使用第一个数组还是第二个数组都表达的是同一个东西,所以Array就是一个Struct类型。

类是引用语义,当定义一个具有特性或者有特殊声明周期的事物时采用Class类型。当你定义人的时候应该定义为类,两个同姓名且同生日的人对象在含义上却是两个不同的东西。

Example 展示

规范的类定义

class Circle: Shape {
  var x: Int, y: Int
  var radius: Double
  var diameter: Double {
    get {
      return radius * 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)
  }

  override func area() -> Double {
    return Double.pi * radius * radius
  }
}

extension Circle: CustomStringConvertible {
  var description: String {
    return "center = \(centerString) area = \(area())"
  }
  private var centerString: String {
    return "(\(x),\(y))"
  }
}

上面的例子展示了一下的代码规范:

  • 指定属性,变量,常量,参数声明和其他语句的类型,在冒号前没有空格而后面有空格。例如x: IntCircle: Shape
  • 在同一行代码中定义相同类型的变量。如:var x: Int,y: Int
  • 缩进getter和setter定义以及属性观察器。
  • 不要添加修饰符,例如internal它们已经是默认值。同样,重写方法时也不要重复访问修饰符。
  • 在Extension中添加额外的功能(例如打印)。
  • centerString使用private访问控制隐藏实现细节。

使用self

为了代码简洁,应当在Swift中避免显式使用self来访问属性及调用方法。

仅在编译器要求使用self的时候使用(在@escaping闭包中、初始化器中用于消除参数中相同名称的属性歧义)

计算属性

为简明起见,如果计算属性是只读的,则省略get子句。只有在提供set子句时才需要get子句。

推荐:

var diameter : Double {
   return radius *  2 
}

不推荐:

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

final标记

特定的情况下使用final标记,当你明确知道你的类或者属性成员是终极的时候可以使用final标记。在下面的示例中,Box具有特定目的并且不希望在派生类中进行自定义。标记它就final清楚了。

// 将泛型类型转换成引用类型
final class Box {
  let value: T
  init(_ value: T) {
    self.value = value
  }
}

函数声明

在左括号下面添加一行简短函数声明。

func reticulateSplines(spline: [Double]) -> Bool {
  // reticulate code goes here
}

对于名称和参数都较多的函数,将每个参数放在一个新行上,并在后续行中添加一个额外的缩进:

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

使用()不要使用(Void)来表示参数为空,在闭包和函数返回时使用Void而不是()

推荐:

func updateConstraints() -> Void {
  // magic happens here
}

typealias CompletionHandler = (result) -> Void

不推荐:

func updateConstraints() -> () {
  // magic happens here
}

typealias CompletionHandler = (result) -> ()

函数调用

和函数声明一样,在函数调用时候如果单行就能表示就是用单行:

let success = reticulateSplines(splines)

如果调用函数参数过多时,每个参数换行并在后续行中添加一个额外的缩进:

let success = reticulateSplines(
  spline: splines,
  adjustmentFactor: 1.3,
  translateConstant: 2,
  comment: "normalize the display")

闭包表达式

仅当参数中只有一个闭包表达式参数且是最后一个参数的时候才使用尾随闭包。

推荐:

UIView.animate(withDuration: 1.0) {
  self.myView.alpha = 0
}

UIView.animate(withDuration: 1.0, animations: {
  self.myView.alpha = 0
}, completion: { finished in
  self.myView.removeFromSuperview()
})

不推荐:

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

UIView.animate(withDuration: 1.0, animations: {
  self.myView.alpha = 0
}) { f 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的原生类型和表达式,Swift提供了Swift提供了与Objective-C的桥接,因此您仍然可以根据需要调用所有OC方法。

推荐:

let width = 120.0                                    // Double
let widthString = "\(width)"                         // String

一般:

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

不推荐:

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

在绘图的代码中使用CGFloat类型,避免过多转换使代码更简洁。

常量

如果变量的值将不会改变时始终使用let而不是var

Tip:你可以一开始就是用 let来定义所有的内容,当编译器编译错误的时候在改为var

你可以在一个类型里面去定义常量而不是在类型的实例变量中去使用类型属性。使用static let声明类型常量比直接声明全局变量更推荐。这样可以更好区分全局变量和实例属性。

推荐:

enum Math {
  static let e = 2.718281828459045235360287
  static let root2 = 1.41421356237309504880168872
}

let hypotenuse = side * Math.root2

不推荐:

let e = 2.718281828459045235360287  // 污染全局命名空间
let root2 = 1.41421356237309504880168872

let hypotenuse = side * root2 // 什么 root2?

静态方法和可变类型属性

静态方法和类型属性跟全局函数和全局变量的工作原理类似,应当谨慎使用。当功能的作用域是一个特定类型或需要与 Objective-C 交互时,它们才非常有用。

可选类型

在可能出现 nil 值的情况下,使用 ?声明变量和函数返回类型为可选类型。

当访问一个可选值时,如果值仅被访问一次或在链中有许多可选项时,使用可选链:

self.textContainer?.textLabel?.setNeedsDisplay()

当一次性解包和执行多个操作更方便时,使用可选绑定:

if let textContainer = self.textContainer {
// do many things with textContainer
}

在命名可选变量和属性时,需避免类似optionalStringmaybeView 这样的命名,因为他们的可选性已经体现在类型声明中了。

对于可选绑定,适当时使用原始名称,而不是使用像 unwrappedView 或 actualLabel 这样的名称。

不推荐:

var subview: UIView?
var volume: Double?

// later on...
if let subview = subview, let volume = volume {
  // 使用展开的 subview 和 volume 做某件事
}

推荐:

var optionalSubview: UIView?
var volume: Double?

if let unwrappedSubview = optionalSubview {
  if let realVolume = volume {
    // 使用 unwrappedSubview 和 volume 做某件事
  }
}

懒加载(延迟初始化)

在更细粒度地控制对象声明周期时考虑使用延迟初始化。 对于 UIViewController ,延迟初始化视图是非常正确的。你也可以直接调用 { }() 的闭包或调用私有工厂方法。例如:

lazy var locationManager: CLLocationManager = self.makeLocationManager()

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

注意:

  • 因为没有发生循环引用,所以这里不需要 [unowned self]。
  • 位置管理器向用户弹出申请权限是影响用户体验的,所以细颗粒地控制在这里是有意义的。

类型推断

优先选择简洁紧凑的代码,让编译器为单个实例的常量或变量推断类型。类型推断也适合于小(非空)的数组和字典。如需使用特定类型,需提前声明。如 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: [String]
var employees: [Int: String]
var faxNumber: Int?

函数 vs 方法

不在类或类型的中声明的自由函数应该被谨慎使用。可能的话,首选方法而不是自由函数。这有助于可读性和易领悟性。

自由函数最适用于它们与任何特定类或实例无关的情况。
推荐:

let sorted = items.mergeSorted()  // easily discoverable
rocket.launch()  // acts on the model

不推荐:

let sorted = mergeSort(items)  // hard to discover
launch(&rocket)

使用自由函数的特列:

let tuples = zip(a, b)  // feels natural as a free function (symmetry)
let value = max(x, y, z)  // another free function that feels natural

内存管理

在你的代码中绝对不能出现循环引用的情况,使用内存分析工具并用 weakunowned 来防止强循环引用。或者,使用值类型( struct、enum )来彻底防止循环引用。

延长对象的生命周期

使用惯用语法 [weak self]guard let strongSelf = self else { return } 来延长对象的生命周期。 在 self超出闭包生命周期不明显的地方,[weak self] 更优于[unowned self]。显式的延长生命周期会优于使用可选解包。

推荐:

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

不推荐:

// might crash if self is released before response returns
resource.request().onComplete { [unowned self] response in
  let model = self.updateModel(response)
  self.updateUI(model)
}

访问控制

一般情况下privatefileprivate优先使用private,只有编译器提示使用fileprivate才做修改。
将访问控制用作前置属性说明符。仅有 static 说明符或诸如 @IBAction 、 @IBOutlet 和 @discardableResult 的属性可以放在访问控制说明符前面。

推荐:

private let message = "Great Scott!"

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

不推荐:

fileprivate let message = "Great Scott!"

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

控制流

优先选择 for 循环的 for-in 格式而不是 while-condition-increment 格式。

推荐:

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

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

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

for index in (0...3).reversed() {
  print(index)
}
var i = 0
while i < 3 {
  print("Hello three times")
  i += 1
}

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

黄金路径

不要嵌套多个条件判断,可以提前将结果返回。guard语句就是因为这个存在的。

推荐:

func computeFFT(context: Context?, inputData: InputData?) throws -> Frequencies {

  guard let context = context else {
    throw FFTError.noContext
  }
  guard let inputData = inputData else {
    throw FFTError.noInputData
  }

  // use context and input to compute the frequencies
  return frequencies
}

不推荐:

func computeFFT(context: Context?, inputData: InputData?) throws -> Frequencies {

  if let context = context {
    if let inputData = inputData {
      // use context and input to compute the frequencies

      return frequencies
    } else {
      throw FFTError.noInputData
    }
  } else {
    throw FFTError.noContext
  }
}

当用 guard 或 if let 解包多个可选值时,在可能的情况下使用最下化复合版本嵌套。举例:

推荐:

guard 
  let number1 = number1,
  let number2 = number2,
  let number3 = number3 
  else {
    fatalError("impossible")
}
// do something with numbers

不推荐:

if let number1 = number1 {
  if let number2 = number2 {
    if let number3 = number3 {
      // do something with numbers
    } else {
      fatalError("impossible")
    }
  } else {
    fatalError("impossible")
  }
} else {
  fatalError("impossible")
}

失败防护

对于用某些方法退出,防护语句是必要的。一般地,它应该是一行简洁的语句,比如: return 、 throw 、 break 、 continue 和 fatalError()。应该避免大的代码块。如果清理代码被用在多个退出点,则可以考虑用 defer 块来避免清理代码的重复。

分号

在 Swift 中,每条代码语句后面都不需要加分号。只有在你希望在一行中结合多条语句,才需要加分号。

不要在用分号分隔的单行中写多条语句。

推荐:

let swift = "not a scripting language"

不推荐:

let swift = "not a scripting language";

括号

条件判断周围的括号是不必要的,应该省略。

推荐:

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

不推荐:

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

在复杂的表达式中,添加括号可以让代码读起来更清晰。

推荐:

let playerMark = (player == current ? "X" : "O")

多行字符串字面量

当你创建长字符串文字时,推荐使用多行字符串字面量,左右"""和字符串间隔开。

推荐:

let message = """
  You cannot charge the flux \
  capacitor with a 9V battery.
  You must use a super-charger \
  which costs 10 credits. You currently \
  have \(credits) credits available.
  """

不推荐:

let message = """You cannot charge the flux \
  capacitor with a 9V battery.
  You must use a super-charger \
  which costs 10 credits. You currently \
  have \(credits) credits available.
  """

不推荐:

let message = "You cannot charge the flux " +
  "capacitor with a 9V battery.\n" +
  "You must use a super-charger " +
  "which costs 10 credits. You currently " +
  "have \(credits) credits available."

不要使用Emoji

在你定义变量或者方法中不要使用Emoji。Emoji会增加开发人员对代码的理解难度。

Xcode中的Organization和Bundle Identifier设置

Xcode中组织名称应该是你单位或者你个人的名称,如Ray Wenderlich,Bundle Identifier一般采用com/cn +公司名+项目名形式。如当你项目名称为TutorialName时,Bundle Identifier则应该命名为 ·com.raywenderlich.TutorialName`

你可能感兴趣的:(raywenderlich.com官方Swift代码规范指南)