原文鏈接:https://swift.org/documentation/api-design-guidelines/
基本原則
代碼明晰是你主要目標(biāo)。實體諸如方法和屬性,一次聲明,往往會被使用多次。故設(shè)計APIs時盡量使之清晰并且簡練。評估某個API設(shè)計是否合理,單從閱讀其聲明并不足以下結(jié)論,往往需要在真實示例下,才能確保它在上下文中是清晰正確的。
明晰優(yōu)先于簡練。盡管Swift代碼可以書寫的非常緊湊簡練,但實現(xiàn)最少代碼量并非是我們的目標(biāo)。Swift代碼的簡練,只是
強(qiáng)類型系統(tǒng)和自然降低功能樣板產(chǎn)生的附加效果而已。-
為每個聲明書寫文檔注釋。通過書寫文檔獲得的經(jīng)驗見解會對你的設(shè)計產(chǎn)生深遠(yuǎn)的影響,所以不要忽視之。
如果你無法使用簡潔的術(shù)語描述你的APIs功能,那么你可能設(shè)計了錯誤的APIs。
詳情
使用Swift的Markdown語法。
-
以摘要開始,描述聲明實體的功能。一般的,通過其聲明和摘要信息,API會被人清晰的理解。
/// Returns `self`的"view"逆向集合。即包含相同元素,但順序相反。 func reversed() -> ReverseCollection專注于摘要:這是最為重要的部分。很多優(yōu)秀的文檔注釋只包含一個優(yōu)質(zhì)的摘要,別無它物。
盡可能使用單句片段,并以句點(diǎn)結(jié)束。不要使用完整冗長的句子。
-
描述方法或函數(shù)的功能和返回值,忽略無意義的null功能和Viod返回:
/// 在
self起始處插入newHead。
mutating func prepend(_ newHead: Int)
/// 返回一個包含head并由self跟隨的列表。func prepending(_ head: Element) -> List
/// 若self非空,刪除并返回第一個元素;
/// 否則返回nil。
mutating func popFirst() -> Element?
```
提示:在極少數(shù)情況下,如上面的popFirst,摘要是由分號分隔的多個句子片段組成。-
下標(biāo)注釋:即描述下標(biāo)訪問的內(nèi)容:
/// 訪問下標(biāo)為 第`index`個的元素。 subscript(index: Int) -> Element { get set } -
構(gòu)造方法:即描述初始化方法創(chuàng)建的內(nèi)容:
/// 創(chuàng)建實例:該實例中包含n個`x`。 init(count n: Int, repeatedElement x: Element) -
其它聲明類場景,所聲明的實體務(wù)必描述清晰。
/// 集合對象:在任何位置均支持同等高效的插入、刪除操作。 struct List { /// `self`非空,`self`的首個元素; ///否則,返回`nil`。 var first: Element? ...
-
(可選),連續(xù)使用一個或多個段落和項目符號項。段落用空行分隔并使用完整的句子。
/// 將`items`中每個元素的文本表示,執(zhí)行標(biāo)準(zhǔn)輸出。 ← 摘要 /// ← 空行 /// 每個元素`x`的文本表示均由`String(x)`表達(dá)式生成 ← 補(bǔ)充說明 /// /// - 參數(shù) separator: 元素之間的打印文本。 ? /// - 參數(shù) terminator: 結(jié)尾打印的文本 ? 參數(shù)部分 /// ? /// - 備注: 若不要在結(jié)尾新起一行, ///則置`terminator: ""`即可。 ? ///- 其它關(guān)聯(lián): `CustomDebugStringConvertible`, ? 命令符號 /// `CustomStringConvertible`, `debugPrint`. ? public func print( _ items: Any..., separator: String = " ", terminator: String = "\n")- 在適當(dāng)場景,在摘要之外使用可識別的符號文檔標(biāo)記元素添加信息。
- 使用符號命令語法,使用已識別的項目符號。
流行的IDE工具(如Xcode)對以下關(guān)鍵字開頭的項目符號進(jìn)行了特殊的處理:
|關(guān)鍵字|||
|:--:|:--:|:--:|:--:|
|Attention|Author|Authors|Bug|
|Complexity|Copyright|Date|Experiment|
|Important|Invariant|Note|Parameter|
|Parameters|Postcondition|Precondition|Remark|
|Requires|Returns|SeeAlso|Since|
|Throws|ToDo|Version|Warning|
命名
提高明晰方法
-
為了便于閱讀代碼,在進(jìn)行命名時,要涵蓋所有必須的單詞,以避免歧義。
例如,一個方法:在集合中刪除給定位置的元素。
code1:? extension List { public mutating func remove(at position: Index) -> Element } employees.remove(at: x)如果我們從方法簽名中省略單詞
at,則可能使讀者認(rèn)為該方法是用于搜索并刪除等于x的元素,而不是使用x來指示要刪除的元素的位置。code2:? employees.remove(x) // 不清晰:是刪除x嗎? -
務(wù)必忽略不必須的單詞。命名中的每個單詞都應(yīng)在使用場景中傳達(dá)重要的信息。
有時需要更多的單詞來闡明意圖或消除歧義,但應(yīng)省略那些眾所周知的冗余詞。特別是要省略那些僅為重復(fù)類型信息的單詞。
code:? public mutating func removeElement(_ member: Element) -> Element? allViews.removeElement(cancelButton)在該示例中,
Element在調(diào)用場景中沒有傳達(dá)任何重要信息,故該API可優(yōu)化為:code:? public mutating func remove(_ member: Element) -> Element? allViews.remove(cancelButton) // clearer有時,重復(fù)類型信息對于避免歧義是有必要的。但通常而言,最好使用描述參數(shù)角色而不是其類型的單詞。有關(guān)詳細(xì)信息,請參閱下一項。
-
根據(jù)角色來命名變量,參數(shù)和關(guān)聯(lián)類型,而不是根據(jù)類型約束。
code:? var string = "Hello" protocol ViewController { associatedtype ViewType : View } class ProductionLine { func restock(from widgetFactory: WidgetFactory) }以這種方式重新定位類型名稱無法優(yōu)化清晰度和表現(xiàn)力。相反,努力選擇一個表達(dá)實體角色的名稱反而更好。
code: ? var greeting = "Hello" protocol ViewController { associatedtype ContentView : View } class ProductionLine { func restock(from supplier: WidgetFactory) }如果關(guān)聯(lián)類型與其協(xié)議約束緊密綁定以使協(xié)議名稱為角色,請通過將
Protocol附加到協(xié)議名稱來避免沖突:protocol Sequence { associatedtype Iterator : IteratorProtocol } protocol IteratorProtocol { ... } -
補(bǔ)償弱類型信息以闡明參數(shù)的作用。
尤其當(dāng)參數(shù)類型是
NSObject,Any,AnyObject或諸如Int或String的基本類型時,類型信息和在使用處的上下文可能無法完全傳達(dá)意圖。 在此示例中,聲明可能是明確的,但使用點(diǎn)是模糊的:code:? func add(_ observer: NSObject, for keyPath: String) grid.add(self, for: graphics) // 模糊為了恢復(fù)清晰度,在每個弱類型參數(shù)前面加上描述其角色的名詞:
code:? func addObserver(_ observer: NSObject, forKeyPath path: String) grid.addObserver(self, forKeyPath: graphics) // 清晰
力求流暢使用
-
使用標(biāo)準(zhǔn)英語語法規(guī)則來命名方法和函數(shù)。
code: ? 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”code: ? x.insert(y, position: z) x.subViews(color: y) x.nounCapitalize()在第一個或第二個參數(shù)之后,當(dāng)這些參數(shù)不是調(diào)用方法的核心時,流利性降級是可以接受的。即如果不影響方法要表達(dá)的含義,那可以簡化第一個或者前兩個參數(shù),這樣使用起來更加流暢。
AudioUnit.instantiate( with: description, options: [.inProcess], completionHandler: stopProgressBar) 用
make前綴開始工廠方法的名稱命名。如 x.makeIterator()。-
構(gòu)造方法和工廠方法調(diào)用的第一個參數(shù)不應(yīng)該形成以基本名稱開頭的短語。如 x.makeWidget(cogCount: 47)。
例如,這些調(diào)用方法的第一個參數(shù)不會作為與基本名稱相同的短語的一部分讀?。?/p>
code: ? let foreground = Color(red: 32, green: 64, blue: 128) let newPart = factory.makeWidget(gears: 42, spindles: 14) let ref = Link(target: destination)在以下示例中,API作者嘗試使用第一個參數(shù)創(chuàng)建語法連續(xù)性:
code: ? let foreground = Color(havingRGBValuesRed: 32, green: 64, andBlue: 128) let newPart = factory.makeWidget(havingGearCount: 42, andSpindleCount: 14) let ref = Link(to: destination)實際上,此準(zhǔn)則以及參數(shù)標(biāo)簽的準(zhǔn)則意味著第一個參數(shù)將具有標(biāo)簽,除非調(diào)用正在執(zhí)行值保留類型轉(zhuǎn)換。
let rgbForeground = RGBColor(cmykForeground) -
根據(jù)副作用命名函數(shù)和方法。
那些沒有副作用的函數(shù)和方法應(yīng)該讀作是一個名詞詞組。如
x.distance(to: y),i.successor().那些有副作用的函數(shù)和方法應(yīng)該讀作是一個命令式的動詞短語。如
print(x),x.sort(),x.append(y)。-
命名名稱一致的Mutating/nonmutating方法對。變異方法通常具有一個類似語義的非突變變體,但返回新值而不是就地更新實例。
- 當(dāng)操作方法由動詞自然描述時,使用動詞對變異方法進(jìn)行命名,而應(yīng)用“ed”或“ing”后綴來命名對應(yīng)的其非變異方法。
Mutating Nonmutating x.sort() z = x.sorted() x.append(y) z = x.appending(y)
* 更傾向于使用[動詞的過去分詞](https://en.wikipedia.org/wiki/Participle)命名非變異變體(通常附加`ed`)
```
/// 即刻逆向 `self`。
mutating func reverse()
/// 返回self的逆向拷貝。
func reversed() -> Self
...
x.reverse()
let y = x.reversed()
```
* 當(dāng)添加`ed`不具有語法性,因為動詞具有直接對象時,使用動詞的當(dāng)前分詞命名非變異變體,通過附加“ing”。(應(yīng)為語法問題)
```
/// 過濾掉self中空行
mutating func stripNewlines()
/// 返回self的拷貝,該拷貝過濾掉了所有的空行。
func strippingNewlines() -> String
...
s.stripNewlines()
let oneLine = t.strippingNewlines()
```
* 當(dāng)操作方法由名詞描述時,使用名詞作為非突變方法。并使用`form`前綴來命名其對應(yīng)的變異方法。
| Nonmutating | Mutating |
|:--:|:--:|
|x = y.union(z)| y.formUnion(z)|
|j = c.successor(i)|c.formSuccessor(&i)|
- 當(dāng)使用非突變方法時,布爾方法和屬性的使用在接收者看來,應(yīng)為斷言的形式。如:
x.isEmpty,line1.intersects(line2). - 描述類的協(xié)議應(yīng)該以名詞命名。如
Collection。 - 功能類的協(xié)議應(yīng)以后綴為
able,ible或ing的單詞命名。如Equatable,ProgressReporting。 - 其它類型,諸如屬性、變量、常量應(yīng)以名詞來命名。
更好的使用術(shù)語
名詞 - 在特定領(lǐng)域或?qū)I(yè)中具有精確、專門意義的詞或短語。
如果一個更常見的詞語同樣傳達(dá)了相同意義,則避免使用模糊術(shù)語 。如果
皮膚能夠闡述您的目的,請不要說表皮。藝術(shù)品是一種必不可少的溝通工具,但只應(yīng)用于捕捉原本會丟失的重要意義。-
如果您使用藝術(shù)術(shù)語,請堅持使用它既定的意義。
使用技術(shù)術(shù)語而不是更常見的單詞,其唯一原因是它可以更精確地表達(dá)一些本來會模棱兩可或不清楚的東西。因此,API應(yīng)嚴(yán)格使用術(shù)語。
- 勿使專家驚訝。如果我們?yōu)楸娝苤男g(shù)語發(fā)明了新的含義,任何已經(jīng)熟悉它的人都會感到驚訝或憤怒。
- 不要迷惑初學(xué)者:任何試圖學(xué)習(xí)該術(shù)語的人都可能會進(jìn)行網(wǎng)絡(luò)搜索并找到其傳統(tǒng)的原始意義。
-
避免使用縮寫??s寫,尤其是非標(biāo)準(zhǔn)的縮寫,實際上是術(shù)語,因為理解依賴于正確地將它們翻譯成非縮寫形式。
您使用的任何縮寫的含義,都可在網(wǎng)絡(luò)找到。即眾所周知的含義。
-
擁抱先例。不要以犧牲與現(xiàn)有文化的一致性為代價來為初學(xué)者優(yōu)化術(shù)語。
命名連續(xù)數(shù)據(jù)結(jié)構(gòu)為
Array比使用簡單術(shù)語(如List)更好,即使初學(xué)者可能更容易理解List的含義。Array是現(xiàn)代計算的基礎(chǔ),因此每個程序員都應(yīng)該知道 - 或者很快就會學(xué)到 -Array的概念。 使用大多數(shù)程序員都熟悉的術(shù)語,他們的問題搜索將很快得到解決。在特定的編程域中,例如數(shù)學(xué),諸如sin(x)之類的廣泛使用的術(shù)語,要優(yōu)于諸如
verticalPositionOnUnitCircleAtOriginOfEndOfRadiusWithAngle(x)的解釋性短語。 請注意,在這種情況下,先例超過了指南中避免縮寫的規(guī)定:雖然完整的單詞是sine,但是sin(x)在程序員中已經(jīng)普遍使用了幾十年,并且在幾個世紀(jì)的數(shù)學(xué)家中也是如此。
代碼規(guī)范
通用規(guī)范
任何復(fù)雜度不是O(1)的計算屬性,均要注釋。人們通常認(rèn)為屬性訪問不涉及重要的計算,因為他們在心理上將屬性作為存儲類型了。當(dāng)一反常態(tài)時,需要備注提醒。
-
首選方法和屬性實現(xiàn)而非函數(shù)。自由函數(shù)僅在以下特例中使用:
-
無顯式self:
min(x,y,z)
-
函數(shù)是無約束的泛型時:
print(x)
-
函數(shù)語法是已建立的域表示法的一部分時:
sin(x)
-
-
命名規(guī)范:類型和協(xié)議的命名遵循大駝峰命名法,其他一切都遵循小駝峰命名法。
縮略語和首字母縮略詞:通常在美式英語中顯示為大寫.應(yīng)根據(jù)拼寫規(guī)范統(tǒng)一大寫或小寫。
var utf8Bytes: [UTF8.CodeUnit] var isRepresentableAsASCII = true var userSMTPServer: SecureSMTPServer其他首字母縮略詞應(yīng)視為普通詞:
var radarDetector: RadarScanner var enjoysScubaDiving = true -
當(dāng)方法具有相同的基本含義或在不同的域中操作時,方法可以共用基本名稱。
例如,以下方案值得推薦,因為這些方法的功能基本上是相同的:
code: ? extension Shape { /// 返回 `true` ,假如 `other` 點(diǎn)在`self`面積之內(nèi). func contains(_ other: Point) -> Bool { ... } /// 返回 `true` 假如 `other` 圖形完全在 `self`之內(nèi). func contains(_ other: Shape) -> Bool { ... } /// 返回 `true` 假如 `other`線條在 `self`之內(nèi). func contains(_ other: LineSegment) -> Bool { ... } }由于幾何類型和集合是不同的域,因此在同一程序中也可以:
code: ? extension Collection where Element : Equatable { /// 返回 `true` 假如 `self` 包含一個同 `sought`相同的元素. func contains(_ sought: Element) -> Bool { ... } }當(dāng)然,這些索引方法具有不同的語義,并且應(yīng)該以不同的方式命名:
code: ? extension Database { /// 重建數(shù)據(jù)庫的搜索索引 func index() { ... } /// 返回給定表中的第n行 func index(_ n: Int, inTable: TableID) -> TableRow { ... } }最后,避免“在返回類型上重載”,因為它會在存在類型推斷時引起歧義:
code: ? extension Box { /// 返回存儲在`self`中的`Int`值,否則,返回`nil` func value() -> Int? { ... } /// 返回存儲在`self`中的`String `值,否則,返回`nil` func value() -> String? { ... } }
參數(shù)
func move(from start: Point, to end: Point)
-
選擇參數(shù)名稱以供文檔注釋。即使參數(shù)名稱沒有出現(xiàn)在函數(shù)或方法的使用點(diǎn),它們也起著重要的解釋作用。
選擇這些名稱可以使文檔易于閱讀。例如,以下這些名稱使文檔閱讀理解更加自然:
code: ? /// 返回滿足`predicate`斷言的,并包含`self`的元素集合 func filter(_ predicate: (Element) -> Bool) -> [Generator.Element] /// 以`newElements`替換給定 `subRange`范圍的集合。 mutating func replaceRange(_ subRange: Range, with newElements: [E])當(dāng)然,以下例子使文檔變得笨拙和不合語法:
code: ? /// 返回滿足`includedInResult `斷言的,并包含`self`的元素集合 . func filter(_ includedInResult: (Element) -> Bool) -> [Generator.Element] /// 以`with `替換給定 `r `范圍的集合。 mutating func replaceRange(_ r: Range, with: [E]) -
一般場景時,合理利用默認(rèn)參數(shù)。某一參數(shù)在大多數(shù)場景下都是某個固定值,比較適合設(shè)置默認(rèn)參數(shù)。
默認(rèn)參數(shù)通過隱藏不相關(guān)的信息來提高可讀性。例如:
code: ? let order = lastName.compare( royalFamilyName, options: [], range: nil, locale: nil)可以更為簡潔:
let order = lastName.compare(royalFamilyName)默認(rèn)參數(shù)通常比使用方法集更可取,因為它們會降低理解API的認(rèn)知負(fù)擔(dān)。
code: ? extension String { /// ...description... public func compare( _ other: String, options: CompareOptions = [], range: Range? = nil, locale: Locale? = nil ) -> Ordering }上述方案可能并不簡單,但相比方法集,足夠簡潔:
code: ? 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 }方法集合的每個成員都需要單獨(dú)的文檔注釋,并由用戶理解。用戶需要完全理解它們,才能選擇最優(yōu)方法。 偶爾也會出現(xiàn)令人驚訝的問題 - 例如,
foo(bar:nil)和foo()并不總是同等的 - 文檔繁瑣,差異卻微小。 使用含默認(rèn)值單一方法可提供極其優(yōu)越的編程體驗。 默認(rèn)參數(shù)應(yīng)放置在參數(shù)列表的末尾。沒有默認(rèn)值的參數(shù)通常對于方法的語義更為重要,并且在調(diào)用方法時提供穩(wěn)定的初始使用模式。
參數(shù)標(biāo)簽
func move(from start: Point, to end: Point)
x.move(from: x, to: y)
-
在無法有效區(qū)分參數(shù)時省略所有標(biāo)簽
如:
min(number1, number2),zip(sequence1, sequence2). -
在執(zhí)行
值保留類型轉(zhuǎn)換的構(gòu)造器中,省略第一個參數(shù)標(biāo)簽。第一個參數(shù)應(yīng)該始終是轉(zhuǎn)換的來源:
extension String { // 將`x`轉(zhuǎn)換為給定基數(shù)中的文本表示 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)但是,在“縮小”類型轉(zhuǎn)換中,添加描述縮小的標(biāo)簽是有必要的。
extension UInt32 { /// Creates an instance having the specified `value`. init(_ value: Int16) ← Widening, so no label /// 創(chuàng)建一個具有最低32位“source”的實例 `source`. init(truncating source: UInt64) /// 創(chuàng)建近似于`valueToApproximate`的實例 init(saturating valueToApproximate: UInt64) }值保持類型轉(zhuǎn)換是單態(tài)的,即原始值的每個差異均會導(dǎo)致結(jié)果值的差異。 例如,從Int8到Int64的轉(zhuǎn)換是值保留的,因為每個不同的Int8值都轉(zhuǎn)換為不同的Int64值;但是,在相反方向上的轉(zhuǎn)換不能保留值:Int64具有比Int8中表示的值更多的可能值。注意:檢索原始值的能力與轉(zhuǎn)換是否有保留值無關(guān)。
-
當(dāng)?shù)谝粋€參數(shù)構(gòu)成介詞短語的一部分時,給它設(shè)置一個參數(shù)標(biāo)簽。參數(shù)標(biāo)簽通常應(yīng)該從介詞開始,如
x.removeBoxes(havingLength: 12)。當(dāng)前兩個參數(shù)表示單個抽象的一部分時會出現(xiàn)異常:
code: ? a.move(toX: b, y: c) a.fade(fromRed: b, green: c, blue: d)這種情況,在介詞后添加參數(shù)標(biāo)簽,以保持抽象概念清晰。
code: ? a.moveTo(x: b, y: c) a.fadeFrom(red: b, green: c, blue: d)
- 否則,如果第一個參數(shù)構(gòu)成語法短語的一部分,則省略其標(biāo)簽.將前置的單詞附加到基本名稱上,例如,
x.addSubview(y)
本指南認(rèn)為如果第一個參數(shù)不構(gòu)成語法短語的一部分,它應(yīng)該有一個標(biāo)簽。
```
?
view.dismiss(animated: false)
let text = words.split(maxSplits: 12)
let studentsByName = students.sorted(isOrderedBefore:
Student.namePrecedes)
```
請注意,短語傳達(dá)正確的含義非常重要。以下可能表達(dá)會錯誤的觀點(diǎn)。
```
?
view.dismiss(false) Don't dismiss? Dismiss a Bool?
words.split(12) Split the number 12?
```
另請注意,可以省略含默認(rèn)值的參數(shù)。在這種情況下,不要形成語法短語的一部分,因此它們應(yīng)始終具有標(biāo)簽。
- 為其它所有參數(shù)添加參數(shù)標(biāo)簽。
特別說明
-
在API中為tuple元組成員添加參數(shù)標(biāo)簽,命名閉包參數(shù)。
這些名稱具有很好的解釋能力,可以從文檔注釋中引用,并提供對元組成員的訪問。
/// 確保我們有requestedCapacity最后一個元素的唯一引用的存儲單元。 /// /// 如果需要更多存儲空間,則調(diào)用`allocate` 。且分配的字節(jié)數(shù)`bygerCount`等于最大數(shù)。 /// /// - 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)用于閉包參數(shù)的名稱,應(yīng)如頂級函數(shù)的參數(shù)名稱一樣。在閉包參數(shù)調(diào)用處不應(yīng)出現(xiàn)參數(shù)標(biāo)簽。
-
使用不受約束的多態(tài)性(例如
Any,AnyObject和無約束的通用參數(shù))時要格外小心,以避免重載集中出現(xiàn)歧義。如考慮重載集:
? struct Array { /// 在 `self.endIndex`處插入`newElement`. public mutating func append(_ newElement: Element) /// 在`self.endIndex`順序插入 `newElements`內(nèi)容。 public mutating func append(_ newElements: S) where S.Generator.Element == Element }
這些方法形成一個語義簇,并且參數(shù)類型明顯不同。但是,當(dāng)Element為Any時,單個元素可以與元素序列具有相同的類型。
```
?
var values: [Any] = [1, "a"]
values.append([2, 3, 4]) // [1, "a", [2, 3, 4]] or [1, "a", 2, 3, 4]?
```
為消除歧義,可以更明確地命名第二個重載方法。
```
?
struct Array {
/// 在 `self.endIndex`處插入`newElement`.
public mutating func append(_ newElement: Element)
/// 在`self.endIndex`順序插入 `newElements`內(nèi)容
public mutating func append(contentsOf newElements: S)
where S.Generator.Element == Element
}
```
注:如何命名以更好地匹配文檔注釋。實際上是在編寫文檔注釋時,得到了API作者的注意。
更多
- 紕漏之處,歡迎斧正。
- 更多內(nèi)容請關(guān)注公眾號:
IT互聯(lián)網(wǎng)自習(xí)室。
