사용하는 시점에 분명한 것 이 가장 중요한 목표입니다. 메소드와 속성 같은 ‘개체 (entities)’ 들은 한 번 선언한 후 반복해서 사용 (used) 합니다. 사용법을 명확하고 간결하게 하도록 API 를 설계합니다. 설계를 잘 했는지 평가할 때, 선언만 봐서는 충분치 않습니다; 상황에 따라 명확하게 보이는지 확인하려면 항상 실제 사례를 검토하기 바랍니다.
분명한 것이 간결한 것보다 더 중요합니다. 스위프트 코드를 더 짧게 만들 순 있겠지만, 최소한의 문자로 코드를 가장 작게 만드는 것이 목표는 아닙니다 (non-goal). 스위프트 코드에서, 발생하는, 간결함이란, ‘강한 타입 시스템 (strong type system)’ 으로 인한 ‘부수적인 효과’ 와 ‘획일적인 코드 (boilerplate)’ 를 자연스럽게 줄이는 특징을 말합니다.
문서화 주석 (documentation comment)1 은 모든 선언마다 작성합니다. 문서화를 작성하면서 획득한 통찰력은 설계에 큰 영향을 줄 수 있으므로, 미루지 않도록 합니다.
스위프트의 ‘자체 마크 다운 (dialect of Markdown)’ 을 사용합니다.
시작을 ‘요약 (summary)’ 으로 해서 선언 중인 ‘개체 (entity)’ 를 먼저 설명합니다. 종종, API 를 선언과 요약만으로 완전히 이해할 수도 있습니다.
/// Returns a "view" of `self` containing the same elements in
/// reverse order.
/// 같은 원소를 순서만 거꾸로 하여 담고 있는 `self` 의 "view" 를 반환함.
func reversed() -> ReverseCollection
요약에 집중합니다; 가장 중요한 부분입니다. 훌륭한 ‘문서화 주석’ 대부분은 사실 뛰어난 요약으로만 구성되어 있습니다.
단일 문장 구절을 가능한 사용하고, 마침표로 끝냅니다. 완전한 문장은 사용하지 않습니다.2
함수 및 메소드는 무엇을 하는 (does) 지와 무엇을 반환하는 (returns) 지 설명하고, ‘널 효과 (null effects)’ 와 Void
반환은 생략합니다:
/// Inserts `newHead` at the beginning of `self`.
/// `self` 의 맨 처음 위치에 `newHead` 를 집어 넣음.
mutating func prepend(_ newHead: Int)
/// Returns a `List` containing `head` followed by the elements
/// of `self`.
/// `head` 와 그 뒤로 `self` 의 원소들을 담고 있는 `List` 를 반환함.
func prepending(_ head: Element) -> List
/// Removes and returns the first element of `self` if non-empty;
/// returns `nil` otherwise.
/// 비어 있지 않으면 `self` 의 첫 번째 원소를 제거하면서 반환함;
/// 그 외 경우면 `nil` 을 반환함.
mutating func popFirst() -> Element?
참고: 위의 popFirst
같이 드문 경우에, 세미콜론으로 구분된 여러 개의 문장으로 ‘요약’ 을 만들기도 합니다.
첨자 연산은 무엇에 접근하는 (accesses) 지 설명합니다:
/// Accesses the `index`th element.
/// `index` 번째 원소에 접근함.
subscript(index: Int) -> Element { get set }
초기자는 무엇을 생성하는 (creates) 지 설명합니다:
/// Creates an instance containing `n` repetitions of `x`.
/// `x` 를 `n` 번 반복하여 담고 있는 인스턴스를 생성함.
init(count n: Int, repeatedElement x: Element)
그 외 다른 모든 선언들은, 선언한 ‘개체’ 가 (is) 무엇인지 설명합니다.
/// 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.
/// `self` 의 맨 처음 위치에 있는 원소, 또는 `self` 가 비었을 경우 `nil`.
var first: Element?
...
선택 사항으로, 계속해서 하나 이상의 문단 및 ‘목록 항목 (bullet items)’ 을 둘 수 있습니다. 문단은 서로 빈 줄로 구분되도록 하며 ‘완전한 문장 (complete sentences)’ 을 사용합니다.
/// Writes the textual representation of each ← Summary
/// element of `items` to the standard output.
/// ← Blank line
/// The textual representation for each item `x` ← Additional discussion
/// is generated by the expression `String(x)`.
///
/// - Parameter separator: text to be printed ⎫
/// between items. ⎟
/// - Parameter terminator: text to be printed ⎬ Parameters section
/// at the end. ⎟
/// ⎭
/// - Note: To print without a trailing ⎫
/// newline, pass `terminator: ""` ⎟
/// ⎬ Symbol commands
/// - SeeAlso: `CustomDebugStringConvertible`, ⎟
/// `CustomStringConvertible`, `debugPrint`. ⎭
/// `items` 의 각 원소를 표현하는 문장을 ← 요약
/// 표준 출력 장치에 작성함.
/// ← 빈 줄
/// 각 항목 `x` 를 표현하는 문장은 ← 추가적인 설명
/// `String(x)` 라는 표현식으로 생성합니다.
///
/// - 매개 변수 separator: 항목 사이마다 출력되는 문장 ⎫
/// - 매개 변수 terminator: 맨 끝에 출력되는 문장 ⎬ 매개 변수 부분
/// ⎭
/// - 노트: 끝에 '새 줄 문자 (newline)' 없이 출력하려면 ⎫
/// `terminator: ""` 를 전달할 것 ⎟
/// ⎬ 기호 명령
/// - 같이보기: `CustomDebugStringConvertible`, ⎟
/// `CustomStringConvertible`, `debugPrint`. ⎭
public func print(
_ items: Any..., separator: String = " ", terminator: String = "\n")
공인된 ‘기호화된 문서화 마크업 (symbol documentation markup)’ 원소를 사용하여, 적절할 때마다, 요약 이외의 정보를 추가합니다.
‘기호화된 명령 구문 표현 (symbol command syntax)’3 을 가지는 공인된 ‘목록 항목’ 을 알아보고 사용하도록 합니다. ‘엑스코드 (Xcode)’ 같은 대중적으로 인기 있는 개발 도구는 다음 키워드로 시작하는 ‘목록 항목 (bullet items)’ 을 특수하게 취급합니다.
필요한 모든 단어를 포함시킨 이름을 사용하여 코드를 읽는데 모호함이 없도록 합니다.
예를 들어, ‘컬렉션 (collection)’ 에서 주어진 위치의 원소를 제거하는 메소드를 생각해 봅시다.
// 좋은 예제
extension List {
public mutating func remove(at position: Index) -> Element
}
employees.remove(at: x)
이 ‘메소드 서명 (method signature)’4 에서 at
이라는 단어를 생략하면, 메소드가 x
와 같은 원소를 찾아서 제거하는 것이라고 생각하지, x
를 사용해서 제거할 원소의 위치를 지시한다고는 생각하지 않을 수 있습니다.
// 잘못된 예제
employee.remove(x) // 불분명함 : x를 제거하는 것입니까?
필요없는 단어는 생략합니다. 이름에 있는 모든 단어는 사용자 쪽에 중요한 정보를 전달해야 합니다.
의도를 명확하게 하거나 의미의 모호함을 없애기 위해 더 많은 단어가 필요할 수도 있지만, 사용자가 이미 확보한 정보라서 과잉인 것들은 생략해야 합니다. 특히, 타입 정보를 단순하게 반복하는 (merely repeat) 단어는 생략합니다.
// 잘못된 예제
public mutating func removeElement(_ member: Element) -> Element?
allViews.removeElement(cancelButton)
이 경우, Element
라는 단어는 호출하는 쪽에 아무런 중요한 것도 추가하지 않습니다. 다음 API 가 더 좋을 것입니다:
// 좋은 예제
public mutating func remove(_ member: Element) -> Element?
allViews.remove(cancelButton) // 더 명확함
가끔씩, 모호함을 피하기 위해 타입 정보를 반복할 때도 있지만, 일반적으로 타입보다는 매개 변수의 역할 (role) 을 설명하는 단어를 사용하는 것이 더 좋습니다. 자세한 것은 다음 항목을 참고합니다.
변수, 매개 변수, 및 결합 타입은 그 역할에 따라 이름을 지으며, 타입 구속 조건으로 짓지 않도록 합니다.
// 잘못된 예제
var string = "Hello"
protocol ViewController {
associatedtype ViewType : View
}
class ProductionLine {
func restock(from widgetFactory: WidgetFactory)
}
이런 식으로 타입 이름을 재사용하면 분명함과 표현력의 최적화에 실패하게 됩니다. 이 보다는, ‘개체’ 의 역할 (role) 을 표현하는 이름을 선택하려고 노력합니다.
// 좋은 예제
var greeting = "Hello"
protocol ViewController {
associatedtype ContentView : View
}
class ProductionLine {
func restock(from supplier: WidgetFactory)
}
‘결합 타입 (associated type)’ 이 프로토콜 구속 조건에 너무 밀접하게 연결되어서 프로토콜 이름 이 (is) 역할인 경우라면, 프로토콜 이름에 Protocol
을 덧붙여서 충돌을 피합니다:
protocol Sequence {
associatedtype Iterator : IteratorProtocol
}
protocol IteratorProtocol { ... }
약한 타입 정보를 보완하여 매개 변수의 역할이 분명하도록 합니다.
특히, 매개 변수 타입이 NSObject
, Any
, AnyObject
이거나, Int
및 String
같은 ‘기반 타입 (fundamental type)’ 일 때는, 타입 정보와 사용 시의 상황이 의도를 온전히 전달하지 못할 수도 있습니다. 아래 예제에서, 선언만 보면 명확한 것 같지만, 사용할 때는 막연합니다.
// 잘못된 예제
func add(_ observer: NSObject, for keyPath: String)
grid.add(self, for: graphics) // 불분명함
분명함을 되살리려면, 타입이 약한 각각의 매개 변수 앞에 역할을 설명하는 명사를 붙입니다:
// 좋은 예제
func addObserver(_ observer: NSObject, forKeyPath path: String)
grid.addObserver(self, forKeyPath: graphics) // 명확함
메소드와 함수 이름은 사용할 때 문법에 맞는 영어 구절이 되도록 만드는 것이 좋습니다.
// 좋은 예제
x.insert(y, at: z) // “x 는, y 를 z 위치에 집어 넣습니다”
x.subViews(havingColor: y) // “x 의 하위 뷰는 y 라는 색상을 가집니다”
x.capitalizingNouns() // “x 는 명사를 대문자로 만듭니다”
// 잘못된 예제
x.insert(y, position: z)
x.subViews(color: y)
x.nounCapitalize()
첫 번째 인자 또는 두 번째 이후로 인자의 의미가 호출에서 중심이 아닐 때는 자연스러움이 줄어들어도 괜찮습니다:
AudioUnit.instantiate(
with: description,
options: [.inProcess], completionHandler: stopProgressBar)
‘공장 메소드 (factory methods)’5 의 이름은 “make
” 로 시작합니다. 가령, x.makeIterator()
라고 합니다.
초기자 및 공장 메소드 (factory methods) 호출 의 첫 번째 인자는 ‘기본 이름 (base name)6 으로 시작하는 구절’ 을 형성하지 않도록 합니다. 가령, x.makeWidget(cogCount: 47)
라고 합니다.
예를 들어, 이런 호출의 첫 번째 인자는 ‘기본 이름 (base name) 과 같은 구절’ 인 것 처럼 읽히도록 하지 않습니다.
// 좋은 예제
let foreground = Color(red: 32, green: 64, blue: 128)
let newPart = factory.makeWidget(gears: 42, spindles: 14)
let ref = Link(target: destination)
다음은, API 작성자가 첫 번째 인자에 ‘문법적인 연속성 (grammatical continuity)’ 을 부여하려고 한 것입니다.
// 잘못된 예제
let foreground = Color(havingRGBValuesRed: 32, green: 64, andBlue: 128)
let newPart = factory.makeWidget(havingGearCount: 42, andSpindleCount: 14)
let ref = Link(to: destination)
사실상, argument labels (인자 이름표) 에 대한 것과 마찬가지로 이 지침이 의미하는 것은 호출이 value preserving type conversion (값 보존 타입 변환) 을 하지 않는 한 첫 번째 인자는 이름표를 가진다는 것입니다.
let rgbForeground = RGBColor(cmykForeground)
함수와 메소드는 ‘부작용 (side-effects)’7 에 따라 이름을 짓습니다.
‘부작용’ 이 없는 것은 ‘명사구’ 로 읽히도록 합니다, 가령 x.distance(to: y)
, i.successor()
라고 합니다.
‘부작용’ 이 있는 것은 ‘명령형 동사구 (imperative verb phrases)’ 로 읽히도록 합니다, 가령 print(x)
, x.sort()
, x.append(y)
라고 합니다.
변경하는/변경하지 않는 메소드 쌍의 이름은 일관성이 있어야 합니다. ‘변경 메소드 (mutating method)’ 는 종종 비슷한 ‘의미 구조 (semantics)’ 를 가지지만, 그 자리에서 인스턴스를 갱신하는 대신 새 값을 반환하는, ‘변경하지 않는 (nonmutating)’ ‘별도 버전 (variant)’ 을 가집니다.
연산을 동사로 설명하는 것이 자연스러울 때는, ‘변경 메소드 (mutating method)’ 에 동사의 ‘명령형 (imperative)’ 을 사용하고 이에 대응되는 ‘변경하지 않는 (nonmutating)’ 것의 이름에 “ed” 나 “ing” 접미사를 적용합니다.
Mutating | Nonmutating | |
---|---|---|
x.sort() |
z = x.sorted() |
|
x.append(y) |
z = x.appending(y) |
|
변경하지 않는 ‘별도 버전 (variant)’ 의 이름은 (주로 “ed” 를 덧붙여서) 동사의 ‘과거 분사 (past participle8)’ 를 사용하는 것이 좋습니다:
/// 그 자리에서 `self` 를 거꾸로 만듬.
mutating func reverse()
/// `self` 를 거꾸로 만든 것의 복사본을 반환함.
func reversed() -> Self
...
x.reverse()
let y = x.reversed()
동사가 직접 목적어를 가지기 때문에 “ed” 를 추가하는 것이 문법적으로 맞지 않을 때는, 변경하지 않는 ‘별도 버전 (variant)’ 의 이름에, “ing” 를 덧붙이는, 동사의 ‘현재 분사 participle’ 를 사용합니다.
/// `self` 에서 모든 개행 문자를 벗겨냄.
mutating func stripNewlines()
/// `self` 에서 모든 개행 문자를 벗겨낸 것의 복사본을 반환함.
func strippingNewlines() -> String
...
s.stripNewlines()
let oneLine = t.strippingNewlines()
연산을 명사로 설명하는 것이 자연스러울 때는, ‘변경하지 않는 메소드 (nonmutating method)’ 에 명사를 사용하고 이에 대응되는 ‘변경하는 (nonmutating)’ 것의 이름에 “form” 접두사를 적용합니다.
Nonmutating | Mutating | |
---|---|---|
x = y.union(z) |
y.formUnion(z) |
|
j = c.successor(i) |
c.formSuccessor(&i) |
|
사용자가 변경하지 않는다면 불리언 메소드와 불리언 속성을 사용할 때 받는 쪽에서 ‘단언문 (assertions)’ 으로 읽혀지도록 합니다 가령 x.isEmpty
, line1.intersects(line2)
라고 합니다.
어떤 것이 무엇인지 (what something is) 설명하는 프로토콜은 명사로 읽혀지도록 합니다 (가령 Collection
이라고 합니다).
보유 능력 (capability) 을 설명하는 프로토콜은 able
, ible
, 또는 ing
접미사를 사용하여 이름을 지어야 합니다. (가령 Equatable
, ProgressReporting
이라고 합니다).
그 외 다른 타입, 속성, 변수, 및 상수는 명사로 읽혀지도록 이름을 짓습니다.
Term of Art (전문 기술 용어) | 명사 (noun) - 특정한 분야나 특정한 직업 내에서 엄밀하고, 특수한 의미를 가지는 단어 또는 구절9 | ||
애매한 용어는 피하고 더 일상적이고 의미도 잘 전달하는 단어를 사용합니다. “피부 (skin)” 가 목적에 알맞다면 굳이 “표피 (epidermis)” 라고 하지 않습니다. ‘전문 기술 용어 (term of art)’ 는 ‘핵심적인 소통 도구’ 이지만, 다른 경우라면 잃어 버릴 ‘결정적인 의미 (crucial meaning)’ 를 붙잡기 위해서만 사용해야 합니다.
기존에 확립된 의미를 유지하면서 전문 기술 용어를 사용합니다.
더 일상적인 단어 대신 ‘기술적인 용어 (technical term)’ 를 사용해야 하는 유일한 이유는 다른 경우라면 모호하거나 불명확해질 어떤 것을 엄밀하게 (precisely) 표현하는 것이기 때문입니다. 그러므로, API 는 ‘이론의 여지가 없는 의미’ 에 따라 엄격하게 용어를 사용해야 합니다.
전문가를 놀라게 하지 않도록 합니다: 이미 용어에 익숙한 사람이라도 새로운 의미가 발명된 것을 보면 놀라면서 화가 날 것입니다.
초보자를 혼란스럽게 하지 않도록 합니다: 용어를 배우려는 사람은 웹 검색을 할 것이고 전통적인 의미도 찾을 것입니다.
축약어를 피하도록 합니다. ‘축약어 (abbreviations)’, 특히 표준이-아닌 것은, 이해를 하려면 축약되지-않은 형태로 올바르게 번역해야만 한다는 점에서, 사실상 ‘전문 기술 용어 (terms-of-art)’ 입니다.
사용한 축약어가 의도한 의미가 어떤 것이든 웹 검색으로 쉽게 찾을 수 있어야 합니다.
선례를 받아들이도록 합니다. 기존 문화를 준수하는 것을 희생하면서까지 완전 초보자를 위해 용어를 최적화하지는 않도록 합니다.
서로 인접하게 붙은 형태의 자료 구조는, 초보자라면 List
라고 하는게 의미 파악이 더 쉬울지라도, List
라는 단순화된 용어보다 Array
라고 이름을 짓는 것이 더 좋습니다. ‘배열 (arrays)’ 은 ‘현대 컴퓨팅 (modern computing)’ 의 기반인 것으로, 모든 프로그래머가 배열이 무엇인지 알고 있거나-곧 배우게 될 것입니다. 거의 모든 프로그래머에게 익숙한 용어를 사용해야, 웹 검색이나 질문에 대한 보상을 받을 것입니다.
수학 같은, 특정한 프로그래밍 분야 (domain) 에서는 sin(x)
처럼 이미 널리 사용되는 용어가 verticalPositionOnUnitCircleAtOriginOfEndOfRadiusWithAngle(x)
10 같은 설명 방식의 구절보다 더 적합합니다. 이 경우에는, 축약어를 피하라는 지침보다 선례가 더 중시된다는 점을 기억하도록 합니다: 완전한 단어는 sine
일지라도, “sin(x)” 이 프로그래머 사이에서는 수십년 동안, 그리고 수학자 사이에서는 수백년 동안 일상적으로 사용되어 왔습니다.
계산 속성의 복잡도가 ‘O(1)’ 이 아니라면 이를 문서화합니다.11 사람들은 속성에 접근할 때, 당연히 저장 속성이라고 생각하기 때문에, 상당한 계산과 얽힐 일은 없을 것이라고 가정합니다. 이러한 가정을 위반하게 될 때는 경고를 하도록 합니다.
자유 함수 (free functions) 보다는 메소드와 속성을 사용하도록 합니다. ‘자유 함수 (free function)’12 는 특수한 경우에만 사용합니다:
1 . self
가 분명하지 않을 때:
```swift
min(x, y, z)
```
2 . 함수가 ‘구속 조건이 없는 제네릭 (unconstrained generic)’ 일 때:
```swift
print(x)
```
3 . ‘함수 구문 표현’ 이 ‘확립된 분야의 표기법’ 에 해당할 때:
```swift
sin(x)
```
대소문자 협약을 따르도록 합니다. 타입과 프로토콜의 이름은 UpperCamelCase
(낙타 모양 대문자)13 입니다. 그 외 모든 것들은 lowCamelCase
(낙타 모양 소문자) 입니다.
미국 영어에서 일상적으로 모두 대문자로 나타내는 두문자어 (Acronyms and Initialisms)14 는 ‘대소문자 협약’ 에 따라 대문자나 소문자로 균일하게 표기해야 합니다:
var utf8Bytes: [UTF8.CodeUnit]
var isRepresentableAsASCII = true
var userSMTPServer: SecureSMTPServer
그 외 다른 ‘두문자어’ 는 일상적인 단어로 취급합니다:
var radarDetector: RadarScanner
var enjoysScubaDiving = true
메소드는 기본 이름을 공유할 수 있는데 동일한 기본 의미를 공유할 때 또는 서로 별개인 분야에서 작동할 때 하면 됩니다.
예를 들어, 메소드들이 본질적으로 같은 것을 하는 경우, 다음 처럼 할 것을 권장합니다:
// 좋은 예제
extension Shape {
/// `other` 가 `self` 내에 있다는 필요충분조건을 만족하면 `true` 를 반환함.
func contains(_ other: Point) -> Bool { ... }
/// `other` 전체가 `self` 내에 있다는 필요충분조건을 만족하면 `true` 를 반환함.
func contains(_ other: Shape) -> Bool { ... }
/// `other` 가 `self` 내에 있다는 필요충분조건을 만족하면 `true` 를 반환함.
func contains(_ other: LineSegment) -> Bool { ... }
}
‘기하학 타입 (geometric types)’ 과 ‘집합체 타입 (collections; 컬렉션)’ 은 서로 별도의 분야이므로, 동일한 프로그램에서 아래 처럼 해도 괜찮습니다:
// 좋은 예제
extension Collection where Element : Equatable {
/// `self` 가 `sought` 와 같은 원소를 가지고 있다는 필요충분조건을 만족하면 `true` 를 반환함.
func contains(_ sought: Element) -> Bool { ... }
}
하지만, 아래의 index
메소드들은 서로 다른 ‘의미 구조 (semantics)’ 를 가지므로, 이름을 서로 다르게 지어야 합니다:
// 잘못된 예제
extension Database {
/// 데이터베이스의 검색 색인을 다시 제작함.
func index() { ... }
/// 주어진 테이블에서 `n` 번째 행을 반환함.
func index(_ n: Int, inTable: TableID) -> TableRow { ... }
}
마지막으로, “반환 타입에 대한 중복 정의 (overloading on return type)” 는 타입 추론 시에 모호함을 유발할 수 있기 때문에 피하도록 합니다.
// 잘못된 예제
extension Box {
/// 만약 있다면, `self` 에 저장된 `Int` 를 반환하고,
/// 다른 경우라면 `nil` 을 반환함.
func value() -> Int? { ... }
/// 만약 있다면, `self` 에 저장된 `String` 을 반환하고,
/// 다른 경우라면 `nil` 을 반환함.
func value() -> String? { ... }
}
func move(from start: Point, to end: Point)
매개 변수 이름은 문서화에 알맞게 선택합니다. 매개 변수 이름이 함수나 메소드를 사용할 때는 나타나지 않는다 하더라도, 설명에 있어서는 중요한 역할을 합니다.
‘문서화’ 한 것이 쉽게 읽을 수 있도록 하는 이름을 선택합니다. 예를 들어, 아래의 이름은 ‘문서화’ 한 것을 자연스럽게 읽히도록 만듭니다.
// 좋은 예제
/// `self` 의 원소 중 `predicate` 를 만족하는 것을 담은 `Array` 를 반환함.
func filter(_ predicate: (Element) -> Bool) -> [Generator.Element]
/// 주어진 `subRange` 의 원소들을 `newElements` 로 대체함.
mutating func replaceRange(_ subRange: Range, with newElements: [E])
하지만, 아래는 ‘문서화’ 를 어색하고 문법에 맞지 않게 만듭니다.
// 잘못된 예제
/// `self` 의 원소 중 `includedInResult` 를 만족하는 것을 담은 `Array` 를 반환함.
func filter(_ includedInResult: (Element) -> Bool) -> [Generator.Element]
/// `r` 로 지시한 범위의 원소들을 `with` 의 내용으로 대체함.
mutating func replaceRange(_ r: Range, with: [E])
기본 설정 매개 변수 (defaulted parameters) 의 장점은 일상적인 사용법을 단순화할 때마다 취하도록 합니다. 일상적으로-사용되는 단일 값을 가지는 매개 변수라면 어떤 것이든 ‘기본 설정 (default)’ 에 대한 후보입니다.
‘기본 설정 인자 (default arguments)’ 는 관련 없는 정보를 숨겨서 가독성을 개선합니다. 예를 들어:
// 잘못된 예제
let order = lastName.compare(
royalFamilyName, options: [], range: nil, locale: nil)
는 훨씬 더 간단하게 될 수 있습니다:15
// 좋은 예제
let order = lastName.compare(royalFamilyName)
‘기본 설정 인자’ 가 일반적으로 ‘메소드 일족 (method families)’ 을 사용하는 것보다 더 좋은데, 왜냐면 API 를 이해하려는 사람에게 더 부담을 적게 주기 때문입니다.
// 좋은 예제
extension String {
/// ...설명...
public func compare(
_ other: String, options: CompareOptions = [],
range: Range? = nil, locale: Locale? = nil
) -> Ordering
}
위에 있는 것은 간단해 보이지 않지만, 아래 보다는 훨씬 더 간단합니다:
// 잘못된 예제
extension String {
/// ...설명 1...
public func compare(_ other: String) -> Ordering
/// ...설명 2...
public func compare(_ other: String, options: CompareOptions) -> Ordering
/// ...설명 3...
public func compare(
_ other: String, options: CompareOptions, range: Range) -> Ordering
/// ...설명 4...
public func compare(
_ other: String, options: StringCompareOptions,
range: Range, locale: Locale) -> Ordering
}
‘메소드 일족 (method family)’ 의 모든 구성원은 사용자가 별도로 문서화해서 이해해야 합니다. 이 중에서 결정을 하려면, 사용자가 이 모든 것을 이해할 필요가 있으며, 가끔 있는 놀라운 관계-예를 들어, foo(bar: nil)
과 foo()
가 항상 ‘동의어 (synonyms)’ 는 아니라는 것-은 이를 거의 대부분이 똑같은 ‘문서화’ 에서 사소한 차이점을 캐내야 하는 지루한 과정으로 만들어 버립니다. 기본 설정을 가진 단일 메소드를 사용하는 것은 대단한 상급 프로그래밍 경험을 제공합니다.
기본 설정을 가진 매개 변수의 위치는 매개 변수 목록의 끝으로 보내는 것이 좋습니다. 기본 설정이 없는 매개 변수가 주로 메소드의 의미 구조상 더 본질적이며, 메소드 호출에서 사용하기에 안정된 ‘초기화 패턴 (initial pattern)’ 을 제공합니다.
func move(from start: Point, to end: Point)
x.move(from: x, to: y)
인자를 구별하는게 쓸모 없을 때는 모든 이름표를 생략합니다, 가령 min(number1, number2)
, zip(sequence1, sequence2)
처럼 합니다.
‘값 보존 타입 변환 (value preserving type conversion)’ 을 하는 초기자에서, 첫 번째의 인자 이름표는 생략합니다, 가령 Int64(someUInt32)
처럼 합니다.
첫 번째 인자는 항상 ‘변환의 원천 (source)’ 이어야 합니다.
extension String {
// `x` 를 주어진 '진수 (radix)' 표현의 문장으로 변환함.
init(_ x: BigInt, radix: Int = 10) // ← 초기 밑줄에 주목합니다.
}
text = "The value is: "
text += String(veryLargeNumber)
text += " and in hexadecimal, it's"
text += String(veryLargeNumber, radix: 16)
하지만, (범위를) “좁히는 (narrowing)” 타입 변환의 경우, 좁힘을 설명하는 이름표를 권장합니다.
extension UInt32 {
/// 지정한 `value` 를 가지는 인스턴스를 생성함.
init(_ value: Int16) // ← 넓히는 것이므로, 이름표가 없습니다.
/// `source` 에서 가장 낮은 32 자리에 해당하는 것을 가지는 인스턴스를 생성함.
init(truncating source: UInt64)
/// `valueToApproximate` 를 가장 근접하게 표현할 수 있는 근사 값을 가지는 인스턴스를 생성함.
init(saturating valueToApproximate: UInt64)
}
값 보존 타입 변환은 단사 사상 (monomorphism)16 입니다. 즉, 소스 값에 있는 모든 차이가 결과 값에 있는 차이로 귀결됩니다. 예를 들어,
Int8
에서Int64
로의 변환은 값 보존인데 서로 다른 모든Int8
값이 서로 다른Int64
값으로 변환되기 때문입니다. 하지만, 다른 방향으로 변환하는 것은 값 보존일 수 없습니다:Int64
은Int8
으로 표현할 수 있는 것보다 더 많은 값을 가지고 있습니다.참고 : 원래의 값을 가져오는 능력은 변환이 값 보존인지 와는 관계가 없습니다.
첫 번째 인자가 전치사 구 (prepositional phrase) 를 형성할 때는, 인자 이름표를 부여합니다. 인자 이름표는 보통 전치사 (preposition) 로 시작해야 합니다, 가령 x.removeBoxes(havingLength: 12)
처럼 합니다.
처음 두 인자가 ‘단일 추상 명사 (single abstraction)’ 를 표현할 때는 예외입니다.
// 잘못된 예제
a.move(toX: b, y: c)
a.fade(fromRed: b, green: c, blue: d)
이런 경우, 전치사 다음에 (after) 인자 이름표를 시작해서, ‘추상성 (abstraction)’ 을 명확하게 유지합니다.
// 좋은 예제
a.moveTo(x: b, y: c)
a.fadeFrom(red: b, green: c, blue: d)
그 외 다른 경우, 첫 번째 인자가 문법에 맞는 구절을 형성한다면, 이름표를 생략하고, ‘기본 이름 (base name)’ 앞에 어떤 단어를 추가합니다, 가령 x.addSubview(y)
처럼 합니다.
이 지침은 만약 첫 번째 인자가 문법에 맞는 구절을 형성하지 않는 (doesn’t) 다면, 이름표를 가져야만 한다는 것을 의미합니다.
// 좋은 예제
view.dismiss(animated: false)
let text = words.split(maxSplits: 12)
let studentsByName = students.sorted(isOrderedBefore: Student.namePrecedes)
구절은 올바른 의미를 전달하는 것이 중요하다는 것을 기억하기 바랍니다. 아래에 있는 것은 문법에는 맞겠지만 잘못된 것을 표현하게 될 것입니다.
// 잘못된 예제
view.dismiss(false) // 물러나지 말아야 하는 것인가? 불리언 값을 물러가게 하라는 것인가?
words.split(12) // 12 라는 수를 쪼개야 하는가?
기본 값을 가지는 인자는 생략할 수 있어서, 그 경우 문법에 맞는 구절을 형성하지 않게 되므로, 이름표를 항상 가져야 한다는 것 또한 기억하기 바랍니다.
다른 모든 인자들은 이름표를 붙입니다.
튜플 멤버의 이름표와 클로저 매개 변수의 이름은 API 에 있는 곳마다 붙입니다.
이 이름들은 설명에 큰 힘이 되며, ‘문서화 주석 (documentation comments)’ 에서 참조할 수도 있고, 튜플 멤버에 접근할 때 의미 부여를 할 수 있게 됩니다.
/// 최소한 `requestedCapacity` 원소들을 위한 유일-참조 저장소를 쥐고 있음을 보장함.
///
/// 더 많은 저장소가 필요하면, 할당 시의 최대-할당 바이트 개수인
/// `byteCount` 를 가지고 `allocate` 를 호출한다.
///
/// - 반환 값:
/// - reallocated: `true` 새로운 메모리 블럭이 할당된 경우 `true`
/// - capacityChanged: `capacity` 가 갱신된 경우 `true`
mutating func ensureUniqueStorage(
minimumCapacity requestedCapacity: Int,
allocate: (_ byteCount: Int) -> UnsafePointer<Void>
) -> (reallocated: Bool, capacityChanged: Bool)
클로저 매개 변수의 이름은 최상단 함수에 대한 매개 변수 이름 (parameter names) 인 것처럼 선택해야 합니다. 클로저 인자에 대한 이름표를 호출할 때 나타내는 것은 지원하지 않습니다.
‘구속 조건이 없는 다형성 (unconstrained polymorphism)’ 은 좀 더 주의해서 (가령 Any
, AnyObject
, 및 구속 조건이 없는 제네릭 매개 변수의) 중복정의 집합에서 모호함을 피하도록 합니다.
예를 들어, 다음의 중복정의 집합을 고려해 봅시다:
// 잘못된 예제
struct Array {
/// `newElement` 를 `self.endIndex` 위치에 집어 넣음.
public mutating func append(_ newElement: Element)
/// `newElements` 의 내용들을 , 순서대로,
/// `self.endIndex` 위치에, 집어 넣음.
public mutating func append(_ newElements: S)
where S.Generator.Element == Element
}
이 메소드들은 ‘의미 구조 상의 일족 (semantic family)’ 을 형성하며, 맨 처음에 나타나는 인자 타입은 뚜렷하게 구별됩니다. 하지만, Element
가 Any
일 때, ‘단일 원소 (single element)’ 도 ‘일련의 원소들 (sequnce of elements)’ 과 같은 타입을 가질 수 있습니다.
// 잘못된 예제
var values: [Any] = [1, "a"]
values.append([2, 3, 4]) // [1, "a", [2, 3, 4]] or [1, "a", 2, 3, 4]?
모호함을 없애기 위해, 두 번째 중복정의의 이름을 더 명시적으로 만듭니다.
// 좋은 예제
struct Array {
/// `newElement` 를 `self.endIndex` 위치에 집어 넣음.
public mutating func append(_ newElement: Element)
/// `newElements` 의 내용들 (contents of) 을 , 순서대로,
/// `self.endIndex` 위치에, 집어 넣음.
public mutating func append(contentsOf newElements: S)
where S.Generator.Element == Element
}
새로운 이름이 ‘문서화 주석’ 과 얼마나 더 잘 일치하는 지를 주목하기 바랍니다. 이 경우, 문서화 주석을 작성한 행위가 실제로 API 작성자의 관심을 논점으로 이끈 것입니다.
‘문서화 주석 (documentation comment)’ 이란 ‘Xcode 편집기’ 에서 스위프트 코드의 선언을 ‘옵션-클릭’ 했을 때 나타나는 주석을 말합니다. 문서화 주석은 개발자가 직접 추가할 수 있으므로, ‘API 설계 지침’ 에서는 자신이 작성하고 있는 코드에도 ‘문서화 주석’ 을 추가하라고 조언하고 있습니다. ↩
여기서 ‘문장 구절 (sentence fragment)’ 을 사용하라는 것은 완전한 문장이 아니라, 하나의 구절 형태로 사용하라는 의미입니다. ‘API 설계 지침’ 에서는 ‘요약 (summary)’ 에는 ‘문장 구절’ 을 사용하고, 이어지는 ‘문단 설명’ 에서는 ‘완전한 문장’ 사용하라고 조언하고 있습니다. ↩
링크 자체는 바로 위의 링크와 같은 symbol documentation markup 문서로 연결됩니다. ↩
‘메소드 서명 (method signature)’ 은 메소드 이름과 매개 변수의 개수 및 타입, 그리고 순서 등으로 구성된 것으로, 메소드를 식별하기 위해 사용되는 것입니다. 보다 자세한 정보는 위키피디아의 Type signature 항목에 있는 Method signature 부분을 보도록 합니다. ↩
‘공장 메소드 (factory method)’ 는 ‘공장 메소드 패턴 (factory method pattern)’ 에서 사용하는 메소드로, 구체적으로 ‘고정된 타입 (concrete type)’ 을 지정하지 않은 채 객체를 생성할 수 있게 해줍니다. 이에 대한 더 자세한 정보는 위키피디아의 Factory method pattern 항목과 팩토리 메서드 패턴 항목을 보도록 합니다. ↩
‘기본 이름 (base name)’ 은 함수 또는 메소드에서 매개 변수와 괄호를 뺀 순수한 함수 자체의 이름인 것으로 추측됩니다. ↩
컴퓨터 용어에서의 ‘부작용 (side-effects)’ 은 무조건 나쁜 것이 아니라 ‘부수적인 효과’ 정도의 의미로 이해할 수 있습니다. ↩
원문 자체가 위키피디아의 participle 항목에 대한 링크로 되어 있습니다. ↩
이어지는 내용을 보면 알겠지만, 스위프트는 이런 ‘기술 용어 (term of art)’ 대신 일상 용어를 더 많이 사용할 것을 권장하고 있습니다. 스위프트 표준 라이브러리에 있는 클래스들을 봐도, Image
나 Button
처럼, 접두사 없이 일상 용어로 타입 이름을 정하는 것을 볼 수 있습니다. ↩
‘verticalPositionOnUnitCircleAtOriginOfEndOfRadiusWithAngle’ 이 말을 직역하면 ‘각도를 가진 반지름의 끝이 원점에 있는 단위 원 상에 있을 때의 수직 위치’ 정도로 옮길 수 있습니다. ↩
컴퓨터 용어로 ‘복잡도 (complexity)’ 라는 것은 알고리즘을 실행하는데 필요한 자원의 총량을 나타내는 말입니다. 보다 자세한 내용은 위키피디아의 Computational complexity 항목을 보도록 합니다. ↩
스위프트에서 ‘자유 함수 (free function)’ 는 어느 영역에도 소속되어 있지 않은 ‘멤버가 아닌 함수 (non-member function)’-즉 일종의 전역 함수-를 의미합니다. 보다 자세한 내용은 위키피디아의 Free function 항목을 보도록 합니다. ↩
‘낙타 모양 대소문자 (camel case)’ 는, 변수 이름을 지정할 때 모든 단어를 붙이고. 각 단어의 첫 글자를 대문자로 표기하면, 모양이 낙타 등처럼 생겼기 때문에 나온 말입니다. ‘낙타 모양 대소문자 (camel Case)’ 에 대한 보다 자세한 내용은 위키피디아의 Camel case 와 낙타 대문자 항목을 보도록 합니다. ↩
‘두문자어 (Acronyms and Initialisms)’ 는 ‘ASCII’ 같이 단어의 앞머리 글자만 떼어 만든 줄임말을 의미합니다. 그리고 영어의 ‘Acronyms’ 와 ‘Initialisms’ 는 사실상 같은 단어입니다. 우리말의 초성체 도 사실상 두문자어라고 할 수 있습니다. 보다 자세한 내용은 위키피디아의 Acronym 항목과 두문자어 항목을 보도록 합니다. ↩
‘기본 설정 매개 변수 (default parameters)’ 를 사용하면 함수를 호출할 때 그와 연관된 인자를 생략할 수 있어서 코드가 간단해집니다. 스위프트의 print(_:separator:terminator:)
함수가 대표적인 예라고 할 수 있습니다. ↩
원문에서는 ‘단사 사상 (monomorphism)’ 이라는 말을 사용하고 있지만, 이를 사실 ‘단사 함수 (injective function)’ 의 의미로써 사용하고 있습니다. ‘단사 함수’ 는 ‘정의역 또는 소스 (source)’ 에 있는 서로 다른 원소를 ‘공역 또는 결과 (result)’ 에 있는 서로 다른 원소로 대응시키는 함수를 말합니다. 보다 자세한 내용은 위키피디아의 Monomorphism 항목 및 단사 사상 항목, 그리고 Injective function 항목 및 단사 함수 항목을 보도록 합니다. ↩