Swift代码规范与实践

文档概述

Swift是苹果公司(Apple Inc.)于2014年推出的一门新的计算机编程语言,可以用来编写iOS、Mac OS、tvOS以及watchOS的应用. Swift是一门新的语言,与OC相比,拥有众多优势,例如:Swift支持OOP、FRP和泛型编程等多种编程范式,相比陈旧的OC而言,使用Swift可以写出很多效率更高、更安全、更优雅的代码.

关于Swift的更多信息,此文档不展开讨论,给出以下参考:

本篇文档着重探讨一个问题,那就是Swift编程规范的问题,例如:

someArray.map({ $0 })
someArray.map { $0 }
someArray.map({ element in
return element
})
someArray.map({ element in
element
})

以上几种写法,在相同的语义和上下文环境中,都是同一个意思,得到的结果也是一样的,再如:

var optionalValue: OptionalType
// 对于可选类型解析的方式:
// 1. if判断.
if optionValue != nil {
// 进行当value有值时的操作.
// ...
} else {
// 进行当value为nil时的操作.
// ...
}
// 2.a. if可选绑定.
if let value = optionalValue {
// 进行当value有值时的操作.
// ...
} else {
// 进行当value为nil时的操作.
// ...
}
// 2.b. guard可选绑定.
guard let value = optionalValue else {
// 进行当value为nil时的操作.
// ...
return
}
// 进行当value有值时的操作.
// ...

对于可选绑定(Optional Chaining)的处理,以上几种方式皆可,但是在实际的项目中,对不同的场景,在代码规范上就需要注意,例如:在上述2.a2.b都可以应用的地方,优选2.b,因为这样的写法逻辑判断更明确.

Swift作为一个简单的语言,易学易用,那么怎样才能尽量规范化,尽量使不同的Programmer写出风格上尽量统一的代码呢?为了解决这个问题,Swift开源社区出了一个工具:SwiftLint. SwiftLint由Realm进行开发和维护,项目概况:commits

language

SwiftLint

简介

SwiftLint是一款用于检查Swift源码编程规范的工具,SwiftLint使用事先给定的规则,通过对Clang层和Swift的代码检查工具SourceKit嵌入的方式工作,在静态代码检查期间对代码中不满足规则的代码给出警告或者错误提示;SwiftLint还支持autocorrect以自动更正代码的规范,但是这个操作存在一定的风险,因为autocorrect是在语言层面进行的修正工作,盲目的使用可能会造成业务逻辑方面的错误;若需要在业务逻辑的地方忽略SwiftLint的警告,可通过声明的方式:

// swiftlint:disable warning_type

进行屏蔽.

由于SwiftLint主要是对SourceKit代码检查嵌入的方式进行工作的,所以SwiftLint是独立于Swift版本的,这使得SwiftLint不依赖于Swift的版本,在同一个项目工程里,SwiftLint可以同时与多个Swift版本工作,例如可以在工程里使用一个SwiftLint版本同时兼容Swift3.2和Swift4.0.

SwiftLint使用事先定好的规则进行代码的检查,规则的定义分为两种:

  1. 默认规则
  2. 可选规则

默认规则是默认开启的,可选规则是默认不开启的,两者都可以在配置文件中进行配置.

工具安装

使用HomeBrew

打开Terminal,输入以下命令:

brew install swiftlint

注意:如果Mac OS的系统是迁移的,可能会造成/usr/local/...下面的文件夹的权限问题,需手动开启当前用户的rw权限.

使用CocoaPods

Podfile里添加以下pod:

pod 'SwiftLint'

运行:

pod install --verbose

即可安装. 通过CocoaPods安装的方式,需要在Script Build Phases里添加:${PODS_ROOT}/SwiftLint/swiftlint才能使用. 使用CocoaPods安装的方式还有一个好处,就是可以指定安装的SwiftLint的版本,这是官方给出的一个Swift版本的支持:

对于Swift3.x和Swift4.x,可以使用最新的版本,但是对于更老的版本,只能使用对应的版本才能正常使用SwiftLint的功能.

使用Mint

对于Mint的介绍,Mint是一个安装和运行Swift命令行的包管理器,GitHub主页

使用Mint安装,输入以下命令:

mint run realm/SwiftLint

即可.

使用安装包

在项目主页latest GitHub release下载SwiftLint.pkg进行安装.

源码编译

仅支持Xcode9.x(或更高)的版本. 安装方式:

cloneSwiftLint,然后运行git submodule update --init --recursive; make install即可.

使用

Xcode

在Xcode里使用是推荐的做法,也是标准的方式.

在Xcode里集成SwiftLint的功能,只需新加一个Run Script Phase

if which swiftlint >/dev/null; then
  swiftlint
else
  echo "warning: SwiftLint not installed, download from https://github.com/realm/SwiftLint"
fi

sample

如果是使用CocoaPods的方式安装的话,把以上脚本更改为:

"${PODS_ROOT}/SwiftLint/swiftlint"

即可.

falstlane

SwiftLint提供了对fastlane的支持,添加:

swiftlint(
mode: :lint, # SwiftLint mode: :lint (default) or :autocorrect
executable: "Pods/SwiftLint/swiftlint", # The SwiftLint binary path (optional). Important if you've installed it via CocoaPods
output_file: "swiftlint.result.json", # The path of the output file (optional)
reporter: "json", # The custom reporter to use (optional)
config_file: ".swiftlint-ci.yml", # The path of the configuration file (optional)
ignore_exit_status: true # Allow fastlane to continue even if SwiftLint returns a non-zero exit status
)

命令行

输入swiftlint help

$ swiftlint help
Available commands:
   autocorrect  Automatically correct warnings and errors
   help         Display general or command-specific help
   lint         Print lint warnings and errors for the Swift files in the current directory (default command)
   rules        Display the list of rules and their identifiers
   version      Display the current version of SwiftLint

在包含.swift文件的文件夹运行swiftlint将会对文件夹进行遍历查找文件进行检查.

指定文件的swiftlint lint/swiftlint autocorrect,需加上option: --use-script-input-files并且设置以下环境变量:

SCRIPT_INPUT_FILE_COUNT

SCRIPT_INPUT_FILE_0, SCRIPT_INPUT_FILE_1SCRIPT_INPUT_FILE_{SCRIPT_INPUT_FILE_COUNT}.

关于设置input files的环境变量参考: custom Xcode script phases.

其他IDE

其他IDE的使用,可以查阅官方项目文档了解更多.

Swift语言多版本

SwiftLint是对SrouceKit的嵌入,所以对多个Swift语言的版本,只要SourceKit的版本一致,那么SwiftLint就能在拥有相同SourceKit的Swift语言版本下正常工作. 在运行SwiftLint时,需保证与编译代码的toolchain一致. 当Xcode有多个版本的toolchain时,可能会覆盖SwiftLint默认的toolchain,这是官方给出的SwiftLint查找使用toolchain的顺序:

  • $XCODE_DEFAULT_TOOLCHAIN_OVERRIDE
  • $TOOLCHAIN_DIR or $TOOLCHAINS
  • xcrun -find swift
  • /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain
  • /Applications/Xcode-beta.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain
  • ~/Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain
  • ~/Applications/Xcode-beta.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain

以倒置域名规则设置TOOLCHAINS环境变量来指定Swift toolchain的版本:

$ TOOLCHAINS=com.apple.dt.toolchain.Swift_2_3 swiftlint autocorrect

规则

SwiftLint拥有超过75条规则,社区也在不停的维护,个人开发者可以通过PR的形式将新的规则提交给SwiftLint.

关于规则的描述:Rules.md.

关于规则的实现:Source/SwiftLintFramework/Rules.

可选的规则

可选规则默认关闭,也就是说,可选的规则需要手动在配置文件里开启才能生效,什么情况下会设置为可选规则呢?

  • 可能会有很多误报的情况,比如empty_count.
  • 规则的检查太耗时.
  • 非普遍的情况,或者只应用于某些情况,比如force_unwrapping.

关闭规则

当在文件域内添加以下格式后,规则将被关闭:

// swiftlint:disable <rule1> [<rule2> <rule3>...]

在同一个文件域内,添加以下格式后,规则将被再次开启:

// swiftlint:enable <rule1> [<rule2> <rule3>...]

例如:

// swiftlint:disable colon
let noWarning :String = "" // No warning about colons immediately after variable names!
// swiftlint:enable colon
let hasWarning :String = "" // Warning generated about colons immediately after variable names

可以在规则关闭格式添加:previous:this以及:next将规则的关闭限制在前一行、当前行、后一行而不影响文件内其他行的规则判断.

例如:

// swiftlint:disable:next force_cast
let noWarning = NSNumber() as! Int
let hasWarning = NSNumber() as! Int
let noWarning2 = NSNumber() as! Int // swiftlint:disable:this force_cast
let noWarning3 = NSNumber() as! Int
// swiftlint:disable:previous force_cast

可通过运行swiftlint rules查看所有的规则信息.

规则配置

通过在运行SwiftLint的根目录添加.swiftlint.yml文件来配置规则信息,可配置的规则信息如下:

  • disabled_rules: 需要关闭的默认开启规则集合
  • opt_in_rules: 需要开启的非默认开启规则集合
  • whitelist_rules: 规则白名单,只有此处定义的规则才会开启,不能同时在以上两个集合里定义.
disabled_rules: # rule identifiers to exclude from running
- colon
- comma
- control_statement
opt_in_rules: # some rules are only opt-in
- empty_count
# Find all the available rules by running:
# swiftlint rules
included: # paths to include during linting. `--path` is ignored if present.
- Source
excluded: # paths to ignore during linting. Takes precedence over `included`.
- Carthage
- Pods
- Source/ExcludedFolder
- Source/ExcludedFile.swift

# configurable rules can be customized from this configuration file
# binary rules can set their severity level
force_cast: warning # implicitly
force_try:
severity: warning # explicitly
# rules that have both warning and error levels, can set just the warning level
# implicitly
line_length: 110
# they can set both implicitly with an array
type_body_length:
- 300 # warning
- 400 # error
# or they can set both explicitly
file_length:
warning: 500
error: 1200
# naming rules can set warnings/errors for min_length and max_length
# additionally they can set excluded names
type_name:
min_length: 4 # only warning
max_length: # warning and error
warning: 40
error: 50
excluded: iPhone # excluded via string
identifier_name:
min_length: # only min_length
error: 4 # only error
excluded: # excluded via string array
- id
- URL
- GlobalAPIKey
reporter: "xcode" # reporter type (xcode, json, csv, checkstyle, junit, html, emoji)

可以通过${SOME_VARIABLE}在配置文件里使用环境变量.

设置自定义规则

可通过在配置文件里添加以下内容设置基于正则的自定义规则:

ustom_rules:
pirates_beat_ninjas: # rule identifier
included: ".*\\.swift" # regex that defines paths to include during linting. optional.
excluded: ".*Test\\.swift" # regex that defines paths to exclude during linting. optional
name: "Pirates Beat Ninjas" # rule name. optional.
regex: "([n,N]inja)" # matching pattern
match_kinds: # SyntaxKinds to match. optional.
- comment
- identifier
message: "Pirates are better than ninjas." # violation message. optional.
severity: error # violation severity. optional.
no_hiding_in_strings:
regex: "([n,N]inja)"
match_kinds: string

输入如下:

output

可以通过设置一个或多个match_kinds来进行正则匹配,但是仅匹配以下列表总的语法类型:

  • argument
  • attribute.builtin
  • attribute.id
  • buildconfig.id
  • buildconfig.keyword
  • comment
  • comment.mark
  • comment.url
  • doccomment
  • doccomment.field
  • identifier
  • keyword
  • number
  • objectliteral
  • parameter
  • placeholder
  • string
  • string_interpolation_anchor
  • typeidentifier

规则嵌套

SwiftLint支持嵌套配置文件以提供规则检查的细粒度控制:

  • 在需要进行规则检查的文件夹添加额外的.swiftlint.yml文件.
  • 每个.swift文件将会优先使用在同一个文件夹里定义的.swiftlint.yml进行规则检查,当.swift文件所在文件夹没有定义.swiftlint.yml文件时,SwiftLint将会使用上级文件夹的.swiftlint.yml(如果存在的话),以此递推.
  • excludedincluded将会被忽略.

自动更正

SwiftLint可以自动更正错误的格式,自动更正会覆盖原有文件,所以自动更正是相对危险的操作,在进行自动更正的时候需要进行必要的备份操作. SwiftLint只有在手动运行swiftlint autocorrent命令的时候才会进行自定更正.

一些实践经验

在以往做项目的经验中,有一些实践的经验:

  • 标记为fileprivate或者private的类型、方法或者变量,以_开头,如:

    fileprivate var _myPrivateVar: _MyPrivateType
    private func _doSomePrivateThing() { }
  • 对于类型的定义,尽量使用嵌套定义,如:

    public class SomeClass { }
    extension SomeClass {
    public struct Subtype { }
    }
    // 对Subtype的引用:
    let subtype = SomeClass.Subtype()
    // 适用于类型推断的场景:
    let subtype = .Subtype()
    // 非嵌套定义:
    public struct SomclassSubtype { } // 看起来比较臃肿,而且不能发挥Swift的类型推断的优势.

    嵌套定义使得类型的定义有明确的依赖结构,这样的话使逻辑更加清晰和明确.

  • 对类型的引用,尽量使用类型推断,这样可以使代码的意图更加明确和清晰:

    public enum OptionType {
    case some, all
    }
    public struct SomeOption {
    let option: OptionType
    }
    extension SomeOption {
    public static func option(_ op: OptionType) { return SomeOption(op) }
    }
    public func doSomethingWith(_ someOption: SomeOption) { }
    // 不使用类型推断:
    doSomethingWith(SomeOption(OptionType.some))
    // 使用类型推断:
    doSomethingWith(.option(.some))

参考:SwiftLint README.md