Swift 代码规范

1,243 阅读28分钟

Swift 代码规范描述了在编写 Swift 代码时常见的注意事项。对于这些规范,除了一些原则性的错误外你不应该当作“圣旨”一样去遵守,也不应该在写一行代码时就来看一下。规范只是作为一种参考以及在你遇到迷惑时给你一些指引。对于现有代码,你也不必按照以下要求去重写一遍。

第一章 文件

1.1 空格和制表符

我们规定,只使用空格而不使用制表符。一次缩进等于 4 个空格。你应该将编辑器设置成自动将制表符替换成空格。

1.2 行宽

尽量让你的代码保持在 100 列之内

通过设置 Xcode > Preferences > Text Editing > Show page guide,来使越界更容易被发现。

但是以下情况例外:

  1. import 语句。
  2. 整行有特殊意义的部分(例如书写长 URL)。

1.3 文件命名

每个 Swift 文件以 .swift 结尾。文件名规则和类名命名规则相同:FileName.swift。如果文件主要包含单个类型,那么文件用该类型命名。

如果是对一个已存在的类进行拓展,则采用原始类名 + 拓展名规则,拓展名规则也采用类的命名规则:UIView+Frame.swift。为某个已存在的类添加协议拓展也采用类似的规则:MyType+MyProtocol.swift

1.4 文件编码

源文件以 UTF-8 方式编码。

1.5 类型、变量和函数声明

一般来说一个文件只包含一个顶层类型。但是你也可以在同个文件中包含与之相关的类型,比如这个类的代理协议,以及它的一些简单辅助类。对于这些简单的辅助类可以考虑用 fileprivate 关键字将这些简单辅助功能限制在该文件内。

为一个类新增属性或者方法时,最好不要将它们直接加在现有代码之后,因为这种方式只是按照时间顺序组织代码。应该根据功能逻辑对代码进行分组,每个分组之间可以考虑用 MARK: 或者 MARK: - 进行分割。

如果一个类中有多个名字相同的方法(参数标签可能不同),这些方法应该放在一起,并且它们中间不能插入其他代码。

如果是对一个类的拓展,可以用 extension 的形式将它们组织到多个部分中,比较典型的是 UITableViewDataSourceUITableViewDelegate

第二章 通用命名规则

2.1 常量和变量命名规则

为 Swift 的变量命名时,采用苹果推荐的驼峰式命名方法。第一个单词用小写表示,之后每个单词的首字母大写。变量名应清晰易懂。

let myName: String = ""  // ok。 

禁止出现拼音和英文混合的形式,更不允许出现中文。这条规则适用于下述所有命名规则:

let myPingYinName: String = ""  // 禁止。
let myPingYinNAME: String = ""  // 禁止。

对于全局变量,可以用小写字母 k 或 g 开头来表示。

let kName: String = ""

命名中若出现缩略词,要么全部大写要么全部小写,规则根据驼峰式规则:

let myURL: String = ""
let urlForHost: String = ""

let myIDCard: String = ""
let idName: String = ""

避免单词的过度简写,除非这个简写众所周知:

let maxNum: Int = 10  // ok。
let textLab: UILabel = UILabel()  // 禁止。

2.2 方法/函数命名规则

方法和函数的命名规则也采用驼峰式命名方法,和变量的命名方法相同:

func doSomeThing()

2.3 类、协议、结构体命名规则

类、协议和结构体采用首字母大写的命名风格:

class SomeClass
struct SomeStruct
protocol SomeProtocol

2.4 枚举命名规则

对于枚举的名字,采用和类的命名方式相同的规则。而对于枚举中的每个 case,采用变量的命名规则:

enum SomeEnum {
    case one
    case two
}

在为每个 case 命名时,注意不要带上类型前缀:

// 禁止。
enum SomeEnum {
    case SomeEnumOne
    case someEnumTwo
}

第三章 常量和变量的声明

3.1 注意区分 var 和 let 关键字

变量的声明用 var 关键字,常量的声明用 let 关键字;禁止全部声明无脑用 var

3.2 请在声明时显式指定类型

Swift 是强类型语言,了解一个变量或常量的类型有助于我们了解该值的操作以及该值的一些特性。所以在声明任何变量或常量时都应显式指出该值的类型

let value = 0  // 不推荐。
let value: Int = 0  // 推荐。

var object = MyPerson()  // 不推荐。
var object: MyPerson = MyPerson()  // 推荐。

var optionValue: Int? = nil
// 以下两个可选值的绑定操作也应该指明类型。
if let value: Int = optionValue {
    // `value` 不等于 `nil` 时的操作。
}

guard let value: Int = optionValue else {
    // `value` 等于 `nil` 时的操作。
    return
}

注意变量名后面的冒号紧跟变量名并和类型之间有一个空格,禁止这种写法:

// 诸如以下的声明应该被禁止。
let value:Int = 0
let value : Int = 0
let tuple:(value1:Int,value2:String) = (0,"")
let dic:[String:Any]=[
    "key1":"value1",
    "key2":"value2"
]

// 正确写法:
let value: Int = 0
let tuple: (value1: Int, value2: String) = (0, "")
let dic: [String: Any] = [
    "key1": "value1",
    "key2": "value2"
]

有一种情况你可以不用标明类型,那就是你需要使用 self 时:

guard let strongSelf = self else {}

总之,在任意位置,包括变量的声明,方法的声明等地,请注意每个值和类型之间的分隔,以确保代码不会显得拥挤

注意:尽管 Swift 编译器会自动帮你推测值的类型你也应该这样做。因为在有时候 Swift 会推断出与你期望不同的类型,这将导致对该值进行操作的时候会产生错误;比如使用方法名相同的方法时会产生歧义。

3.3 注意变量在声明时就应该立即被初始化

对于一个常量,在声明时就必须赋值。而对于一个变量,强烈建议在声明时就进行初始化以避免未知行为

let value: Int = 0  // ok。

// 以下行为不建议。
var intValue: Int
// 中间这里可能还有其他代码。
intValue = 0

var intValue: Int = 0  // ok。

var doubleValue: Double = 0.0  // 推荐。
var person: Person = Person(name: "wy")  // 推荐。

3.4 作用域问题

对于一个方法,Swift 允许在该方法内部的任意位置声明变量。这里建议声明变量的位置距离变量使用的位置应尽可能近,以方便代码浏览者可以尽快了解该变量。

对于类 C 语言,在诸如 ifwhilefor 等语句中,属于这些语句内部的变量的作用域应控制在这些语句内部:

for (int i = 0; i < 1000000; ++i) {
    int j = 0;
    j.doSomeThing(i);
}

但是 Swift 例外。在 Swift 中所有的类型都是对象,若采用上述方法则每次进入作用域中都要调用构造函数,每次退出作用域都要调用析构函数,这将影响效率。

// 低效写法。
for i in 0..<1000000 {
    var j: Int = 0  // Int 构造函数和析构函数将分别调用 1000000 次!
    j.doSomeThing(i)
}

// 推荐写法。
var j: Int = 0  // 构造函数和析构函数都只调用一次。
for i in 0..<1000000 {
    j.doSomeThing(i)
}

3.5 整数类型的声明

对于整数类型,尽管 Swift 提供了诸如:Int64UInt8 等不同的长度的有符号和无符号整数,但在通常情况下你都应该使用 Int 来确保代码的统一性和兼容性Int 拥有与当前平台原生字长度相同的特性,所以除非在特殊情况下你才应该使用指定长度的声明形式。

注意:只在的确需要存储一个和当前平台原生字长度相同的无符号整数的时候才使用 UInt 。其他情况下,推荐使用 Int ,即使已经知道存储的值都是非负的。统一使用 Int 会提高代码的兼容性,同时可以避免不同数字类型之间的转换问题,也符合整数的类型推断。

3.6 元组的声明

元组可以组合多个不同类型的值。为了显式指明每个值的含义,在声明时就应该指明每个值的类型以及给一个好的名字

let http404Error = (404, "Not Found")  // 不推荐。

// 推荐。注意前一个值的逗号和下一个值的声明之间应该有一个空格。
let http404Error: (errorCode: Int, description: String) = (404, "Not Found")

注意逗号的书写位置。每个值之间用于分割的逗号都紧跟着前一个值,该规则对于之后的集合类型也适用。避免如下写法:

let http404Error = (404 , "Not Found")  // 不推荐。
let http404Error = (404 ,"Not Found")  // 不推荐。

对于逗号位置的规则之后不再赘述。

这里还有一个有意思的细节:对于第一种写法,编译器将变量的类型推断为 (Int, String),而对于第二种写法,编译器将类型推断为 (errorCode: Int, description: String)。很明显,第二种更直观。

与此同时,对元组中保存的值进行分别取值时,第二种声明的优势也更明显:

// 第一种声明方法的取值。
let errorCode: Int = http404Error.0

// 第二种声明方法的取值。
let errorCode: Int = http404Error.errorCode

3.7 可选值的声明

对于一个可选值来说,若没有初始化,则编译器默认赋值为 nil。根据上述原则,我们也应该对可选值显式初始化。

let optionalValue: Int? = nil
let http404Error: (errorCode: Int?, description: String?) = (nil, nil)

在声明可选类型时尽量避免隐式解包,因为可选值的隐式解包本身是不安全的。

但是那些基于 UI 生命周期而不是严格基于持有关系而存在的 UI 控件可以使用隐式解包。比如 xib 拖出来的 @IBOutlet 控件、viewDidLoad 里初始化的控件。因为这些属性一经初始化在生命周期中就肯定有值,你可以直接使用隐式解包来减小解包的负担。

class MyViewController: UIViewController {
    @IBOutlet var button: UIButton!
    
    var label: UILabel!
    
    override func viewDidLoad() {
        self.label = UILabel()
    }
}

3.8 集合类型的声明

注意用 let 来创建不可变集合,用 var 来创建可变集合。

对于数组来说,完整的声明类似如下形式:

let array: Array<Int> = [0, 1, 2, 3, 4]  // 注意每个元素之间有空格,下同。注意逗号位置。

这种完整的写法比较冗长,我们采用简写的形式:

let emptyArray: [Int] = []
let array: [Int] = [0, 1, 2, 3, 4]
let repeatArray: [Int] = [Int](repeating: 1, count: 5)

这里有一个细节需要注意:对于上例数组的第三种声明方法本质上是调用了 Int 结构体的 init(repeating repeatedValue: Element, count: Int) 初始化方法,所以完整的写法是:

let repeatArray: [Int] = [Int].init(repeating: 1, count: 5)

为了使代码简洁,我们在这里做个约定:今后对于所有的对象(包括结构体和类),在使用初始化构造函数时,我们去掉 init,时候类名后面直接跟括号的形式构造对象,例如:

let repeatArray: [Int] = [Int](repeating: 1, count: 5)
let dic: [String: Any] = [String: Any](minimumCapacity: 5)
let view: UIView = UIView()

而对于合集(Set)来说,Swift 没有提供简写的方法,所以我们采用完整的写法:

let sets: Set<String.Element> = ["a", "b", "c"]
let emptySets: Set<String.Element> = []

Set 的正确叫法应该是“集合”,这里为了与标题的集合类型区分开,所以采用“合集”的叫法。

对于字典,完整的写法如下:

let dic: Dictionary<String, Any> = ["key": "value"]

我们采用上面的方法统一采用简写的形式:

// 注意冒号紧跟字典的 Key,冒号和后面的字典 Value 之间空一格。
let dic: [String: Any] = ["key": "value"]
let emptyDic: [String: Any] = [:]

// 禁止如下写法:
let dic: [String : Any] = ["key" : "value"]
let dic: [String :Any] = ["key" :"value"]

// 若初始化时字典有多个值,每个键值对单独占据一行,以一个缩进开始。
// 字典的左括号空一格跟在等号后面,右括号另起一行在最后。
let dic: [String: Any] = [
    "key1": "value1",
    "key2": "value2",
    "key3": "value3"
]

// 字典数组写法,简写形式。
let dicArray: [[String: Any]] = []

3.9 闭包的声明

在声明闭包时应该完整写出每个参数的名称和类型。和上述一样,每个参数之间应该有一个空格,返回值的 -> 左右两边应该也有一个空格。

// 禁止。
let block:((String,Int)->Void)? = nil

// 推荐写法。
let block: ((_ str: String, _ value: Int) -> Void)? = nil

为了避免与返回值的可选类型产生歧义,如果你需要将闭包值设置为可选值,则必须将整个闭包用括号包裹,就像上例一样。

如果你的闭包不是可选的,那么你在声明时也应该给闭包一个默认值:

// 这里同样遵循方法可以一行写完就没必要换行的规则。
let block: (_ str: String, _ value: Int) -> Void = { _, _ in }

注意上例中闭包参数列表的书写规则,第一个参数标签和 "{" 之间有一个空格。闭包内的具体实现前面一个缩进:

block = { value1, value2 in
    // 从这里开始,前面有一个缩进。
}

// 禁止。
block = {value1, value2 in
    // 从这里开始。
}

若只想使用一个没有具体实现的空闭包,则参数列表的参数用 _ 占位,代码块和闭包变量在同一行:

block1 = {}  // 空闭包时,"{" 和 "}" 之间没有空格。
block2 = { _, _ in }  // 若闭包只有一行,in 和 "}" 之间也有一个空格。

在闭包声明时,请记住永远写出返回类型,尽管返回值是 void。这点和函数/方法的声明不同,因为在很多时候会和元组的声明产生歧义。

// 正确声明方法。
let block: ((_ str: String, _ value: Int) -> Void)? = nil

// 以下声明方式会报错:Tuple element cannot have two labels。
// 很明显,编译器以为你声明的是元组。
let block: (_ str: String, _ value: Int)? = nil

另外请注意,不能用 () 代替空返回值 Void

第四章 运算符和控制流

4.1 运算符书写规则

对于一元运算符,采用一元运算符+表达式的形式书写:!(i > 0)。一元运算符和表达式之间没有空格。

对于二元运算符,运算符的左右两边应该用空格隔开:

// 禁止。
if i>0 {}
let sum: Int = a+b

// 推荐。
if i > 0 {}
if i > 0 && j > 0 {}
let sum: Int = a + b

对于三元运算符则更应如此:

let value: Int = i >= 0 ? j : z

这里强调下,在使用三元运算符进行逻辑判断时,不应该有逻辑的嵌套,这会影响代码的可读性并且也容易出错。

// 禁止。
let value: Int = i > 0 ? j == 0 ? 0 : 1 : z

对于如上所说的运算符书写规则有一些例外,在书写范围表达式时,表达式的左右两边不需要有空格:

// 推荐。
for i in 0..<100 {}

// 禁止。
for i in 0 ..< 100 {}

4.2 流程控制语句书写的一般规则

与 C 和 Objective-C 不同,Swift 的 if 语句可以不用加括号,为了代码书写的流畅与美观,所以我们规定:在一般情况下(代表优先级的括号除外),if 语句不用括号包裹。

对于 ifwhilefor 等流程控制语句,左括号应与关键字在同一行,右括号在结尾另起一行。注意左括号左边的空格。

for i in 0..<100 {
    // 从这里开始。
}
while true {
    // 从这里开始。
}
if true {
    // 从这里开始。
}

禁止诸如以下的书写
for i in 0..<100{
    //
}
while true{
    //
}
if true{
    //
}
if (i == 0) {
    //
}
if(true) {
    //
}
switch i{
    //
}

若你的条件足够多且足够长,你也可以选择换行,此时为了避免混乱,"{" 可以另起一行书写。每行的条件左对齐并且逻辑运算符总是位于行尾。

if i == 0 &&
   j == 0 &&
   z == 0 {
    // 从这里开始。   
}

if aBooleanValueReturnedByAVeryLongOptionalThing() &&
   aDifferentBooleanValueReturnedByAVeryLongOptionalThing() &&
   yetAnotherBooleanValueThatContributesToTheWrapping()
{
    // 从这里开始。 
}

对于可选值绑定语句,条件换行时根据 let 关键字左对齐。注意:对于 guard 的可选值绑定语句,else 关键字必须和后续的 "{" 在同一行。

guard let value: Int = someOptionalValue,
      let value2: String = otherOptionalValue else {
    // 从这里开始。      
}

guard let value = aValueReturnedByAVeryLongOptionalThing(),
      let value2 = aDifferentValueReturnedByAVeryLongOptionalThing()
else {
    // 从这里开始。
}

// 若要写 where 进行条件限制,where 关键字可以考虑考虑换行缩进。
for i in 0..<100
    where i % 2 == 0 {
    // 从这里开始。
}

4.3 布尔值的书写

如果逻辑为真的表达式,可以不用显式写出 == true,但对于逻辑为非的表达式强烈建议显式写出 == false 而不是用 ! 运算符,因为这不够直观:

if i > 0 {}  // ok。
if i > 0 == false {}  // ok。
if !(i > 0) {}  // 不建议。

4.4 switch 开关语句的书写

正常情况下,每个 case 后面的条件不需要用括号包裹(优先级的括号除外)。

Swift 的 switch 语句没有隐式贯穿,这意味着你不需要在每个 case 后都写 break

switch 语句的左括号与关键字在同一行,并用一个空格隔开;右括号在结尾另起一行。

每个 case 关键字和 switch 关键字左对齐,case 语句内的代码换行开始并和 case 关键字之间有一个缩进。若要同时匹配多个值,每个值之间要用空格隔开,若匹配值足够多,你也可以选择换行,此时下一行与上一行左对齐:

switch i {
case 0:
    // 这里开始。前面有一个缩进
case 1, 2:
    // 这里开始。前面有一个缩进。
case 3, 4, 5, 6, 7, 8,
     9, 10, 11, 12, 13:
    // 从这里开始。
default:
    break  // 前面有一个缩进。
}

若可能,应尽量将开关语句的所有场景穷举,以避免代码走到 default 处而产生与你预期不同的行为。

当进行 switch 的模式匹配时,每个匹配的值之前都有单独的 letvar 关键字。禁止用一个 letvar 匹配整个模式匹配:

switch direction {
case let .north(angle: 30):
    // 从这里开始。
    
// 不建议这种写法:
case let .south(angle: angle, position: position):
    // 从这里开始。
    
// 推荐这种写法,多个参数可以换行并左对齐,对于缺省参数用 _ 代替:
case .east(angle: let angle, position: _,
               positionAngle: let positionAngle):
    // 从这里开始。
default:
    break
}

适用于整个模式匹配的前置简写 letvar 关键字是禁止的,因为当匹配模式的值本身是个变量时,会引入非预期行为:

enum DataPoint {
    case unlabeled(value: Int)
    case labeled(name: String, value: Int)
}

let label: String = "goodbye"

// 因为没有前置的 let 关键字,label 在这里是一个值,所以下面的模式匹配中只会匹配标签是 “goodbye” 的数据点。 
switch DataPoint.labeled(name: "hello", value: 100) {
case .labeled(name: label, value: let value):
    // 从这里开始。
}

// 每个单独的绑定使用前置 let 能清晰地表达引入了一个新的绑定(覆盖枚举项里的局部变量),而不是匹配局部变量的值。
// 这样,这个模式匹配会将数据点和任意字符串标签匹配。
switch DataPoint.labeled(name: "hello", value: 100) {
case .labeled(name: let label, value: let value):
    // 从这里开始。
}

在下面的例子中,如果你的意图是使用上面的 label 变量进行匹配,那么就会因为 let 适用于整个模式匹配,导致该值会被任何绑定的字符串所覆盖:

switch DataPoint.labeled(name: "hello", value: 100) {
case let .labeled(name: label, value: value):
    // 从这里开始。
}

4.5 return 的书写

return 关键字通常用在一个方法的结尾,代表方法的结束和返回值。若在一个方法中间位置使用 return 会导致流程提前退出,这容易引发未知问题。

应该尽量避免频繁在方法中间位置使用 return,这会使一段代码的逻辑变得复杂,且难以定位问题。

不要在 return 关键字后面加上非必要的圆括号,除非后面是一个表达式时你才可以这样做:

if i == 0 {
    return (true)  // 禁止。
}
if i == 0 {
    return(true)  // 禁止,return 不是方法。
}

4.6 提前退出的 guard

和 4.5 所述的规则对应。若你想提前退出一段逻辑,你可以用 guard 语句从当前作用域退出。

比起 if 条件语句,guard + return 的组合写法可以更好的从视觉上强调这里将会提前退出作用域。比起在一大段代码中频繁使用 return 要更直观,逻辑也更清晰。

另外,灵活的使用 guard 可以减少代码的缩进层级,增加代码美观程度以及可读性。

// 推荐写法。
func discombobulate(_ values: [Int]) throws -> Int {
    guard let first: Int = values.first else {
        throw DiscombobulationError.arrayWasEmpty
    }
    guard first >= 0 else {
        throw DiscombobulationError.negativeEnergy
    }

    var result: Int = 0
    for value in values {
        result += invertedCombobulatoryFactory(of: value)
    }
    return result
}

// 不推荐。
// 和上例逻辑相同,只是写法不同。
func discombobulate(_ values: [Int]) throws -> Int {
    if let first: Int = values.first {
        if first >= 0 {
            var result: Int = 0
            for value in values {
                result += invertedCombobulatoryFactor(of: value)
            }
            return result
        } else {
            throw DiscombobulationError.negativeEnergy
        }
    } else {
        throw DiscombobulationError.arrayWasEmpty
    }
}

第五章 方法与函数

5.1 方法/函数的声明和定义

一个方法的声明应该像这样:

func doSomeThing() -> Any {
    // 注意这里前面一个缩进。
    let i: Int = 0
}

// 返回值为空时,你可以不必显式写出 Void。
func doSomeThing()
// 禁止空返回值用这种写法:
func doSomeThing() -> ()

方法的左大括号 "{" 和参数列表的右括号 ")" 之间有一个空格。若方法有返回值,则返回值类型左右两边都有空格。

在方法结尾,右大括号 "}" 另起一行。方法内的每行具体实现前面有一个缩进。

对于运算符重载方法,运算符两边也遵循 4.1 所述的运算符书写规则:

// 注意运算符左右两边有空格。
static func == (lhs: MyType, rhs: MyType) -> Bool {
    // 从这里开始。
}

// 禁止。
static func ==(lhs: MyType, rhs: MyType) -> Bool {
    // 从这里开始。
}

若方法参数列表有多个参数,每个参数之间用一个空格隔开。

func doSomeThing(value1: String, value2: Int)

若方法参数列表比较多,则可以一行写一个参数,每行参数前一个缩进,并且右括号另起一行。

func doSomeThing(
    value1: String,
    value2: Int,
    value3: Any
) -> Any {
    // 具体实现从这里开始,前面一个缩进。
    let i: Int = 0
}

如果是返回值较长的情况下,你也可以对返回值换行:

public func doSomeThing(
    value1: Int,
    value2: String,
    value3: Any
) -> (
    value1: Int,
    value2: String,
    value3: Any    
) {
    // 从这里开始。
}

若你定义的方法的返回值是一个闭包,那么你应该将返回值闭包用括号包裹以增加可读性

// 不建议。
func functionName() -> () -> Int

// 推荐。
func functionName() -> (() -> Int)

一个方法的声明若带有泛型,这个方法的声明通常会很长,此时换行显得更重要了:

public func index<Elements: Collection, Element>(of element: Element, in collection: Elements) -> Elements.Index? where Elements.Element == Element, Element: Equatable

public func index<Elements: Collection, Element>(
    of element: Element,
    in collection: Elements
) -> Elements.Index?
where
    Elements.Element == Element,
    Element: Equatable
{
    // 从这里开始。
}

注意上面的 where 条件限制语句另起一行并和方法声明左对齐,where 语句的主体另起一行开始,并且和 where 关键字之间一个缩进。

在这种方法声明语句比较长的情况下,方法体开始的 "{" 可以考虑另起一行开始以避免显得混乱。

对于这种方法声明语句比较长的场景,有的语言喜欢采用括号左对齐的方式,但这容易产生锯齿效应,应该避免:

// 不推荐。
public func index<Elements: Collection, Element>(of element: Element,
                                                 in collection: Elements) -> Elements.Index?
    where Elements.Element == Element, Element: Equatable {
    // 从这里开始。
}

5.2 方法的调用

方法的调用应尽量和方法的声明保持一致。

调用时所有的参数在同一行:

doSomeThing(value1: "", value2: 0, value3: "")

或者每行一个参数,保持和声明时一样的规则。

doSomeThing(
    v1: "",
    val2: 0,
    value3: ""
)

若方法有返回值但你不需要,用 _ 表示缺省情况,并用 let 声明:

func functionHasRetuenValue() -> String
let _ = functionHasRetuenValue()

5.3 多返回值的函数

得益于 Swift 拥有类似元组这样可以组合多个值的类型,所以你可以让一个函数返回多个值。为了让函数返回多个值作为一个复合的返回值,你应该使用元组类型作为返回类型

在使用元组作为返回值时,你也应该遵循 3.6 中关于元组的声明:在返回值中指定元组中每个元素参数名和类型。

// 不推荐。
func doSomeThing() -> (Int, Int)

// 推荐写法。
func doSomeThing() -> (value1: Int, value2: Int)

在元组作为返回值的函数内部,当你要书写 return 语句时,为了使代码拥有统一的风格,你也应该指定元组的参数名字。

func doSomeThing() -> (value1: Int, value2: Int) {
    return (value1: 1, value2: 2)
}

除了元组之外当然你也可以使用字典来使函数返回多个值,但是除非在特定需求下否则不建议这样做。函数调用者在接收返回值时容易产生疑惑,因为字典中的键值是隐藏在函数的实现中的。

func doSomeThing() -> [String: String] {
    return [
        "key1": "value1",
        "key2": "value2"
    ]
}

let dic: [String: Any] = doSomeThing()
// 获取返回值的字典时,Key 只有方法的书写者知道,方法调用者只能知道字典类型。
let value1: String = dic[<#???#>]
let value2: String = dic[<#???#>]

5.4 函数参数的书写顺序

对于一个函数参数的书写顺序,我们这样规定:

  • 有默认值的参数,其位置在普通的无默认值的参数之后。
  • 对于函数类型的参数,统一写在参数列表的最后。

5.5 考虑用 final

若你确定一个方法不会被重写,请用 final 关键字修饰。Swift 在编译时会优化 final 修饰的方法,派发方式可能由函数表派发优化为直接派发。

第六章 闭包

6.1 记住永远显式写出 self

在类、结构体和枚举等实现主体中,无论是调用其中的属性还是方法,你都应该显式表明 self 来表明你确实是想调用属性和方法,而不是其他同名的临时变量。

class SomeClass {
    
    var property1: String = ""
    var property2: Int = 0
    
    func aMethod() {
        let property1: String = "other_property1"
        self.property1 = "self.property1"
        print(property1, self.property1)
    }
    
    func aOtherMethod() {
        self.property2 = 1
        self.aMethod()
    }
}

对于显式标明 self 这个规则在闭包中尤其有用。它可以帮助你发现你是否在闭包中捕获了当前对象而发生循环强引用。

6.2 慎用闭包捕获列表

闭包捕获列表显示捕获了它所处环境的变量或常量。需要注意的是:处于捕获列表中的值在闭包创建时会以常量的形式进行初始化

var a: Int = 0
var b: Int = 0
let closure: (_ value: Int) -> Void = { [a] in
    print(a, b)
}
 
a = 10
b = 10
closure()
// Prints "0 10"

注意上例中 a 值的变化。由于上述中 a 类型是 Int,是一个值类型,它在闭包环境中被重新初始化,所以可能产生与你期望不同的结果。

但如果你捕获的是引用类型,则不存在这个问题:

class SimpleClass {
    var value: Int = 0
}
var x: SimpleClass = SimpleClass()
var y: SimpleClass = SimpleClass()
let closure: (_ value: Int) -> Void = { [x] in
    print(x.value, y.value)
}
 
x.value = 10
y.value = 10
closure()
// Prints "10 10"

所以在使用捕获列表时,你必须清楚你捕获的是值类型还是引用类型来编写相应的代码,以确保代码行为符合你预期。

6.3 闭包的强引用循环

6.3.1 推荐使用 weak 而不使用 unowned

swift 提供了两种方式来打破闭包和实例之间的强引用循环:weakunowned

尽管 unowned 的效率提高,但是unowned 声明的对象必须保证它的生命周期必须不小于强引用它的对象的生命周期,否则你将会得到运行时崩溃。而在多人维护的大型项目中,这是很难做到的。除非你真的可以确保该对象不会为 nil 时你才可以使用 unowned

weak 生命的对象通常为可选值,因此在生命周期中它是可为 nil 的,这保证了你访问对象的安全性。

6.3.2 weak 下的局部强引用

weak 声明的对象可为 nil,所以你在使用对象时需要进行判断。你可以使用局部强引用的方式来确保安全使用这个可选对象:

class SomeClass {
    var property: String = "property"
    
    var block: (_ value: String) -> Void = { [weak self] str in
        guard let strongSelf: SomeClass = self else { return }
        strongSelf.property = "property_other"
    }
}

6.4 高阶函数的使用

对于高阶函数的调用,若闭包可以在一行写完,则没必要换行:

// 注意 { 和 } 之间的空格。
let array: [Int] = [0, 1, 2, 3, 4].filter({ $0 > 0 })
// 当然你也可以使用尾随闭包的形式让代码书写看起来更简洁。强烈推荐使用尾随闭包!
let array: [Int] = [0, 1, 2, 3, 4].filter { $0 > 0 }
// 若使用尾随闭包,请注意对应空格,禁止如下写法:
let array: [Int] = [0, 1, 2, 3, 4].filter {$0 > 0}

// 对于上述 Int 类型的数组,filter 方法接收一个“带一个 Int 类型的参数,返回值为 Bool 类型”的闭包。你也可以在一行内写完。
let array: [Int] = [1, 2, 3, 4].filter { (value: Int) -> Bool in return value > 0 }

// 没必要。
let array: [Int] = [0, 1, 2, 3, 4].filter { (value: Int) in
    return value > 0
}

对于上述的闭包写法我们规定,若闭包的参数列表用 $0$1 等形式表示,则可以不用在参数列表指明每个参数的类型;否则应该指明参数列表中每个参数的类型。如上例的第二种写法。

第七章 类、结构体和枚举

7.1 自定义类型书写规则

在自定义类型时,若有继承关系和遵循某些协议,则父类和类名后的冒号之间有一个空格,之后的协议列表中的每个协议之间也有一个空格。

若继承和协议列表很长时也应该换行:

class SomeClass:
    SomeSuperClass,
    SomeProtocol {
    // 从这里开始。    
}

class MyContainer<BaseCollection>:
    MyContainerSuperclass,
    MyContainerProtocol,
    SomeoneElsesContainerProtocol,
    SomeFrameworkContainerProtocol
where
    BaseCollection: Collection,
    BaseCollection.Element: Equatable,
    BaseCollection.Element: SomeOtherProtocolOnlyUsedToForceLineWrapping
{
    // 从这里开始。    
}

7.2 属性声明禁止类型左对齐

禁止在为多个属性指定类型时,所有类型左对齐的形式。因为这种形式在新属性引入时,需要修改所有的属性。

// 推荐。
class SomeClass {
    var value1: Int = 0
    var anotherValue: Int = 0
}

// 禁止。
class SomeClass {
    var value1:       Int = 0
    var anotherValue: Int = 0
}

7.3 枚举的声明

我们规定枚举的声明一行一个 case,除非同时满足下列条件你才可以在一行里用逗号分隔的形式写出所有枚举值:

  1. 枚举含义简单,从名字就可以清晰看出代表含义,并且不需要注释。
  2. 枚举没有关联值。

如果枚举有关联值,建议写出关联值标签来表明关联值的含义:

public enum Token {
    case comma
    case semicolon
    case identifier(label: String)
}

若枚举没有关联值,则不允许在后增加空的括号。

如果一个枚举中的所有场景都是可递归的,那么就将整个枚举值声明为可递归,没必要对每个 case 都声明一次:

enum ArithmeticExpression {
    case number(Int)
    indirect case addition(ArithmeticExpression, ArithmeticExpression)
    indirect case multiplication(ArithmeticExpression, ArithmeticExpression)
}

indirect enum ArithmeticExpression {
    case number(Int)
    case addition(ArithmeticExpression, ArithmeticExpression)
    case multiplication(ArithmeticExpression, ArithmeticExpression)
}

7.4 静态属性和类属性声明

在书写静态属性和类属性时,不必加上类型后缀:

class UIColor {
    class var red: UIColor {}  // 推荐。
    class var redColor: UIColor {}  // 不推荐。
}

7.5 计算属性书写

对于 getset 等语句,若一行可以写完则没必要换行。

var someProperty: Int {
    get { return otherObject.property }
    set { otherObject.property = newValue }
}

若一个属性只有 get 语句,则 get 可以省略:

var someProperty: Int {
    return 0
}

7.6 属性/方法分组

在某些场景下你可能需要对某些属性或者方法进行分组,这时你可以考虑声明一个没有枚举值的枚举类型来创建一个“命名空间”。由于枚举类型没有枚举值,所以该枚举值不会有实例:

// 推荐。
enum ScreenValue {
    static let screenWidth: CGFloat = UIScreen.main.bounds.width
    static let screenHeight: CGFloat = UIScreen.main.bounds.height
}

// 不推荐。
struct ScreenValue {
    private init() {}
    
    static let screenWidth: CGFloat = UIScreen.main.bounds.width
    static let screenHeight: CGFloat = UIScreen.main.bounds.height
}

7.7 避免自定义新的运算符

随意的自定义新的运算符会导致代码难以理解,因此除非你确定你自定义的新运算符有更好的含义且容易被理解的情况下你才可以这样做。

第八章 访问控制符规则

8.1 省略 internal

若你不指定任何访问控制符,则访问控制符默认是 internal,你不必显式写出它

8.2 extension 的访问控制符

我们通常会用 extension 为某个类增加拓展,对于拓展的访问控制符有两种写法:

// 这种写法访问控制符写在 extension 关键字前面,本次拓展里的所有方法和计算属性共享这个访问控制符。
public extension SomeClass {
    
    // 该方法其实是 puiblic。
    func doSomeThingExtension()
}

// 这种写法分别控制本次拓展里的每个计算属性和方法的访问控制符。
extension SomeClass {

    // public。
    public func doSomeThingExtensionPublic()
    
    // internal。
    func doSomeThingExtensionInternal()
}

为了避免歧义和准确表示意图,我们统一用第二种方法

第九章 注释

9.1 单行注释

在 swift 里,单行注释应该用 // 而不应该用 /* */

在使用行内注释时,强烈建议 // 距离末尾的代码应该有两个空格的距离,并且注释开始的内容和 // 应该有一个空格的距离:

// 推荐。
let value: Int = 0  // 这是一个整数。

// 禁止。
let str: String = ""//这是一个字符串。

9.2 多行注释

若你需要多行注释,则每行注释前面都应该有 //,而不能使用 /** */

// 推荐写法。
// 这是一个多行注释的开始。
// 这是一个整数。
let value: Int = 0

// 禁止。
/**
    这是一个多行注释。
    这是一个整数。
*/
let str: String = ""

9.3 文档注释

文档注释用 /// 表示,而不能使用 /** */。无论是单行文档注释还是多行文档注释都遵循此规则:

/// 这是推荐的写法。
/// 文档注释。
/// 这是一个整数。
let value: Int = 0

/// 以下写法是禁止的。
/// 这是一个整数。
let str: String = ""

如果一个方法可以用简单的一句话概括,那么只要使用单行的文档注释即可。否则,你应该在注释中写出方法的含义、参数、返回值和异常处理。

/// Returns the output generated by executing a command.
///
/// - Parameter command: The command to execute in the shell environment.
/// - Returns: A string containing the contents of the invoked process's
///   standard output.
func execute(command: String) -> String {
    // 从这里开始。
}

/// Returns the output generated by executing a command with the given string
/// used as standard input.
///
/// - Parameters:
///     - command: The command to execute in the shell environment.
///     - stdin: The string to use as standard input.
/// - Returns: A string containing the contents of the invoked process's
///   standard output.
func execute(command: String, stdin: String) -> String {
    // 从这里开始。
}

若方法只有一个参数,则使用内联形式的 Parameter。若方法有多个参数,则使用分组形式的 Parameters,下一级的注释和上一级的注释间保持一个缩进,就像上面的例子一样。

其他注意事项

不推荐魔法值(即未经定义的常量)

若一个项目由多人维护,使用魔法值会产生疑惑:

// 推荐。
let maxNum: Int = 5
if i == maxNum {}

// 不推荐。
if i == 5 {}

多个修饰符的书写顺序

若你要同时书写多个修饰符,请按照:@objc(注解)、访问控制、staticfinal 的顺序:

@objc public static final doSomeThing()

带参数的注解单独占据一行

如果一个注解带参数你可以考虑将其单独占据一行,和主体声明采用左对齐的方式:

@available(iOS 9.0, *)
public func doSomeThing {}

若注解不带参数,则可以不用换行。

禁止使用分号来分隔语句

尽管 Swift 允许在每行代码末尾使用分号来代表语句结束,但是你也不应该这样做。

// 以下行为禁止。
let value: Int = 0; let value2: String = ""
func sum(a: Int, b: Int) {
    let sum: Int = a + b;
    print(sum);
}

这意味着分号只能出现在字符串字面值或者注释中。

总之,只在你确保一行写多个语句可以使代码更简洁并且不影响逻辑的阅读时,你才可以这样做。如果你不确定哪种好,那么就使用多行语句。

将警告看成是错误

代码在编译时应尽可能没有警告,警告就意味着程序潜伏着安全漏洞,现在没问题不代表将来没问题。

对于第三方框架产生的警告,如果使用 Cocoapods 进行管理,则可以考虑在每个第三方框架的子工程下对 build settings 将其关闭。

本文参考

  1. juejin.cn/post/697996…
  2. pages.swift.gg/google-swif…