本文翻译于The Official raywenderlich.com Swift Style Guide。目前更新到 Swfit4.2。
前言:作为一名开发人员,须知好的代码规范,不仅能够提升代码的可读性、提升开发效率同时也会对让团队间的开发沟通效果得到加强。针对Swift语言raywenderlich.com给出来一份较为完善的开发指南。本文就是基于原文翻译而成。英文水平较好这可点击上方链接,查看原文。
开发者应该努力做到代码没有警告的通过编译,此规则会由许多的代码编写风格来决定,例如使用#selector
类型来代替字符串字母量、尽量避免使用标记了DEPRECATED
的API等。
描述性和一致性的命名会使代码更易于阅读和理解。苹果官方的API设计指南中也描述了Swift的命名约定,一些关键点包括:
make
开头formX
Swift类型由包含它们的模块自动命名,并且不应添加类前缀,例如RW。如果来自不同模块的两个名称发生冲突,则可以通过在类型名称前加上模块名称来消除歧义。但是,只有在存在混淆的时才指定模块名称,这在平时开发中应该很少出现。
import SomeModule
let myClass = MyModule.UsefulClass()
创建自定义委托方法时,未命名的第一个参数应该是代理源。
推荐:
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
}
只导入所需的模块,例如当你只需要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
等)始终保持左括号与语法在相同的行上,右括号换行。推荐:
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]
}
使用注释来说明代码块的作用,注释应当实时的更新或者删除。
避免代码内联的块注释,代码应该做到自我解释,无需多的注释说明就能看懂。
避免使用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: Int
和Circle: Shape
。var x: Int,y: Int
centerString
使用private
访问控制隐藏实现细节。为了代码简洁,应当在Swift
中避免显式使用self
来访问属性及调用方法。
仅在编译器要求使用self
的时候使用(在@escaping闭包中、初始化器中用于消除参数中相同名称的属性歧义)
为简明起见,如果计算属性是只读的,则省略get子句。只有在提供set子句时才需要get子句。
推荐:
var diameter : Double {
return radius * 2
}
不推荐:
var diameter : Double {
get {
return radius * 2
}
}
特定的情况下使用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
}
在命名可选变量和属性时,需避免类似optionalString
或 maybeView
这样的命名,因为他们的可选性已经体现在类型声明中了。
对于可选绑定,适当时使用原始名称,而不是使用像 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
}
注意:
优先选择简洁紧凑的代码,让编译器为单个实例的常量或变量推断类型。类型推断也适合于小(非空)的数组和字典。如需使用特定类型,需提前声明。如 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?
不在类或类型的中声明的自由函数应该被谨慎使用。可能的话,首选方法而不是自由函数。这有助于可读性和易领悟性。
自由函数最适用于它们与任何特定类或实例无关的情况。
推荐:
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
在你的代码中绝对不能出现循环引用的情况,使用内存分析工具并用 weak
和 unowned
来防止强循环引用。或者,使用值类型( 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)
}
一般情况下private
和fileprivate
优先使用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会增加开发人员对代码的理解难度。
Xcode中组织名称应该是你单位或者你个人的名称,如Ray Wenderlich
,Bundle Identifier
一般采用com/cn +公司名+项目名
形式。如当你项目名称为TutorialName
时,Bundle Identifier
则应该命名为 ·com.raywenderlich.TutorialName`