Swift代码规范

801 阅读14分钟

这是一个Swift开发代码规范:原文

1、正确性

尽可能地让你的代码在没有警告的情况下编译。这个规则适用很多的情况,比如#selector类型去替代适用字符串字面值。

.

2、命名

命名遵循以下关键点:

  • 尽可能的明确使用场景
  • 清晰比简短更重要
  • 使用驼峰命名法
  • 类型和协议使用大写字母开头,其他的小写字母开头
  • 包含所有你需要的关键字,省略不必要的关键字
  • 用使用角色来命名,而不是类型
  • 弱引用类型要做注释说明
  • 要善用控制流
  • 工厂方法用make开头
  • 方法命名遵循以下规则
    • 动作化的方法遵循-ed,-ing这样的规则
    • 名词化的方法遵循类似formX这样的规则就可以了
    • boolean类型的方法,命名出来是要给人看到是用来做判断用的
    • protocols的描述应该名词化
    • protocols如果是描述一个“能否......”的时候,应该以-able或者ible结束
  • 使用专业的术语
  • 避免缩写
  • 尽可能通过方法 和属性来实现功能。
  • 命名要具有约束力
  • 上下文的首字母缩写要保持一致性
  • 避免函数返回类型重载
  • 选择好的参数命名当作文档使用
  • 闭包和元组的参数要写标识
  • 要善用默认参数

.

3、Xcode的方法属性简介列表

(img) 当你在这个列表里参阅的时候,清晰的含义就显得格外重要了。你需要让别人尽可能简单的去参阅一个方法名。

  1. 编写没有参数的方法。比如:addTarget
  2. 编写方法的时候要写参数的标签。

.

4、Class的前缀

Swift里面的类型已经自动地增加了它们所属模块的命名空间,你不需要为它们编写前缀。如果来自不同模块的两个名称发生冲突,您可以通过使用模块名称预先确定类型名称来消除歧义。但是,只有在可能出现混淆的情况下才指定模块名称。(但是在tdw项目里面,还是建议使用前缀,这个前缀是用来作为tdw出品的标识)

import SomeModule                               
let myClass = MyModule.UsefulClass()

.

5、代理

当你自定义一个代理方法等时候,第一个参数应该是一个匿名参数,这个参数是代理源。(UIkit包含了很多这样的例子) 正确的:

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

错误的:

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

.

6、使用上下文类型推断

利用编译器上下文类型推断编写更短更简洁的代码。

正确的:

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)

.

7、模版

范型类型应该被有意义的描述,用驼峰命名首字母大写的方式编写。如果你找不到词语用来表达这个类型的意思,可以使用传统的26个字母中的某一个,大写,来表示,比如:T,U,V等。 正确的:

struct Stack<Element> { ... }
func write<Target: OutputStream>(to target: inout Target)
func swap<T>(_ 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)

语言

使用美式英语来描述API 正确的:

let color = "red"

错误的:

let colour = "red"

.

8、代码组织

使用extensions来组织你的代码逻辑块。每个extension都应该使用// MARK: -来说明该extension的功能。

.

9、协议的遵循

特别的,当你增加一个继承协议的时候,最好用extension分割协议方法。

正确的:

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
}

.

10、无用的代码

没有用的代码,该删掉的还是要删掉,不要只是注释掉

.

11、间隔

xcode设置:

控制流

正确的:

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

错误的:

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

关于空格的使用:

正确的:

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]
}

.

12、注释

当需要时,使用注释来解释为什么特定的代码会做一些事情。注释必须保持最新或删除。

.

13、类和结构体

a、该使用哪个?

记住,struct具有值语义。对于没有身份的事物使用结构。包含a、b、c的数组与包含a、b、c的另一个数组完全相同,它们是完全可互换的。不管你用的是第一个数组还是第二个数组,因为它们表示的是完全一样的东西。这就是为什么数组是结构的原因。

类具有引用语义。为具有标识或特定生命周期的事物使用类。你可以将一个人建模为一个类,因为两个对象是两个不同的东西。仅仅因为两个人有相同的名字和生日,并不意味着他们是同一个人。但是这个人的生日是一个结构体,因为1950年3月3日的日期和1950年3月3日的任何其他日期对象是一样的。日期本身没有标识。

b、定义的例子

下面是一个精心设计的类定义示例:

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: Int和Circle: Shape。
  • 如果共享一个共同目的/上下文,则在一行中定义多个变量和结构。
  • 缩进getter和setter定义和属性观察者。
  • 不要添加修饰符,例如internal当它们已经是默认值时。同样,在覆盖方法时,不要重复使用修饰符。
  • 在扩展中添加额外的功能 (e.g. printing) 。
  • 隐藏非共享、实现细节如 centerString 扩展内部使用 private 访问控制的代码。

c、Self的使用

为了简洁起见,当不需要访问对象的属性或调用它的方法时要避免使用‘self’。

只有当编译器要求必须使用self的时候才使用。

d、计算属性:

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

正确的:

var diameter: Double {
    return radius * 2
}

错误的:

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

e、Final

不允许对其修饰的内容进行继承或者重新操作。
final标记class或members可能会分散主题,这不必需。然而,使用 final 有时可以阐明你的意图,是值得的。在以下示例中, Box 有一个特殊用途并且并不打算允许在派生类定制。标记 final 会让这很清楚。

// Turn any generic type into a reference type using this Box class.
final class Box<T> {
    let value: T
    init(_ value: T) {
        self.value = value
    }
}

.

14、函数声明

在一行上使用简洁的功能声明(包括大括号)

func reticulateSplines(spline: [Double]) -> Bool {
   // reticulate code goes here
}
对于具有长签名的功能, 在适当的点添加换行符,并后续行上添加一个额外的缩进
func reticulateSplines(spline: [Double], adjustmentFactor: Double,
        translateConstant: Int, comment: String) -> Bool {
     // reticulate code goes here
}

.

15、闭包表达式

仅在参数列表末尾有单个闭包表达式参数时才使用尾随闭包。并给出闭包参数的描述性名称。

正确的:

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}

.

16、类型

尽可能使用Swift自带的类型。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

a、常量

定义常量使用 let 关键字, 变量使用 var 关键字。如果变量的值不会改变通常使用 let,而不是 var 。

提示:有一个好方法就是所有东西都使用 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  // pollutes global namespace
let root2 = 1.41421356237309504880168872

let hypotenuse = side * root2 // what is root2?

b、静态方法和变量类型属性

静态方法和类型属性的工作方式类似于全局函数和全局变量,应该谨慎使用。当功能局限于某一特定类型或者要求与Objective-C互操作时是很有用的。

c、可选类型

将变量和函数返回类型声明为可选类型,返回一个nil值是可接受的。

只有在实例变量使用前确定会初始化的前提下,才能使用 !隐式强解的类型声明,例如将设置的子视图viewDidLoad。

访问可选值时,如果该值仅访问一次,或者链中有多个可选项,请使用可选链接:

self.textContainer?.textLabel?.setNeedsDisplay()

使用可选绑定更方便一次打开并执行多个操作:

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

当命名可选变量和属性时,避免像optionalString或maybeView这样命名它们,因为它们的可选参数已经在类型声明中。

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

正确的:

var subview: UIView?
var volume: Double?

// later on...
if let subview = subview, let volume = volume {
  // do something with unwrapped subview and volume
}

错误的:

var optionalSubview: UIView?
var volume: Double?

if let unwrappedSubview = optionalSubview {
  if let realVolume = volume {
    // do something with unwrappedSubview and realVolume
  }
}

d、懒加载

考虑使用懒加载细粒度控制对象生命周期。对于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]这里不需要 没有创建保留循环。 位置管理器具有弹出UI的副作用,要求用户获得许可,因此细粒度控制在这里是有意义的。

e、类型推断

希望使用简洁的代码,让编译器推断单个实例的常量或变量的类型。类型推断也适用于小(非空)数组和字典。需要时,指定特定的类型如CGFloat或Int16。

正确的:

let message =  “点击按钮”
let currentBounds =  computeViewBounds()
var names = [ “ Mic ”,“ Sam ”,“ Christine ” ]
let maximumWidth : CGFloat =  106.5

错误的:

let message : String  =  “点击按钮”
let currentBounds : CGRect =  computeViewBounds()
let names = [ String ]()

空数组和字典的类型注释: 对于空数组和字典,使用类型注释。(对于分配了大量多行内容的数组或字典,使用类型标注)

正确的:

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

错误的:

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

注意:遵循此准则意味着选择描述性名称比以前更重要。

f、语法糖

与完整的泛型语法相比,更倾向于使用简洁版的类型声明。

正确的:

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

错误的:

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

.

17、功能与方法

对于不附加到类或类型的不受约束的功能,应该谨慎使用。如果可以,尽可能使用‘.’方法而不是不受约束的功能。这有助于可读性和可发现性。 正确的:

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

.

18、内存管理

代码不应该创建引用循环,即使是非生产或教程demo中。分析您的对象图,并使用weak或unowned防止强烈的循环。或者,使用值类型(struct,enum)来完全防止循环。

a、延长对象生命周期

使用【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)
}

不推荐的:

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

不推荐的:

// deallocate could happen between updating the model and updating UI
resource.request().onComplete { [weak self] response in
  let model = self?.updateModel(response)
  self?.updateUI(model)
}

.

19、访问控制

文章中的全局访问控制注释可能会分散主题,这并且不是必要的。然而,适当地使用private和fileprivate,增进了清晰度并促进了封装。条件允许的话,尽可能使用private而不是fileprivate 。使用扩展可能需要您使用fileprivate。

只有当你需要完整的访问控制规范时才明确的使用open,public及internal。

使用访问控制作为首选属性说明符。访问控制前唯一的条件是 static符,或属性例如@IBAction、@IBOutlet和@discardableResult等。

推荐的:

private let message = "Great Scott!"

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

不推荐的:

fileprivate let message = "Great Scott!"

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

.

20、控制流

尽量使用for-in风格的for循环而不是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
}

.

21、Golden Path

a、

当用条件语句编码时,左边的代码应该是"golden" 或 "happy”路径。也就是说,不要嵌套if语句。使用多个返回语句也是可以的,这个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
   }
 }

b、

无论使用 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")
 }

c、失败的Guard

Guard声明需要以某种方式退出。一般情况下,这应该是简单的一行语句,如return、throw、break、continue和fatalError()。大量代码的block应该避免使用。如果有多个退出点需要清除代码,请考虑使用defer(延迟)代码块来避免重复清除代码。

.

22、分号

Swift在您的任何一个代码语句后都不需要分号。如果你想在一行中组合多个语句,则需要‘;’。 但不建议在一行上写用‘;’分隔的多条语句。

推荐的:

let swift = "not a scripting language"

不推荐的:

let swift = "not a scripting language";

注意:Swift与JavaScript截然不同,后者省略分号通常被认为是不安全的。

.

23、括号

if 附近的括号不是必须的,应该省略。 推荐的:

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

不推荐的:

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

在有大量代码的表达式中,有括号可以使代码更清晰地读取。

推荐的:

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

.

24、机构和 Bundle Identifier

凡是xcode有关的,机构应该设置为‘Ray Wenderlich’ ,Bundle Identifier应该设置为com.razeware.TutorialName形式,其中TutorialName是项目的名字。