Airbnb Swift风格指南(翻译)

575 阅读12分钟

前言

由于团队在使用Swift开发应用,而Swift又过于灵活,在编写代码过程中出现一些风格差异,导致团队成员在阅读其他人代码时会出现一些门槛,不利于code review;所以,在空闲时间,参考国外在使用Swift的大厂的一些规范,并将其翻译下来。

目标

按照这份指南进行编程能够:

  • 使代码更易阅读,对自己不熟悉的更易理解
  • 使代码更易维护
  • 减少简单的编码错误
  • 减少编码中的认知负担
  • 更多的关注于代码逻辑差异的讨论,而不是风格差异

注意:代码简洁不是最主要的目的,当代码质量(如可读性、简单性、清晰度)保持稳定或者进一步提高,才更多的考虑代码简洁。

宗旨

  • 这份指南是在官方Swift API Design Guildelines基础上的一份增补,其中规则不能与官方指南相抵触。
  • 这些规则不能与Xcode里面的 ^ + I缩进规则向冲突
  • 我们努力使每条规则变得可行:
    • 如果某条规则改变了代码格式,需要使它能够自动的对代码进行重新格式化(不论是使用SwiftLint自动纠错或者SwiftFormat)。
    • 这些规则不会直接改变代码格式,我们应该有一个提示规则并抛出一条警告
    • 规则之外的意外情况尽量少,并且要有充分的理由

目录

  1. Xcode格式化
  2. 命名
  3. 样式 i. 函数 ii. 闭包 iii. 操作符
  4. 模式
  5. 文件组织
  6. Objective-C互通性

Xcode格式化

可以在build phase里添加如下脚本,使设置生效

#!/bin/bash

set -e

defaults write com.apple.dt.Xcode DVTTextEditorTrimTrailingWhitespace -bool YES
defaults write com.apple.dt.Xcode DVTTextEditorTrimWhitespaceOnlyLines -bool YES

defaults write com.apple.dt.Xcode DVTTextIndentTabWidth -int 2
defaults write com.apple.dt.Xcode DVTTextIndentWidth -int 2

defaults write com.apple.dt.Xcode DVTTextPageGuideLocation -int 100
  • 每一行应该限制最多100个字符

由于屏幕尺寸比较到,我们选择限制的字符个数大于80

  • 使用2个空格缩进
  • 截掉所以行尾部的空格

PS:这几个规则不一定符合我们的编码风格,我觉得不一定要遵守

命名

  • 使用帕斯卡命名法(PascalCase)命名协议名,其他则使用驼峰命名法命名。
protocol SpaceThing {
  // ...
}

class SpaceFleet: SpaceThing {

  enum Formation {
    // ...
  }

  class Spaceship {
    // ...
  }

  var ships: [Spaceship] = []
  static let worldName: String = "Earth"

  func addShip(_ ship: Spaceship) {
    // ...
  }
}

let myFleet = SpaceFleet()

例外情况:如果私有属性支持具有更高访问级别的同名属性或方法,则可以在其下加下划线(_)前缀

Why?

在某些特定情况下,支持属性或方法比使用更具描述性的名称更容易阅读。

// 类型擦除
public final class AnyRequester<ModelType>: Requester {

  public init<T: Requester>(_ requester: T) where T.ModelType == ModelType {
    _executeRequest = requester.executeRequest
  }

  @discardableResult
  public func executeRequest(
    _ request: URLRequest,
    onSuccess: @escaping (ModelType, Bool) -> Void,
    onFailure: @escaping (Error) -> Void) -> URLSessionCancellable
  {
    return _executeRequest(request, session, parser, onSuccess, onFailure)
  }

  private let _executeRequest: (
    URLRequest,
    @escaping (ModelType, Bool) -> Void,
    @escaping (NSError) -> Void) -> URLSessionCancellable

}
// 用较具体的类型支持较不具体的类型
final class ExperiencesViewController: UIViewController {
  // We can't name this view since UIViewController has a view: UIView property.
  private lazy var _view = CustomView()

  loadView() {
    self.view = _view
  }
}
  • **使用类似isSpaceship,hasSpacesuit等名称命名布尔类型变量。**这能清楚的表示这是布尔类型,而不是其他类型。

  • 名称缩写(如URL)应该全部大写,当它作为名称开头时,则按照驼峰命名规则,应该全部小写。

// WRONG
class UrlValidator {

  func isValidUrl(_ URL: URL) -> Bool {
    // ...
  }

  func isProfileUrl(_ URL: URL, for userId: String) -> Bool {
    // ...
  }
}

let URLValidator = UrlValidator()
let isProfile = URLValidator.isProfileUrl(URLToTest, userId: IDOfUser)

// RIGHT
class URLValidator {

  func isValidURL(_ url: URL) -> Bool {
    // ...
  }

  func isProfileURL(_ url: URL, for userID: String) -> Bool {
    // ...
  }
}

let urlValidator = URLValidator()
let isProfile = urlValidator.isProfileURL(urlToTest, userID: idOfUser)
  • 命名应该把共同的部分放在前面,不同的部分放在后面。“共同的”的含义取决于上下文,但应大致表示“最能帮助您缩小搜索范围的项目”。最重要的是,要与命名排序的方式保持一致。
// WRONG
let rightTitleMargin: CGFloat
let leftTitleMargin: CGFloat
let bodyRightMargin: CGFloat
let bodyLeftMargin: CGFloat

// RIGHT
let titleMarginRight: CGFloat
let titleMarginLeft: CGFloat
let bodyMarginRight: CGFloat
let bodyMarginLeft: CGFloat
  • 当名称容易出现歧义时,命名时应该包含名称类型的暗示
// WRONG
let title: String
let cancel: UIButton

// RIGHT
let titleText: String
let cancelButton: UIButton
  • **事件处理函数要使用过去式语句命名。**如果不需要为了清楚起见,这个规则可以忽略
// WRONG
class ExperiencesViewController {

  private func handleBookButtonTap() {
    // ...
  }

  private func modelChanged() {
    // ...
  }
}

// RIGHT
class ExperiencesViewController {

  private func didTapBookButton() {
    // ...
  }

  private func modelDidChange() {
    // ...
  }
}
  • **避免Objective-C风格首字母缩写前缀。**在Swift里面不需要再顾虑命名冲突问题。
// WRONG
class AIRAccount {
  // ...
}

// RIGHT
class Account {
  // ...
}
  • 在classes命名中,如果不是view controllers避免使用*Controller

Why? Controller是一个重载后缀,不提供有关类职责的信息。

样式

  • 当类型很容易推导的时候,则不需要再声明类型。
// WRONG
let host: Host = Host()

// RIGHT
let host = Host()
enum Direction {
  case left
  case right
}

func someDirection() -> Direction {
  // WRONG
  return Direction.left

  // RIGHT
  return .left
}
  • 除非需要消除歧义,或者语言需要,否则不要使用self
final class Listing {

  init(capacity: Int, allowsPets: Bool) {
    // WRONG
    self.capacity = capacity
    self.isFamilyFriendly = !allowsPets // `self.` not required here

    // RIGHT
    self.capacity = capacity
    isFamilyFriendly = !allowsPets
  }

  private let isFamilyFriendly: Bool
  private var capacity: Int

  private func increaseCapacity(by amount: Int) {
    // WRONG
    self.capacity += amount

    // RIGHT
    capacity += amount

    // WRONG
    self.save()

    // RIGHT
    save()
  }
}
  • 在多行的数组或者字典中,最后一个元素后面要加上逗号(,)
// WRONG
let rowContent = [
  listingUrgencyDatesRowContent(),
  listingUrgencyBookedRowContent(),
  listingUrgencyBookedShortRowContent()
]

// RIGHT
let rowContent = [
  listingUrgencyDatesRowContent(),
  listingUrgencyBookedRowContent(),
  listingUrgencyBookedShortRowContent(),
]
  • **为了使元组更加清晰,对元组里面的成员命名。**经验法则:如果有3个以上的字段,你可能应该使用struct
// WRONG
func whatever() -> (Int, Int) {
  return (4, 4)
}
let thing = whatever()
print(thing.0)

// RIGHT
func whatever() -> (x: Int, y: Int) {
  return (x: 4, y: 4)
}

// THIS IS ALSO OKAY
func whatever2() -> (x: Int, y: Int) {
  let x = 4
  let y = 4
  return (x, y)
}

let coord = whatever()
coord.x
coord.y
  • 将冒号(:)放在标识符之后,后面跟一个空格。
// WRONG
var something : Double = 0

// RIGHT
var something: Double = 0
// WRONG
class MyClass : SuperClass {
  // ...
}

// RIGHT
class MyClass: SuperClass {
  // ...
}
// WRONG
var dict = [KeyType:ValueType]()
var dict = [KeyType : ValueType]()

// RIGHT
var dict = [KeyType: ValueType]()

  • 在返回箭头(->)两侧都加一个空格,使其可读性更强
// WRONG
func doSomething()->String {
  // ...
}

// RIGHT
func doSomething() -> String {
  // ...
}
// WRONG
func doSomething(completion: ()->Void) {
  // ...
}

// RIGHT
func doSomething(completion: () -> Void) {
  // ...
}
  • 忽略不需要的括号
// WRONG
if (userCount > 0) { ... }
switch (someValue) { ... }
let evens = userCounts.filter { (number) in number % 2 == 0 }
let squares = userCounts.map() { $0 * $0 }

// RIGHT
if userCount > 0 { ... }
switch someValue { ... }
let evens = userCounts.filter { number in number % 2 == 0 }
let squares = userCounts.map { $0 * $0 }
  • 当所有参数都未标记时,省略case语句中的enum关联值。
// WRONG
if case .done(_) = result { ... }

switch animal {
case .dog(_, _, _):
  ...
}

// RIGHT
if case .done = result { ... }

switch animal {
case .dog:
  ...
}
  • 对于NSRange等,请使用构造函数代替Make()函数。
// WRONG
let range = NSMakeRange(10, 5)

// RIGHT
let range = NSRange(location: 10, length: 5)

函数

  • 定义函数时,忽略Void返回值类型
// WRONG
func doSomething() -> Void {
  ...
}

// RIGHT
func doSomething() {
  ...
}

闭包

  • **在闭包声明中更偏向于使用Void返回值类型,尽量少使用()返回值类型。**如果在函数声明中你必须指定一个Void类型返回值,为了提升可读性,使用Void,而不是使用()
// WRONG
func method(completion: () -> ()) {
  ...
}

// RIGHT
func method(completion: () -> Void) {
  ...
}
  • 将未使用的闭包参数命名为下划线(_

Why? 将未使用的闭包参数命名为下划线,可以清楚的表示哪些参数使用,哪些参数不使用,减轻阅读闭包时的认知负担。

// WRONG
someAsyncThing() { argument1, argument2, argument3 in
  print(argument3)
}

// RIGHT
someAsyncThing() { _, _, argument3 in
  print(argument3)
}
  • 单行闭包应在每个中间加一个空格
// WRONG
let evenSquares = numbers.filter {$0 % 2 == 0}.map {  $0 * $0  }

// RIGHT
let evenSquares = numbers.filter { $0 % 2 == 0 }.map { $0 * $0 }

操作符

  • **应在中间操作符两侧有一个空格。**当有许多操作符时,最好使用括号(())进行明显的分组,而不要使用空白宽度。这个规则不适用于范围运算(如1...3)和后缀或前缀运算(如guest?-1)。
// WRONG
let capacity = 1+2
let capacity = currentCapacity   ?? 0
let mask = (UIAccessibilityTraitButton|UIAccessibilityTraitSelected)
let capacity=newCapacity
let latitude = region.center.latitude - region.span.latitudeDelta/2.0

// RIGHT
let capacity = 1 + 2
let capacity = currentCapacity ?? 0
let mask = (UIAccessibilityTraitButton | UIAccessibilityTraitSelected)
let capacity = newCapacity
let latitude = region.center.latitude - (region.span.latitudeDelta / 2.0)

模式

  • **尽可能在初始化时初始化属性,而不是使用隐式展开的可选属性。**其中一个例外就是UIViewController的view属性。
// WRONG
class MyClass: NSObject {

  init() {
    super.init()
    someValue = 5
  }

  var someValue: Int!
}

// RIGHT
class MyClass: NSObject {

  init() {
    someValue = 0
    super.init()
  }

  var someValue: Int
}
  • **避免在init()中执行有意义或者耗时的工作。**避免做一些类似数据库连接、网络请求、从磁盘读取大量数据等工作。如果需要在准备好使用对象之前完成这些操作,可以创建一个类似于start()函数的内容。

  • **将复杂的属性观察器提取到方法中。**这样减少前台,隔离属性声明的副作用影响,并明确使用隐式传递的参数(如oldValue)。

// WRONG
class TextField {
  var text: String? {
    didSet {
      guard oldValue != text else {
        return
      }

      // Do a bunch of text-related side-effects.
    }
  }
}

// RIGHT
class TextField {
  var text: String? {
    didSet { textDidUpdate(from: oldValue) }
  }

  private func textDidUpdate(from oldValue: String?) {
    guard oldValue != text else {
      return
    }

    // Do a bunch of text-related side-effects.
  }
}
  • **将复杂的回调block提取到方法中。**这限制了weak-self的引入导致的复杂性,并减少了嵌套。如果您需要在方法调用中引用self,请在回调过程中使用guard来包装self。
//WRONG
class MyClass {

  func request(completion: () -> Void) {
    API.request() { [weak self] response in
      if let strongSelf = self {
        // Processing and side effects
      }
      completion()
    }
  }
}

// RIGHT
class MyClass {

  func request(completion: () -> Void) {
    API.request() { [weak self] response in
      guard let this = self else { return }
      this.doSomething(this.property)
      completion()
    }
  }

  func doSomething(nonOptionalParameter: SomeClass) {
    // Processing and side effects
  }
}
  • 最好在作用域的开头使用guard。

Why? 当所有保护语句都放在顶部而不是与业务逻辑混合在一起时,就更容易推理出代码块。

  • **访问控制应尽可能严格。**除非需要这种行为,使用public而非open,使用private而非fileprivate

  • **尽可能避免使用全局函数。**首选是在类型中定义方法。

// WRONG
func age(of person, bornAt timeInterval) -> Int {
  // ...
}

func jump(person: Person) {
  // ...
}

// RIGHT
class Person {
  var bornAt: TimeInterval

  var age: Int {
    // ...
  }

  func jump() {
    // ...
  }
}
  • **如果常量是私有的,则最好将其放在文件的顶层。**如果它们是publicinternal,则将它们定义为静态属性以用于命名空间。
private let privateValue = "secret"

public class MyClass {

  public static let publicValue = "something"

  func doSomething() {
    print(privateValue)
    print(MyClass.publicValue)
  }
}
  • **使用无case的枚举将publicinternal常量和函数组织到名称空间中。**避免创建无命名空间的全局常亮或函数,随意嵌套名称空间以增加清晰度。

Why? 无case的枚举也能够正常使用,因为枚举不能被实例化,这刚好满足我们的意图。

enum Environment {

  enum Earth {
    static let gravity = 9.8
  }

  enum Moon {
    static let gravity = 1.6
  }
}
  • **使用Swift的自动枚举值,除非它们映射到外部源。**添加备注解释为什么定义显式值。

Why? 依赖Swift的自动枚举值,能够尽可能的减少错误,增加可读性且编码更快速。但是,如果枚举值映射到外部源(如从网络请求来的)或者跨二进制文件持久化,则应明确定义这些值,并记录这些值映射到的内容。

这样可以确保,如果有人在中间添加了新的值,他们就不会意外破坏之前的逻辑。

// WRONG
enum ErrorType: String {
  case error = "error"
  case warning = "warning"
}

enum UserType: String {
  case owner
  case manager
  case member
}

enum Planet: Int {
  case mercury = 0
  case venus = 1
  case earth = 2
  case mars = 3
  case jupiter = 4
  case saturn = 5
  case uranus = 6
  case neptune = 7
}

enum ErrorCode: Int {
  case notEnoughMemory
  case invalidResource
  case timeOut
}

// RIGHT
enum ErrorType: String {
  case error
  case warning
}

/// These are written to a logging service. Explicit values ensure they're consistent across binaries.
// swiftlint:disable redundant_string_enum_value
enum UserType: String {
  case owner = "owner"
  case manager = "manager"
  case member = "member"
}
// swiftlint:enable redundant_string_enum_value

enum Planet: Int {
  case mercury
  case venus
  case earth
  case mars
  case jupiter
  case saturn
  case uranus
  case neptune
}

/// These values come from the server, so we set them here explicitly to match those values.
enum ErrorCode: Int {
  case notEnoughMemory = 0
  case invalidResource = 1
  case timeOut = 2
}
  • 仅当它们具有语义时才使用可选。

  • **尽可能使用不可变值。**使用mapcompactMap,不要创建新的集合;使用filter,不要从可变集合移除元素。

Why? 可变变量会增加复杂性,因此请尝试将它们保持在尽可能小的范围内。

// WRONG
var results = [SomeType]()
for element in input {
  let result = transform(element)
  results.append(result)
}

// RIGHT
let results = input.map { transform($0) }
// WRONG
var results = [SomeType]()
for element in input {
  if let result = transformThatReturnsAnOptional(element) {
    results.append(result)
  }
}

// RIGHT
let results = input.compactMap { transformThatReturnsAnOptional($0) }
  • **通过使用断言方法(assert)和生产中的适当日志记录来处理意外但可恢复的条件。如果意外情况无法恢复,请使用前置条件方法(precondition)或fatalError()。**这在崩溃和提供意外情况监听之间提供了一种平衡。仅当失败信息时动态时,我们更倾向于使用fatalError取代precondition方法,这是由于precondition方法在崩溃报告中不会报告信息。
func didSubmitText(_ text: String) {
  // It's unclear how this was called with an empty string; our custom text field shouldn't allow this.
  // This assert is useful for debugging but it's OK if we simply ignore this scenario in production.
  guard !text.isEmpty else {
    assertionFailure("Unexpected empty string")
    return
  }
  // ...
}

func transformedItem(atIndex index: Int, from items: [Item]) -> Item {
  precondition(index >= 0 && index < items.count)
  // It's impossible to continue executing if the precondition has failed.
  // ...
}

func makeImage(name: String) -> UIImage {
  guard let image = UIImage(named: name, in: nil, compatibleWith: nil) else {
    fatalError("Image named \(name) couldn't be loaded.")
    // We want the error message so we know the name of the missing image.
  }
  return image
}
  • 默认类型方法为static

Why? 如果需要重写某个方法,则作者应改为使用class关键字来选择该功能。

// WRONG
class Fruit {
  class func eatFruits(_ fruits: [Fruit]) { ... }
}

// RIGHT
class Fruit {
  static func eatFruits(_ fruits: [Fruit]) { ... }
}
  • 默认类为final

Why? 如果需要重写某个类,则作者应通过省略final关键字来选择使用该功能。

// WRONG
class SettingsRepository {
  // ...
}

// RIGHT
final class SettingsRepository {
  // ...
}
  • switch枚举值时,切勿使用default case。

Why? 枚举每个case都要求开发人员和审阅者,在添加新case时必须考虑每个switch语句的正确性。

// WRONG
switch anEnum {
case .a:
  // Do something
default:
  // Do something else.
}

// RIGHT
switch anEnum {
case .a:
  // Do something
case .b, .c:
  // Do something else.
}
  • 如果不需要使用该值,请检查nil而不是使用可选绑定。

Why? 检查nil可以立即清楚该语句的意图。可选绑定不太明确。

var thing: Thing?

// WRONG
if let _ = thing {
  doThing()
}

// RIGHT
if thing != nil {
  doThing()
}

组织文件

  • 按字母顺序排列的模块将在文件顶部的标题注释的最后一行下方导入一行。不要在import语句之间添加其他换行符。

Why? 一种标准的组织方法可以帮助工程师更快地确定文件所依赖的模块。

// WRONG

//  Copyright © 2018 Airbnb. All rights reserved.
//
import DLSPrimitives
import Constellation
import Epoxy

import Foundation

//RIGHT

//  Copyright © 2018 Airbnb. All rights reserved.
//

import Constellation
import DLSPrimitives
import Epoxy
import Foundation

例外:@testable import应该组织在常规导入之后,并使用空行分割。

// WRONG

//  Copyright © 2018 Airbnb. All rights reserved.
//

import DLSPrimitives
@testable import Epoxy
import Foundation
import Nimble
import Quick

//RIGHT

//  Copyright © 2018 Airbnb. All rights reserved.
//

import DLSPrimitives
import Foundation
import Nimble
import Quick

@testable import Epoxy
  • **将空白垂直空白限制为一行。**对于以不同高度的空白行将文件进行逻辑分组则偏爱这种风格指南。

  • 文件应以换行符结尾。

Objective-C互通性

  • **首选纯Swift类而不是NSObject的子类。**如果您的代码需要由某些Objective-C代码使用,则将其包装以暴露所需的功能。根据需要在各个方法和变量上使用@objc,而不是通过@objcMembers将类中的所有API暴露给Objective-C。

PS: 这条不适用于我们的代码,我们需要热修复。

class PriceBreakdownViewController {

  private let acceptButton = UIButton()

  private func setUpAcceptButton() {
    acceptButton.addTarget(
      self,
      action: #selector(didTapAcceptButton),
      forControlEvents: .TouchUpInside)
  }

  @objc
  private func didTapAcceptButton() {
    // ...
  }
}