Skip to content

Latest commit

 

History

History
587 lines (461 loc) · 26.7 KB

swift3apiguideline.md

File metadata and controls

587 lines (461 loc) · 26.7 KB

翻译 By 幻想小肉丸

基本原则

  • 清晰易懂代码编写 是首要的目标。像方法,属性这些只会被申明一次但往往要被重复使用。应该设计的清晰易懂。

  • 清晰易懂比精简更重要 尽管Swift的代码可以写的很紧凑,用最少的字符写出最短的代码并不符合我们的期望。Swift里出现的紧凑的代码只不过是强类型语言的副作用和减少编写样板文件而生成的一些特性。

  • 给每个声明编写注释文档 靠直觉来猜测代码意思是相当不靠谱的行为。

    如果你无法用简单说明来表述自己API功能,那么你很可能设计一个错误的API

    • 使用Swift Markdown 语法

    • 从概要说明开始 好的设计往往能通过API名称和概要说明就能完全理解API的功能

      /// Returns a "view" of `self` containing the same elements in
      /// reverse order.
      func reversed() -> ReverseCollection
    • 重视概要说明 概要说明是非常重要的部分。杰出的文档注释始终贯彻优秀概要胜于一切的理念

    • 使用句子片段描述 切记不要用完整句子。如果可以每句话结束加个句号(好像对中文没啥鸟用

    • 描述清楚函数的功能和他的返回值 省略返回null或者void的描述

      /// Inserts `newHead` at the beginning of `self`.
      mutating func prepend(newHead: Int)  
      /// Returns a `List` containing `head` followed by the elements
      /// of `self`.
      func prepending(head: Element) -> List
      /// Removes and returns the first element of `self` if non-empty;
      /// returns `nil` otherwise.
      mutating func popFirst() -> Element?
    • 说明下标访问内容

      /// Accesses the `index`th element.
      subscript(index: Int) -> Element { get set }
    • 描写构造函数构造的内容

      /// Creates an instance containing `n` repetitions of `x`.
      init(count n: Int, repeatedElement x: Element)
    • 对于其他的声明,要描述清楚它到底是干什么的

      /// A collection that supports equally efficient insertion/removal
      /// at any position.
      struct List {
      
        /// The element at the beginning of `self`, or `nil` if self is
        /// empty.
        var first: Element?
        ...
      
    • 必要时可以续写更多段落和项目列 每个段落用空行隔开

      /// Writes the textual representation of each    ← 概述
      /// element of `items` to the standard output.
      ///                                              ← 空行
      /// The textual representation for each item `x` ← 附加讨论
      /// is generated by the expression `String(x)`.
      ///
      /// - Parameter separator: text to be printed    ⎫
      ///   between items.                             ⎟
      /// - Parameter terminator: text to be printed   ⎬  参数
      ///   at the end.                                ⎟
      ///                                              ⎭
      /// - Note: To print without a trailing          ⎫
      ///   newline, pass `terminator: ""`             ⎟
      ///                                              ⎬ 其他
      /// - SeeAlso: `CustomDebugStringConvertible`,   ⎟
      ///   `CustomStringConvertible`, `debugPrint`.   ⎭
      public func print(
        items: Any..., separator: String = " ", terminator: String = "\n")
    • 使用关键字来标记内容

    Xcode支持下列关键字来标记

Attention Author Authors Bug
Complexity Copyright Date Experiment
Important Invariant Note Parameter
Parameters Postcondition Precondition Remark
Requires Returns SeeAlso Since
Throws Todo Version Warning

命名

尽可能描述清楚用法

  • 命名需要包含所有的需要的词语避免他人阅读代码时候看到该命名产生歧义

    举个例子,考虑下下面代码,从一个集合中移除特定位置的元素

     extension List {
     	public mutating func remove(at position: Index) -> Element
     }
     employees.remove(at: x)

    如果从函数申明中删除介词at,很可能是阅读代码的人认为这个函数是删除元素x,而不是删除处于x位置的元素

     employees.remove(x) // unclear:我们到底是要删除元素x还是第x个元素?
  • 删除不必要的单词 在命名里用的每个单词都必须传达重要的使用信息

    再举个栗子 要用一些单词来表述意图和避免歧义,但是要删除那些读者已经了解的信息,特别是那些仅仅是重复参数类型的单词

     public mutating func removeElement(_ member: Element) -> Element?
    
     allViews.removeElement(cancelButton)

    像上面的代码 单词Element对于使用函数的完全没有提供任何有用的信息.命名是不是应该想下面这样更好:

     public mutating func remove(_ member: Element) -> Element?
    
     allViews.remove(cancelButton) // clearer

    极个别情况下,重复参数类型是为了避免歧义,但是通常来说用词语来描述参数的*用途(role)*比描述参数的类型更好,会在接下来的章节具体描述。

  • **根据其用途(role)命名变量,参数,关联类型(associatedtype)**而不是他们本身类型

    举个栗子

     var string = "Hello"
     protocol ViewController {
     	associatedtype ViewType : View
     }
     class ProductionLine {
     	func restock(from widgetFactory: WidgetFactory)
     }

    像上面代码那样无法使表达丰富和清晰,还不如像下面代码一样选择一个能表示其用途的命名。

     var greeting = "Hello"
     protocol ViewController {
     	associatedtype ContentView : View
     }
     class ProductionLine {
     	func restock(from supplier: WidgetFactory)
     }

    如果关联类型(associatedtype)和协议紧紧相关,协议又是按其照角色命名,为了避免冲突可以在关联类型后面添加Type一词,像下面代码

     protocol Sequence {
     	associatedtype IteratorType : Iterator
     }
  • 补充泛类型(weak type)信息以便清晰传递其用途

    当参数类型是NSObject,Any,AnyObject或者其他基础类型如Int,String等。这些类型的信息可能无法完全表达使用意图的。举个栗子,像下面代码这样,add方法还是申明还是比较清晰的,可是在使用的时候会让人模糊不清。

     func add(_ observer: NSObject, for keyPath: String)
    
     grid.add(self, for: graphics) // vague

    为了表述清晰,可以在泛类型的参数前面(即参数标签或函数名)添加一个名词来描述参数的用途

     func addObserver(_ observer: NSObject, forKeyPath path: String)
     grid.addObserver(self, forKeyPath: graphics) // clear
     ``
     

争取使用顺畅(Fluent Usage)

  • 每一个方法和函数调用时候,如同符合英语语法的短语

    好的命名

     x.insert(y, at: z)          “x, insert y at z”
     x.subViews(havingColor: y)  “x's subviews having color y”
     x.capitalizingNouns()       “x, capitalizing nouns”

    差的命名

     x.insert(y, position: z)
     x.subViews(color: y)
     x.nounCapitalize()

    为了保证通顺,降低非关键性的第一个参数或前2个参数的命名表述,是被允许的。

    如下所示, description是个枚举值,核心是后2个参数,小肉丸注

    AudioUnit.instantiate( with: description, options: [.inProcess], completionHandler: stopProgressBar)

  • 工厂方法前面添加makex.makeIterator()

  • 构造函数或者工厂方法调用时要像个短语,短语不要包含第一个参数,例如x.makeWidget(cogCount: 47)

    举个栗子,看看下面的用法就对该要求一目了然了.

     let foreground = Color(red: 32, green: 64, blue: 128)
     let newPart = factory.makeWidget(gears: 42, spindles: 14)

    有些API作者想让函数看起来像连贯的语法会写出下列的函数,这样不好

     let foreground = Color(havingRGBValuesRed: 32, green: 64, andBlue: 128)
     let newPart = factory.makeWidget(havingGearCount: 42, andSpindleCount: 14)

    除非该函数是类型转换,不然都需要给第一个参数添加参数标签

    类型转化函数把cmy色彩转换为rgb

     let rgbForeground = RGBColor(cmykForeground)
  • 命名函数的时候需要根据函数的副作用而定(原文叫side-effects,大概是指会作用于参数对象的一些函数,小肉丸注)

    • 没有副作用的函数看起来要想名词短语,例如 x.distance(to: y), i.successor().

    • 具有副作用的函数看起来要像动词短语,例如print(x), x.sort(), x.append(y).

    • 坚持成对的命名可变/不可变函数(Mutating/nonmutating)。可变的函数通常会伴有相同语义的不可变的版本(nomutating variant),不可变的方法返回一个新值而不是改变自身.

      • 当一个方法是以动词命名的时候,使用动词的祈使形态来做完可变方法的名称,用动词的过去分词(ed)或者进行时(ing)作为不可变方法的命名

        Mutating Nonmutating
        x.sort() z = x.sorted()
        x.append(y) z = x.appending(y)
      • 使用动词的过去分词命名一个不可变的方法

         /// Reverses `self` in-place.
         mutating func reverse()
        
         /// Returns a reversed copy of `self`.
         func reversed() -> Self
         ...
         x.reverse()
         let y = x.reversed()
      • 如果动词后面带有对象,使用动词的进行时命名不可变方法

         /// Strips all the newlines from `self`
         mutating func stripNewlines()
        
         /// Returns a copy of `self` with all the newlines 			stripped.
         func strippingNewlines() -> String
         ...
         s.stripNewlines()
         let oneLine = t.strippingNewlines()
      • 如果一个方法是以名词命名的,对于可变方法直接使用名词,不可变方法在名词前面添加单词form

        Mutating Nonmutating
        x.formUnion(z) j = x.union(z)
        x.formSuccessor(y) z = x.successor(y)
      • 当命名命名不可变布尔类型的不可变((nomutating))方法或者属性的时候,方法或者属性要以断言的形式命名.如x.isEmpty line1.intersects(line2)

      • 一个描述对象的协议要命名成名词.如Collection

      • 一个描述某种能力的协议要在命名后加上able,ible,或者 ing.如Equatable ProgressReporting

      • 属性,类型,常量以及变量要以名词命名

###正确使用专业术语

专业术语(Terms of Art):名词,在某一领域拥有指定和特殊意思的单词或者短语.

  • 避免使用复杂的术语越通俗易懂的词语越好表达意思.如果医生能满足需求就不要使用郎中.(原文Don’t say “epidermis” if “skin” will serve your purpose.)专业术语是一种必须的交流工具,但是仅仅用于表达至关重要的意思,否则会让人迷失.

  • 如果使用专业术语要保证术语原有的意思

    使用专业术语的唯一理由就是它能恰到好处的表达出一个不明确或者有歧义的东西,所以API设计中要严格依照其原意使用术语

    • 不要让老司机震惊那些老司机可能会被我们创造出来的新意思感到震惊甚至愤怒
    • 不要迷惑菜鸟菜鸟只会谷歌出这个术语原有意思
  • 不要使用缩略词缩略词,特别是那种无标准的缩略词,实际上就是专业术语.理解缩略词基本上靠你能不能把它还原成未缩略前的形式.

    这意味着你所使用的缩略词时,需要保证该词能弄通过谷歌找到其意思

  • **不要打破常规(Embrace precedent)**不要为了新手而去破坏已经形成的文化内容.

    数组对于是个一连串相邻的数据结构来说是个好名字,但是有些简单词语比如列表可能对新手理解起来更简单.数组是在现代计算机中是个基本术语.每个程序猿都应该知道或者将要知道数组是什么.新手可以通过自己谷歌来获取数组的含义.

    在编程中的某个领域里,如数学,用一些常用术语像用sin(x)verticalPositionOnUnitCircleAtOriginOfEndOfRadiusWithAngle(x)更好的表达求正弦值得意思.尽管这个点有悖之前说的不要使用缩略词这一点,sin是sine的缩略词,但是sin在编程中已经用了数十年,在数学上了更已经用数百年了.

##约定 ###通常约定

  • 任何时间复杂度超过o(1)(o(1)即是常数时间)的复杂计算需要编写文档人们通常假设对属性的访问不会产生任何运算开销,因为他们已经下意识的使用了存储属性.请在这种假设不成立的时候提醒使用者

  • 宁可使用类方法或者属性也不是使用普通函数(free function) 普通函数只有在一些特殊情况下使用

    当该函数没有明显的self时候:

      ```swift
      min(x, y, z)
      ```
    

    当该函数属于无约束通用行:

      ```swift
      print(x)
      ```
    

    当函数是一个明确领域的符号:

      ```swift
      sin(x)
      ```
    
  • 遵守大小写约定 类型名和协议用UpperCamelCase其他都用lowerCamelCase

    尽管缩略词或者简称在美式英语中都是用大写字母,但是在代码里这些缩略词也要符合大小写约定

     var utf8Bytes: [UTF8.CodeUnit]
     var isRepresentableAsASCII = true
     var userSMTPServer: SecureSMTPServer

    简称用起来和普通单词一样

     var radarDetector: RadarScanner
     var enjoysScubaDiving = true
  • 当方法都表示一个相似的功能或者使用在不同的地方的时候方法可以重名

    举个栗子,下面代码定义多个相同名字的方法是没问题的

     extension Shape {
       	/// Returns `true` iff `other` is within the area of `self`.
       	func contains(_ other: Point) -> Bool { ... }
     
       	/// Returns `true` iff `other` is entirely within the area of 	`self`.
       	func contains(_ other: Shape) -> Bool { ... }
     
       	/// Returns `true` iff `other` is within the area of `self`.
       	func contains(_ other: LineSegment) -> Bool { ... }
     }

    几何图形类和集合类的方法重名也是没问题的,它们工作在不同的地方

     extension Collection where Element : Equatable {
       /// Returns `true` iff `self` contains an element equal to
       /// `sought`.
       func contains(_ sought: Element) -> Bool { ... }
     }

    下面的代码中index尽管有着不同的意思,但是应该改用不同方法名(两个方法的功能不同,小肉丸注)

     	extension Database {
       /// Rebuilds the database's search index
       func index() { ... }
     
       /// Returns the `n`th row in the given table.
       func index(_ n: Int, inTable: TableID) -> TableRow { ... }
     }

    最后再说一句,不要仅仅重载方法返回值,这样会在推导函数类型产生歧义.

##参数

func move(from start: Point, to end: Point)
  • 为文档选择合适的参数名尽管参数名在调用的时候看不到,但是参数还是有着解释性的用途

    选择合适的的参数名字可以让文档更易阅读,下面的代码中的合适参数名就使得文档阅读更加自然

     /// Return an `Array` containing the elements of `self`
     /// that satisfy `predicate`.
     func filter(_ predicate: (Element) -> Bool) -> [Generator.Element]
    
     /// Replace the given `subRange` of elements with `newElements`.
     mutating func replaceRange(_ subRange: Range, with newElements: [E])

    不好的名字会让文档看起来不通顺,读起来郁闷

     /// Return an `Array` containing the elements of `self`
     /// that satisfy `includedInResult`.
     func filter(_ includedInResult: (Element) -> Bool) -> 	[Generator.Element]
    
     /// Replace the range of elements indicated by `r` with
     /// the contents of `with`.
     mutating func replaceRange(_ r: Range, with: [E])
  • 当方法经常被调用的时候就要发挥默认参数的优势.为每一个参数提供一个常用的默认候选值

    默认参数通过隐藏无关的信息能提高可阅读性,像下面这样调用就很不好看到懂:

     let order = lastName.compare(
     royalFamilyName, options: [], range: nil, locale: nil)

    可以修改下,调用就简单多了:

    let order = lastName.compare(royalFamilyName)

    默认参数比一组方法来的更友好,对于任何人来说这样会降低学习这些API的负担

    extension String {
        /// ...description...
         public func compare(
         _ other: String, options: CompareOptions = [],
         range: Range? = nil, locale: Locale? = nil
         ) -> Ordering
    }

    尽管上面的方法不是很简单,但是总比下面的好吧

    extension String {
         /// ...description 1...
         public func compare(_ other: String) -> Ordering
         /// ...description 2...
         public func compare(_ other: String, options: CompareOptions) -> Ordering
         /// ...description 3...
         public func compare(
         _ other: String, options: CompareOptions, range: Range) -> Ordering
         /// ...description 4...
         public func compare(
         _ other: String, options: StringCompareOptions,
         range: Range, locale: Locale) -> Ordering
    }

    在这一堆的函数里,每个函数需要有各自的文档描述并且需要程序猿分别理解使用.为了决定使用哪个函数,需要了解每一个函数,有时候还会让人惊讶这些方法之间的关系.比如:foo(bar:nil)foo()他们不总是同样效果(原文是aren’t always synonyms),需要一个枯燥的过程去在相近的文档中寻找他们的不同.使用带有默认参数的方法可以大大提高编程体验. 把默认参数放到参数列表末尾较好没有默认值得参数对于函数名的语意更为重要并且也能在函数调用的时候提供一个稳定的初始使用模式.

##参数标签

func move(from start: Point, to end: Point)

x.move(from: x, to: y)

  • **当参数无法被有效区分的时候隐藏所有参数标签.**比如min(number1, number2), zip(sequence1, sequence2).

  • 做类型转换的构造函数隐藏第一个参数标签.

    第一个参数永远都是类型转换的原始值

    extension String {
      // Convert `x` into its textual representation in the given radix
      init(_ x: BigInt, radix: Int = 10)   ← Note the initial underscore
    }
    
    text = "The value is: "
    text += String(veryLargeNumber)
    text += " and in hexadecimal, it's"
    text += String(veryLargeNumber, radix: 16)
  • 在收窄的转换下(In “narrowing” type conversions). 建议用函数标签标注下

    extension UInt32 {
      /// Creates an instance having the specified `value`.
      init(_ value: Int16)             Widening, so no label
      /// Creates an instance having the lowest 32 bits of `source`.
      init(truncating source: UInt64)
      /// Creates an instance having the nearest representable
      /// approximation of `valueToApproximate`.
      init(saturating valueToApproximate: UInt64)
    }

    值进行安全类型(preserving type conversion )转换时单向的,换句话说,起始值的不同都会导致转换结果的不同.比如int8转换到int64就是安全的类型转换,因为任何一个int8类型的值都可以转换成int64的值.但是反向转换就不是了.int64有很多值是超出int8的范围的.

  • 当第一个参数是介词短语的一部分的时候,需要给它添加参数标签.参数标签应该以介词开头比如x.removeBoxes(havingLength: 12).

    当前几个参数都是同一个抽象对象中的一部分,很可能我们会命名错误.像下面这个样命名:

    a.move(toX: b, y: c)
    a.fade(fromRed: b, green: c, blue: d)

    应该把参数标签中的介词提前到函数名中,这样让参数中的抽象对象更加清晰. 该条准则要求当参数不是语法的一部分的时候也应该添加参数标签如下:

    view.dismiss(animated: false)
    let text = words.split(maxSplits: 12)
    let studentsByName = students.sorted(isOrderedBefore: Student.namePrecedes)

    请记住,让函数命名应该能准确表达函数及其参数的意思,这点很重要.下面的命名符合语法,但是会表达出错误的意思.

    view.dismiss(false)   Don't dismiss? Dismiss a Bool?
    words.split(12)       Split the number 12?

    带有默认值的参数是可以被隐藏的并且它们名字也不会是自然语法中的一部分,所以它们必须要有参数标签.

  • 给没提及参数类型也都加上参数标签

###特殊语法

  • 在你的API请注释清楚那些出现的作为参数的闭包(closure parameters)和元祖中的成员
   /// Ensure that we hold uniquely-referenced storage for at least
   /// `requestedCapacity` elements.
   ///
   /// If more storage is needed, `allocate` is called with
   /// `byteCount` equal to the number of maximally-aligned
   /// bytes to allocate.
   ///
   /// - Returns:
   ///   - reallocated: `true` iff a new block of memory
   ///     was allocated.
   ///   - capacityChanged: `true` iff `capacity` was updated.
   mutating func ensureUniqueStorage(
     minimumCapacity requestedCapacity: Int, 
     allocate: (byteCount: Int) -> UnsafePointer<Void>
   ) -> (reallocated: Bool, capacityChanged: Bool)
>尽管已经闭包使用了明确的参数标签,但是你也应该把这些闭包的参数像函数参数一样标注在文档上.闭包函数在函数体中调用的时候读起来也应该和函数一样--从一个不含第一参数的基本名字开始的短语.
   /// Ensure that we hold uniquely-referenced storage for at least
   /// `requestedCapacity` elements.
   ///
   /// If more storage is needed, `allocate` is called with
   /// `byteCount` equal to the number of maximally-aligned
   /// bytes to allocate.
   ///
   /// - Returns:
   ///   - reallocated: `true` iff a new block of memory
   ///     was allocated.
   ///   - capacityChanged: `true` iff `capacity` was updated.
   mutating func ensureUniqueStorage(
     minimumCapacity requestedCapacity: Int, 
     allocate: (byteCount: Int) -> UnsafePointer<Void>
   ) -> (reallocated: Bool, capacityChanged: Bool)
   ```    
-**要特别小心多态类型**(比如`Any``AnyObject`以及其他的通用类型)以免造成模糊的函数重载版本.
   >举个例子,详细这样重载版本会如何
   
   ```swift
   struct Array {
    /// Inserts `newElement` at `self.endIndex`.
    public mutating func append(_ newElement: Element)

    /// Inserts the contents of `newElements`, in order, at
    /// `self.endIndex`.
    public mutating func append(_ newElements: S)
    where S.Generator.Element == Element
   }
   ```
   > 这两个方法是类似功能的方法,仅仅是参数类型不同.`Element`是`Any`类型的时候,传入的参数也就可能是数组类型的了.
   
   ```swift
   var values: [Any] = [1, "a"]
   values.append([2, 3, 4]) // [1, "a", [2, 3, 4]] or [1, "a", 2, 3, 4]?
   ```
   
   >为了消除模糊,需要把第二函数命名的更明确些
   
   ```swift
   struct Array {
    /// Inserts `newElement` at `self.endIndex`.
     public mutating func append(_ newElement: Element)

     /// Inserts the contents of `newElements`, in order, at
    /// `self.endIndex`.
     public mutating func append(contentsOf newElements: S)
        where S.Generator.Element == Element
   }
   ```
   > 记住,新的函数命名更贴近文档注释.在这个例子里文档,注释文档的编写确实要引起API作者的注意.