Apple 제공 Swift 프로그래밍 가이드(4.2)의 A Swift Tour 부분을 공부하며 정리한 글입니다. 개인적인 생각, 이해를 돕기 위한 예제도 조금 들어가있습니다.
들어가며
새로운 언어를 시작할 때 우리는 전통적으로 "Hello, world!"를 출력해봅니다. Swift에서 이것은 다음 한 줄로 해볼 수 있습니다.
print("Hello, world!")
만약 당신이 C 혹은 Objective-C를 사용해보았다면 Swift의 문법은 당신에게 친숙하게 느껴질 것입니다. input/output 혹은 string handling을 위한 별도의 라이브러리를 import 할 필요없이 위의 한 줄로 끝납니다. global scope에 작성된 코드는 프로그램의 entry point로 사용됩니다. main() 함수가 필요하지 않은 것입니다. 또한 모든 statement 뒤에 세미콜론(;)을 입력할 필요도 없습니다.
지금부터 시작하는 Swift 투어는 코드 작성을 위한 충분한 정보를 당신에게 제공할 것입니다. Swift를 통해 어떻게 다양한 프로그래밍 테스크들을 달성할 수 있는지 보여드리겠습니다. 도중 이해할 수 없는 것들이 있더라도 안심하세요. 이 투어에서 소개되는 모든 개념들은 나중에 자세하게 다룰 것입니다.
<Note>
이 투어는 Xcode의 playground와 함께 하면 더 좋습니다!
Simple Values
상수(constant)를 만들기 위해 let을, 변수(variable)를 만들기 위해 var를 사용하세요. 상수의 값(value)은 컴파일 타임에 알 필요가 없습니다. 단, 반드시 값 할당을 정확히 한 번 해야합니다. 상수는 불변하는 값을 자주 사용할 때 이용하면 좋습니다.
var myVariable = 42 myVariable = 50 let myConstant = 42
상수나 변수는 반드시 그것에 할당하고자 하는 값의 타입과 같은 타입을 가져야 합니다. 하지만 타입을 항상 명시적으로 나타내주어야 하는 것은 아닙니다. 상수나 변수를 생성할 때 그 값을 할당해주면 컴파일러가 알아서 타입을 유추합니다. 위 예제코드에서 컴파일러는 myVariable의 타입이 Integer라고 유추했습니다. 할당된 초기값이 Integer이기 때문입니다.
초기값이 타입 유추를 위한 충분한 정보를 제공하지 않는 경우 (또는 초기값이 없는 경우), 명시적으로 타입을 정의해주어야 합니다. 변수 이름 뒤에 콜론(:)으로 구분하여 명시해줍니다.
let implicitInteger = 70
let implicitDouble = 70.0
let explicitDouble: Double = 70
<Experiment>
상수를 새로 만들되 Float 타입을 명시적으로 정의해주고 4 라는 값을 할당해보세요.
변수는 절대 다른 타입으로 암시적 convert되지 않습니다. 만약 값을 다른 타입으로 convert 하고 싶다면, 원하는 타입의 인스턴스를 명시적으로 만들어야 합니다.
let label = "The width is "
let width = 94
let widthLabel = label + String(width)
<Experiment>
마지막 라인에서 conversion을 위한 String을 지워보세요. 어떤 에러가 나나요?
문자열에 값을 포함하기 위한 더 쉬운 방법을 소개합니다. 괄호 안에 값을 쓰세요. 그리고 괄호의 시작 앞에 백슬래쉬(\)를 쓰세요. 예제코드를 보여드리겠습니다.
let apples = 3
let oranges = 5
let appleSummary = "I have \(apples) apples."
let fruitSummary = "I have \(apples + oranges) pieces of fruit."
\() 를 사용해서 문자열 안에 floating-point 계산을 포함해보세요. 그리고 누군가의 이름을 포함한 인삿말을 작성해보세요.
여러 줄의 문자열을 작성할 때는 더블 따옴표 세 개(""")를 사용합니다. 각 라인의 들여쓰기는 closing quote(마지막 """)의 들여쓰기와 같은 레벨이 아닌 경우에는 무시됩니다. 예제를 보여드릴게요.
let quotation = """
Even though there's whitespace to the left,
the actual lines aren't indented.
Except for this line.
Double quotes (") can appear without being escaped.
I still have \(apples + oranges) pieces of fruit.
"""
let quotation1 = """
Line1
Line2
Line3
"""
//
1Line\n2Line\n3Linelet quotation2 = """
Line1
Line2
Line3
"""
//
1Line\n 2Line\n 3Linelet quotation3 = """
Line1
Line2
Line3
"""
//
1Line\n2Line\n3Line대괄호( [] )를 이용하여 배열과 딕셔너리를 만들 수 있습니다. 이것들의 각 요소에는 대괄호 안에 index(배열)와 Key(딕셔너리)를 넣어서 접근할 수 있습니다. 마지막 요소 뒤에 쉼표(comma)를 넣어도 괜찮습니다.
var shoppingList = ["catfish", "water", "tulips", "blue paint"]
shoppingList[1] = "bottle of water"
var occupations = [
"Malcolm": "Captain",
"Kaylee": "Mechanic",
]
occupations["Jayne"] = "Public Relations"
빈 배열이나 빈 딕셔너리를 만드려면 initializer 문법을 사용하세요.
let emptyArray = [String]()
let emptyDictionary = [String: Float]()
타입 정보가 유추될 수 있는 케이스에서는, 빈 배열은 [ ]로, 빈 딕셔너리는 [ : ]로 셋팅할 수 있습니다. 예를 들면 이미 만들어져 있는 변수에 새롭게 값을 할당하는 경우, 혹은 함수에 인자로 넘길 값을 셋팅하는 경우입니다.
shoppingList = []
occupations = [:]
Control Flow
조건문을 만들기 위해 if 와 switch를 사용하세요. 루프를 만들기 위해서는 for-in, while, repeat-while을 사용하세요. 조건문과 루프 변수를 감싸는 괄호는 생략 가능합니다. 조건문 본문(body)를 감싸는 괄호는 필수입니다.
let individualScores = [75, 43, 103, 87, 12]
var teamScore = 0
for score in individualScores {
if score > 50 {
teamScore += 3
} else {
teamScore += 1
}
}
print(teamScore)
if 안의 조건문은 반드시 Boolean expression 이어야 합니다. 예를 들어, if score { ... } 같은 코드를 생각해봅시다. Objective-C에서는 이와 같은 코드는 암시적으로 값이 0인지를 비교하는 조건문이었습니다. Swift에서 이런 코드는 에러로 판단되니 차이점에 유의합시다.
값이 없을지도 모르는 케이스를 위해 if 와 let 을 함께 사용할 수 있습니다. 참고로 이러한 값의 상태를 표현하기 위해 optional 이라는 개념을 사용합니다. Optional value는 실제 value(값) 혹은 nil(값이 없는 상태)을 저장할 수 있습니다. (추가설명: 반대로, Optional value가 아니라면 nil 이라는 상태를 나타낼 수 없습니다.) Value의 타입 뒤에 물음표 마크(?)를 붙임으로써 이 value가 optional이라고 정의할 수 있습니다. Objective-C에서는 따로 이런 정의를 하지 않더라도 새로 만든 객체에 값을 할당하기 전까지는 객체의 값이 nil이었습니다. 차이점에 유의합시다.
greeting = "Hello, \(name)"
<Experiment>
optionalName을 nil로 바꿔보세요. greeting은 어떤 값이 되나요? optional이 nil일 때 다른 greeting을 만들어주는 else문을 추가해보세요.
만약 optional value가 nil이라면 조건문은 false가 되고 괄호 안의 코드는 스킵됩니다. 반대의 경우(optional value가 nil이 아닌 경우)에는 optional value가 unwrap 되고 let 뒤의 상수에 그 unwrap된 값이 할당됩니다(위 예제에서는 name이라는 상수에 할당됩니다). 이 상수는 if block 안에서 유효합니다.
Optional value를 다루는 또 다른 방법은 ?? 연산자를 이용해 디폴트 값을 제공하는 것입니다. 만약 Optional value의 값이 없는 상태(nil인 상태)라면 디폴트 값이 대신 사용되는 방식입니다. 예제를 봅시다.
let nickName: String? = nil let fullName: String = "John Appleseed" let informalGreeting = "Hi \(nickName ?? fullName)"
Switch는 모든 종류의 데이터와 넓은 범위의 다양한 비교 연산을 지원합니다. Swift의 Switch에서는 Integer값으로만 비교 범위를 제한하거나 equality에 대해서만 비교할 필요가 없는 것입니다.
let vegetable = "red pepper"
switch vegetable {
case "celery":
print("Add some raisins and make ants on a log.")
case "cucumber", "watercress":
print("That would make a good tea sandwich.")
case let x where x.hasSuffix("pepper"):
print("Is it a spicy \(x)?")
default:
print("Everything tastes good in soup.")
}
<Experiment>
default case를 지워보세요. 어떤 에러가 발생하나요?
이제부터 살펴 볼 예제에서는 다음과 같은 점들을 살펴보겠습니다.
(1) 매치되는 값을 찾는 패턴에서 let 이 어떻게 사용되는지 살펴보겠습니다.
(2) Switch 문에서, 매치되는 case 안의 코드가 실행된 후 그 Switch문은 곧바로 끝나게 됩니다. 바로 아래의 case로 실행이 넘어가지 않는다는 것입니다. Objective-C에서는 break 등을 명시적으로 표기하지 않은 경우 case 실행이 끝나면 바로 아래의 case를 실행했었습니다. 이 차이에 유의하세요.
(3) for-in을 이용해서 딕셔너리의 아이템(key-value 페어로 제공된)을 iterate(순회)할 수 있습니다. 딕셔너리는 순서가 없는 collection입니다. 따라서 이 경우 랜덤 순서로 iterate 하게 됩니다.
let interestingNumbers = [
"Prime": [2, 3, 5, 7, 11, 13],
"Fibonacci": [1, 1, 2, 3, 5, 8],
"Square": [1, 4, 9, 16, 25],
]
var largest = 0
for (kind, numbers) in interestingNumbers {
for number in numbers {
if number > largest {
largest = number
}
}
}
print(largest)
<Experiment>
가장 큰 수가 어떤 종류의 수인지 (Prime, Fibonacci, Square 중) 추적할 수 있도록 변수를 하나 더 만들어서 이용해보세요.
while을 사용해서 조건을 충족할 때까지 code block을 반복 실행해보세요. 루프에 대한 조건 검사는 code block 실행 후 마다 이루어집니다. 적어도 한 번은 code block을 실행할 수 있게요.
var n = 2 while n < 100 { n *= 2 } print(n) var m = 2 repeat { m *= 2
..< 를 사용해 인덱스 범위(range)를 만들어 루프를 돌릴 수도 있습니다.
var total = 0 for i in 0..<4 { total += i } print(total)
A 이상 B 미만의 범위를 만드려면 A..<B 를, A 이상 B 이하의 범위를 만드려면 A...B를 사용하세요.
Functions and Closures
func 을 사용하여 함수를 정의하세요. 함수의 이름과 함수 인자들을 괄호 안에 넣어서 함수를 호출하세요. 함수 파라미터의 이름&타입 정의와 함수의 리턴 타입 정의를 구분하기 위해 -> 를 사용하세요.
func greet(person: String, day: String) -> String {
return "Hello \(person), today is \(day)."
}
greet(person: "Bob", day: "Tuesday")
<Experiment>
day 파라미터를 지우고, 오늘의 점심 식단을 알려주는 인사를 위해 새로운 파라미터를 추가해보세요.
기본적으로 함수는 parameter name을 argument label(인자 라벨)에 그대로 사용합니다. (추가설명: 함수 본문 안에서 쓰이는 이름이 parameter name이고 함수를 호출할 때 명시해주는 이름이 argument label입니다.) 따로 argument label을 정의하고 싶은 경우 함수 정의 시 파라미터 이름 앞에 써주도록 합시다. Argument label이 필요없도록 하고 싶다면 _ 를 써줍시다. 개인적으로 함수 호출 시에 사용하는 이름과 함수 본문 내에서 사용하는 이름을 구분할 수 있게 된 것이 마음에 듭니다. 좀 더 가독성이 높은 코드를 작성할 수 있을 것 같습니다.
func greet(_ person: String, on day: String) -> String {
return "Hello \(person), today is \(day)."
}
greet("John", on: "Wednesday")
합성된 값(compound value)을 만들기 위해 tuple을 사용해보세요. 함수에서 여러 개의 값을 리턴하고 싶었던 적이 있지 않았나요? Tuple은 이럴 때 사용합니다. Tuple의 요소들은 이름이나 숫자(0부터 시작하는 인덱스)에 의해 참조될 수 있습니다.
func calculateStatistics(scores: [Int]) -> (min: Int, max: Int, sum: Int) {
var min = scores[0]
var max = scores[0]
var sum = 0
for score in scores {
if score > max {
max = score
} else if score < min {
min = score
}
sum += score
}
return (min, max, sum)
}
let statistics = calculateStatistics(scores: [5, 3, 100, 3, 9])
print(statistics.sum)
print(statistics.2)
함수는 다른 함수 안에 정의될 수 있습니다(can be nested). 함수 A의 본문에 함수 B가 정의되어 있을 때, 함수 B를 nested function이라고 지칭하고 함수 A를 outer function이라고 지칭하겠습니다. Nested function은 outer function에 정의되어 있는 변수들에 접근할 수 있습니다. 함수가 길거나 복잡해질 때 nested function을 사용해보세요.
func returnFifteen() -> Int {
var y = 10
func add() {
y += 5
}
add()
return y
}
returnFifteen()
함수는 first-class type입니다. 즉 함수는 다른 함수를 리턴할 수 있다는 말입니다.
func makeIncrementer() -> ((Int) -> Int) {
func addOne(number: Int) -> Int {
return 1 + number
}
return addOne
}
var increment = makeIncrementer()
increment(7)
함수는 인자로 다른 함수를 받을 수도 있습니다.
func hasAnyMatches(list: [Int], condition: (Int) -> Bool) -> Bool {
for item in list {
if condition(item) {
return true
}
}
return false
}
func lessThanTen(number: Int) -> Bool {
return number < 10
}
var numbers = [20, 19, 7, 12]
hasAnyMatches(list: numbers, condition: lessThanTen)
함수는 사실 클로저(closure)의 특례(special case)입니다. 나중에 호출될 수 있는 코드 블럭이죠. 클로저 안의 코드는 클로저가 생성된 범위(scope)내의 변수나 함수에 접근가능합니다. 클로저가 실행되는 곳은 다른 scope라 할지라도 말입니다. 앞에서 살펴본 nested function만 봐도 그렇습니다. 클로저를 작성할 때 중괄호({ })로 코드를 감싸면 클로저의 이름을 정의하지 않아도 됩니다. 클로저의 argument & return type과 본문을 구분하기 위해 in 을 사용하세요.
numbers.map({ (number: Int) -> Int in
let result = 3 * number
return result
})
<Experiment>
위 예제를 수정하여 홀수라면 0을 리턴하는 클로저를 만들어보세요.
클로저를 좀 더 간결하게 작성하기 위한 몇 가지 옵션들이 있습니다. 클로저의 타입을 이미 알고 있는 경우(Delegate의 콜백 같은 케이스), 파라미터와 리턴 값의 타입을 생략할 수 있습니다. 단일 구문 클로저(Single statement closure. 한 줄의 명령어로만 이루어진 클로저를 말합니다)는 단일 구문의 결과값을 리턴합니다. 예제를 보시죠.
let mappedNumbers = numbers.map({ number in 3 * number })
print(mappedNumbers)
이름 대신 숫자(0부터 시작하는 인덱스)를 통해 파라미터에 접근할 수 있습니다. 이 방식은 짧은 클로저에서 흔히 사용됩니다. 함수의 마지막 인자로 전달되는 클로저는 괄호 바로 뒤에 올 수 있습니다. 함수의 인자가 오직 클로저 하나인 경우에는 괄호 전체를 생략할 수 있습니다.
let sortedNumbers = numbers.sorted { $0 > $1 }
print(sortedNumbers)
Objects and Classes
Class 키워드 뒤에 클래스 이름을 정의하여 자신만의 클래스를 생성하세요. 클래스 안의 프로퍼티 선언은 일반적인 상수/변수 선언과 같은 방식으로 하면 됩니다. 메서드와 함수 정의도 마찬가지입니다.
class Shape {
var numberOfSides = 0
func simpleDescription() -> String {
return "A shape with \(numberOfSides) sides."
}
}
<Experiment>
let으로 상수 프로퍼티를 하나 선언해보고 그 값을 리턴해주는 메서드를 하나 만들어보세요.
클래스 이름 옆에 괄호를 붙여서 클래스의 인스턴스를 생성할 수 있습니다. 점(.)을 통해 인스턴스의 프로퍼티와 메서드에 접근할 수 있습니다.
var shape = Shape()
shape.numberOfSides = 7
var shapeDescription = shape.simpleDescription()
사실 위 예제의 Shape 클래스는 중요한 것이 빠져있습니다. 그것은 인스턴스가 생성될 때 set up 되는 이니셜라이저입니다. init을 사용해 하나 만들어볼 수 있습니다.
class NamedShape {
var numberOfSides: Int = 0
var name: String
init(name: String) {
self.name = name
}
func simpleDescription() -> String {
return "A shape with \(numberOfSides) sides."
}
}
이니셜라이저 안을 살펴보시면 self 키워드를 통해서 프로퍼티로써의 name과 이니셜라이저 인자로써의 name을 구분하고 있습니다. 참고로 이니셜라이저 인자는 클래스의 인스턴스를 생성할 때 마치 함수 호출할 때처럼 넘겨주면 됩니다. 모든 프로퍼티는 값이 할당되어야 합니다. 선언 시(예제의 numberOfSides)에 할당하거나 이니셜라이저 안에서(예제의 name) 할당하세요.
객체가 해제(deallocated)되기 직전에 따로 cleanup 작업이 필요하다면 deinit을 사용하여 디이니셜라이저를 정의하세요.
클래스의 자식 클래스를 만들고 싶다면 자식 클래스의 이름 뒤에 콜론(:)과 함께 부모 클래스의 이름을 쓰세요. 참고로 클래스는 어떤 루트 클래스를 반드시 상속할 필요가 없습니다. 부모 클래스가 필요 없다면 정의하지 마세요. (Objective-C에서는 NSObject 객체가 루트 클래스의 역할을 합니다. 새로운 클래스가 필요한 경우 NSObject를 상속받아 새로운 객체를 만드는 것이 정석입니다.)
자식 클래스는 override 키워드를 사용해 부모 클래스의 메서드를 오버라이드할 수 있습니다. override 키워드를 붙이지 않은 채 부모 클래스가 가진 메서드 이름을 그대로 사용한다면 컴파일러가 에러로 인식합니다. 또한 부모 클래스가 가지지 않은 메서드를 정의하여 override 키워드를 붙인다면 그것 역시 컴파일러가 에러로 인식합니다.
class Square: NamedShape {
var sideLength: Double
init(sideLength: Double, name: String) {
self.sideLength = sideLength
super.init(name: name)
numberOfSides = 4
}
func area() -> Double {
return sideLength * sideLength
}
override func simpleDescription() -> String {
return "A square with sides of length \(sideLength)."
}
}
let test = Square(sideLength: 5.2, name: "my test square")
test.area()
test.simpleDescription()
<Experiment>
NamedShape의 또 다른 자식 클래스 Circle을 정의해보세요. 이니셜라이저의 인자로는 반지름과 이름을 받으면 되겠네요. Circle만의 area()와 simpleDescription() 메서드도 정의해보세요.
프로퍼티는 getter와 setter도 가질 수 있습니다. 예제를 통해 알아보아요.
class EquilateralTriangle: NamedShape {
var sideLength: Double = 0.0
init(sideLength: Double, name: String) {
self.sideLength = sideLength
super.init(name: name)
numberOfSides = 3
}
var perimeter: Double {
get {
return 3.0 * sideLength
}
set {
sideLength = newValue / 3.0
}
}
override func simpleDescription() -> String {
return "An equilateral triangle with sides of length \(sideLength)."
}
}
var triangle = EquilateralTriangle(sideLength: 3.1, name: "a triangle")
print(triangle.perimeter)
triangle.perimeter = 9.9
print(triangle.sideLength)
변수의 setter 로 넘어오는 새로운 값은 newValue라는 이름으로 접근할 수 있습니다. 위 예제에서 확인해보세요. newValue 대신 다른 이름으로 접근하고 싶다면 set 뒤에 괄호를 사용해 명시적인 이름을 정해줄 수 있습니다. (간단히 말하면 set (newPerimeter) { sideLength = newPerimeter / 3.0 } 같은 코드입니다.) 참고로 명시적인 이름을 정하게 되면 기존의 newValue라는 이름은 사용할 수 없게 됩니다.
이제 EquilateralTriangle의 이니셜라이저를 살펴봅시다. 이니셜라이저는 세 가지 스텝을 거칩니다.
1. 자식 클래스에 정의된 프로퍼티의 값을 할당.
2. 부모 클래스의 이니셜라이저를 호출.
3. 부모 클래스에 정의된 프로퍼티의 값을 변경(필요 시). 이 시점에서 메서드, getter, setter를 위한 각종 setup 작업들도 함께 함.
프로퍼티에 새로운 값을 할당하기 직전이나 직후에 항상 이루어져야 하는 작업이 있다면 willSet과 didSet을 사용하세요. 이니셜라이저 밖에서 프로퍼티의 값이 변경될 때마다 불리게 되는 코드 블록입니다. 예를 들어 아래 예제에서는 willSet을 통해 삼각형의 변 길이와 사각형의 변 길이를 항상 동일하게 만들어줍니다.
class TriangleAndSquare {
var triangle: EquilateralTriangle {
willSet {
square.sideLength = newValue.sideLength
}
}
var square: Square {
willSet {
triangle.sideLength = newValue.sideLength
}
}
init(size: Double, name: String) {
square = Square(sideLength: size, name: name)
triangle = EquilateralTriangle(sideLength: size, name: name)
}
}
var triangleAndSquare = TriangleAndSquare(size: 10, name: "another test shape")
print(triangleAndSquare.square.sideLength)
print(triangleAndSquare.triangle.sideLength)
triangleAndSquare.square = Square(sideLength: 50, name: "larger square")
print(triangleAndSquare.triangle.sideLength)
optional value를 다룰 때는 operation(메서드, 프로퍼티, 서브스크립팅 등) 앞에 ? 를 써주시면 됩니다. ? 앞에 있는 것이 nil이라면 ? 뒤에 있는 모든 것들은 무시되고 expression 자체가 nil을 반환합니다. ? 앞의 있는 것이 nil이 아니라면 optional value는 unwrapped되어 실제 값을 사용하여 expression을 수행하게 됩니다. 두 경우 모두 전체 expression의 값은 optional value입니다(nil이 되는 경우가 있으니까).
let optionalSquare: Square? = Square(sideLength: 2.5, name: "optional square")
let sideLength = optionalSquare?.sideLength
Enumerations and Structures
enum을 사용하여 Enumeration을 생성할 수 있습니다. 클래스를 비롯한 다른 타입들처럼 enumeration은 자신의 메서드를 가질 수 있습니다.
enum Rank: Int {
case ace = 1
case two, three, four, five, six, seven, eight, nine, ten
case jack, queen, king
func simpleDescription() -> String {
switch self {
case .ace:
return "ace"
case .jack:
return "jack"
case .queen:
return "queen"
case .king:
return "king"
default:
return String(self.rawValue)
}
}
}
let ace = Rank.ace
let aceRawValue = ace.rawValue
<Experiment>
Rank 두 개의 raw value 값을 비교해보는 함수를 작성해보세요.
스위프트에서는 기본적으로 Enumeration의 raw value를 0부터 할당하고 하나씩 올려갑니다. 그러나 명시적인 값을 할당할 수도 있습니다. 위 예제에서 Ace는 명시적으로 1을 raw value로 정했습니다. 나머지 케이스들은 명시적으로 raw value를 정하지 않았으므로 순차적으로 raw value가 매겨집니다. Raw value를 문자열이나 부동 소수점으로 정하는 것도 가능합니다. Raw value는 rawValue 라는 프로퍼티를 통해 접근할 수 있습니다.
Raw value를 통해 enumeration의 인스턴스를 만드려면 init?(rawValue:) 이니셜라이저를 이용하세요. 이 이니셜라이저는 raw value와 매칭되는 케이스를 반환해줍니다. 매칭되는 케이스가 없다면 nil을 반환합니다.
if let convertedRank = Rank(rawValue: 3) {
let threeDescription = convertedRank.simpleDescription()
}
enumeration의 case value들은 실제 value들입니다(raw value를 다른 방식으로 표현한 것이 아닙니다). 따라서 의미있는 raw value가 없는 케이스에는 굳이 raw value를 지정하지 않아도 됩니다.
enum Suit {
case spades, hearts, diamonds, clubs
func simpleDescription() -> String {
switch self {
case .spades:
return "spades"
case .hearts:
return "hearts"
case .diamonds:
return "diamonds"
case .clubs:
return "clubs"
}
}
}
let hearts = Suit.hearts
let heartsDescription = hearts.simpleDescription()
<Experiment>
Suit에 color() 메서드를 추가해보세요. spades와 clubs에 대해서는 "black"을, hearts와 diamonds에 대해서는 "red"를 반환하는 메서드입니다.
위 예제에서 hearts 케이스가 참조되는 두 가지 방식을 살펴봅시다. 먼저 hearts 상수에 값을 할당할 때는 Suit.hearts 라는 풀네임으로 참조되었습니다. hearts 상수에 따로 타입이 명시되지 않았기 때문입니다. 두 번째로, Switch문 안에서는 .heart로 참조되었습니다. 이미 Suit라는 타입임을 알고 있기 때문에 굳이 Suit.hearts 를 쓰지 않고 .hearts라는 단축된 형태를 써도 됩니다. 타입을 알고 있을 때는 언제라도 이렇게 단축된 형태를 사용할 수 있습니다.
Enumeration은 정의되는 시점에서 각 케이스의 raw value가 결정됩니다. 즉 하나의 Enumeration에서 만들어지는 모든 인스턴스들은 같은 케이스일 경우 언제나 같은 raw value를 가진다는 뜻입니다. Enumeration의 인스턴스를 생성하는 시점에서 결정되는 값(=각 인스턴스마다 달라지는 값)이 필요하다면, raw value 대신 associated value를 사용하세요. Associated value는 Enumeration 케이스 인스턴스의 stored property처럼 동작하는 값입니다. 예를 들어 일출 & 일몰 시간을 서버로부터 받아오는 상황을 생각해봅시다. 서버는 리퀘스트에 대한 응답으로 일출 & 일몰 시간을 제공하거나, 리퀘스트가 실패했다는 에러 메시지를 주어야 할 것입니다.
enum ServerResponse {
case result(String, String)
case failure(String)
}
let success = ServerResponse.result("6:00 am", "8:09 pm")
let failure = ServerResponse.failure("Out of cheese.")
switch success {
case let .result(sunrise, sunset):
print("Sunrise is at \(sunrise) and sunset is at \(sunset).")
case let .failure(message):
print("Failure... \(message)")
}
<Experiment>
ServerResponse에 대한 세 번째 케이스를 더해보고 switch 문에도 더해보세요.
위 예제에서 ServerResponse의 일출 & 일몰 시간을 Switch문의 케이스 안에서 어떻게 가져오고 있는지 살펴볼 수 있습니다.
struct 키워드를 이용하여 Structure를 생성하세요. Structure는 클래스로 할 수 있는 많은 것들을 똑같이 지원해줍니다. 메서드와 이니셜라이저도 포함해서요. Structure와 클래스의 가장 큰 차이점은 Structure는 다른 곳으로 값을 넘길 때 Copy가 되고 클래스는 참조가 넘어간다는 것입니다.
struct Card {
var rank: Rank
var suit: Suit
func simpleDescription() -> String {
return "The \(rank.simpleDescription()) of \(suit.simpleDescription())"
}
}
let threeOfSpades = Card(rank: .three, suit: .spades)
let threeOfSpadesDescription = threeOfSpades.simpleDescription()
<Experiment>
Card에 다음과 같은 메서드를 추가해보세요: 카드의 풀 덱(rank와 suit의 모든 조합)을 생성해주는 메서드.
Protocols and Extensions
protocol을 이용하여 프로토콜을 선언해봅시다.
protocol ExampleProtocol {
var simpleDescription: String { get }
mutating func adjust()
}
Class, enumeration, struct 모두 프로토콜을 따를 수 있습니다.
class SimpleClass: ExampleProtocol {
var simpleDescription: String = "A very simple class."
var anotherProperty: Int = 69105
func adjust() {
simpleDescription += " Now 100% adjusted."
}
}
var a = SimpleClass()
a.adjust()
let aDescription = a.simpleDescription
struct SimpleStructure: ExampleProtocol {
var simpleDescription: String = "A simple structure"
mutating func adjust() {
simpleDescription += " (adjusted)"
}
}
var b = SimpleStructure()
b.adjust()
let bDescription = b.simpleDescription
<Experiment>
위 프로토콜을 따르는 enumeration을 하나 작성해보세요.
SimpleStructure에 사용된 mutating 키워드는 무엇을 위함일까요? 이 메서드가 structure를 변경(modify)할 수 있음을 명시해주는 키워드입니다. SimpleClass에서는 mutating 키워드를 사용하지 않고도 adjust() 메서드를 정의하고 있습니다. 클래스의 메서드는 항상 그 클래스를 변경(modify)할 수 있기 때문입니다.
기존 타입에 기능을 더할 때는 extention 키워드를 사용해서 기존 타입을 확장해보세요. 새로운 메서드, computed property 같은 것들을 더할 수 있습니다. 확장을 통해 프로토콜을 따르게 할 수도 있습니다. 이때 따르고자 하는 프로토콜은 다른 곳(다른 라이브러리나 프레임워크도 포함해서)에 정의되어 있어도 상관없습니다.
extension Int: ExampleProtocol {
var simpleDescription: String {
return "The number \(self)"
}
mutating func adjust() {
self += 42
}
}
print(7.simpleDescription)
<Experiment>
Double 타입을 확장해서 absoluteValue 라는 프로퍼티를 더해보세요.
정의한 프로토콜을 마치 다른 타입들처럼 사용할 수 있습니다. 예를 들어 서로 타입은 다르더라도 동일하게 하나의 프로토콜을 따르고 있는 객체들이 있다고 해봅시다. 이 객체들을 collection으로 저장할 때 collection의 저장값 타입으로 프로토콜을 사용할 수 있습니다.
let protocolValue: ExampleProtocol = a
print(protocolValue.simpleDescription)
// print(protocolValue.anotherProperty) // Uncomment to see the error
변수 protocolValue는 런타임에 SimpleClass 타입이 됩니다. 그러나 컴파일러는 이 사실을 미리 알지 못하기 때문에 protocolValue를 단순히 ExampleProtocol 타입으로 취급합니다. 즉 예제의 세 번째 라인이 가리키는 것처럼, SimpleClass에만 정의되어 있는 메서드나 프로퍼티에 접근하려고 시도하면 컴파일 오류가 납니다.
Error Handling
Error 프로토콜을 따르는 타입을 통해 에러를 표현할 수 있습니다.
enum PrinterError: Error {
case outOfPaper
case noToner
case onFire
}
에러를 던지려면(throw) throw 키워드를 사용하세요. 에러를 던지는 함수에는 throws 라고 마크를 해주세요. 만약 함수 안에서 에러를 던지게 되면 함수는 그 즉시 return되고 함수를 호출한 코드에서 에러를 처리하게 됩니다.
func send(job: Int, toPrinter printerName: String) throws -> String {
if printerName == "Never Has Toner" {
throw PrinterError.noToner
}
return "Job sent"
}
에러를 처리하기 위한 방법은 몇 가지 정도 있습니다. 그 중 하나는 do-catch를 이용하는 것입니다. do 블럭 안에서, 에러가 던져질 수 있는 코드 앞에 try 라는 키워드를 쓰세요. do 블럭 안에서 발생한 에러는 catch 블럭 안에서 error 라는 이름으로 접근할 수 있습니다. 물론 error 대신 다른 이름을 지정할 수도 있습니다.
do {
let printerResponse = try send(job: 1040, toPrinter: "Bi Sheng")
print(printerResponse)
} catch {
print(error)
}
<Experiment>
send(job:toPrinter:) 함수가 에러를 던지도록 프린터 이름을 "Never Has Toner"로 바꿔보세요.
특정한 에러들을 처리하기 위해 catch 블럭을 여러 개 만들 수 있습니다. 마치 Switch문의 case를 만드는 것처럼 catch문을 작성하세요.
do {
let printerResponse = try send(job: 1440, toPrinter: "Gutenberg")
print(printerResponse)
} catch PrinterError.onFire {
print("I'll just put this over here, with the rest of the fire.")
} catch let printerError as PrinterError {
print("Printer error: \(printerError).")
} catch {
print(error)
}
<Experiment>
do 블럭 안에서 에러가 던져질 수 있도록 코드를 추가해보세요. 첫 번째 catch 블럭에서 에러가 처리되게 하려면 어떤 종류의 에러가 필요할까요? 두 번째와 세 번째 블럭은요?
에러를 처리하는 또 다른 방법은 try?를 이용하여 결과값을 optional로 바꾸는 것입니다. 만약 함수가 에러를 던진다면 결과값은 nil이 됩니다(발생된 에러는 무시됩니다). 함수가 에러를 던지지 않는다면 결과값은 함수가 리턴한 값을 담은 optional value가 됩니다.
let printerSuccess = try? send(job: 1884, toPrinter: "Mergenthaler")
let printerFailure = try? send(job: 1885, toPrinter: "Never Has Toner")
// 추가설명 print(printerSuccess) // Optional("Job sent") print(printerSuccess!) // "Job sent"
함수가 리턴되기 직전에, 즉 함수의 코드 실행이 끝난 다음 맨 마지막에 실행시키고 싶은 코드가 있다면 defer 블럭을 이용하세요. defer 블럭 안의 코드는 함수가 에러를 던지든 말든 무조건 실행됩니다. setup과 cleanup 작업 등을 위해 사용해보세요.
var fridgeIsOpen = false
let fridgeContent = ["milk", "eggs", "leftovers"]
func fridgeContains(_ food: String) -> Bool {
fridgeIsOpen = true
defer {
fridgeIsOpen = false
}
let result = fridgeContent.contains(food)
return result
}
fridgeContains("banana")
print(fridgeIsOpen)
Generics
꺾쇠 괄호 안에 이름을 넣어 제네릭 함수나 타입을 만들 수 있습니다.
func makeArray<Item>(repeating item: Item, numberOfTimes: Int) -> [Item] {
var result = [Item]()
for _ in 0..<numberOfTimes {
result.append(item)
}
return result
}
makeArray(repeating: "knock", numberOfTimes: 4)
제네릭 형태의 함수, 메서드, 클래스, enumeration, structure 등을 만들 수 있습니다.
// Reimplement the Swift standard library's optional type
enum OptionalValue<Wrapped> {
case none
case some(Wrapped)
}
var possibleInteger: OptionalValue<Int> = .none
possibleInteger = .some(100)
요구사항 리스트를 지정하기 위해 본문 앞에 where 키워드를 사용하세요. 예를 들면, 특정 프로토콜을 구현하고 있는 타입이 필요한 경우나, 서로 같은 타입이 두 개 필요한 경우나, 특정 부모 클래스를 가진 클래스가 필요한 경우입니다.
func anyCommonElements<T: Sequence, U: Sequence>(_ lhs: T, _ rhs: U) -> Bool
where T.Iterator.Element: Equatable, T.Iterator.Element == U.Iterator.Element {
for lhsItem in lhs {
for rhsItem in rhs {
if lhsItem == rhsItem {
return true
}
}
}
return false
}
anyCommonElements([1, 2, 3], [3])
<Experiment>
anyCommonElements(_:_:) 함수를 변경해보세요: 두 개의 순서가 일치하는 경우 요소들의 배열을 리턴하게 해보세요.
<T: Equatable>
은 <T> ... where T: Equatable 과 같은 뜻입니다.
'Swift 공식 가이드 > Swift 4' 카테고리의 다른 글
Strings and Characters (0) | 2018.06.20 |
---|---|
Basic Operators (0) | 2018.03.23 |
The basics (0) | 2018.03.02 |
Version Compatibility (0) | 2017.10.04 |
About Swift (0) | 2017.10.04 |