Swift 둘러보기

새로운 언어의 첫 번째 프로그램은 "Hello, world!"라는 단어를 화면에 찍는것이 전통이다. Swift에서는 다음 한줄로 화면에 수행할 수 있다.

print("Hello, world!")

C 또는 Objective-C로 코드를 작성해보았다면 Swift에서 이 구문은 Swift에서 위의 코드는 익숙할것이다. 입/출력 또는 문자열 처리와 같은 기능을 위해 별도의 라이브러리를 가져올 필요가 없다. 전역 범위에서 작성된 코드는 프로그램의 진입점으로 사용되는 main() function이 필요하지 않다. 또한 모든 문장의 끝에 세미콜론을 쓸 필요가 없다.

이 장에서는 다양한 프로그래밍 작업을 수행하는 방법을 보여줌으로써 Swift에서 코드를 작성하기에 충분한 정보를 제공한다. 이해할 수 없어도 걱정하지 마시라 .이 둘러보기에서 소개 된 모든 내용이 이 책의 나머지 부분에서 자세히 설명된다.

NOTE

가장 좋은 방법은, Xcode에서 playground로 이 챕터를 열어볼것. Playground를 이용하면 코드 목록을 편집하고 바로 그 결과를 볼 수 있다.

Download Playground


Simple Values

상수를 만들려면 let 을 이용하고 변수를 만들려면 var를 이용하라. 상수 값은 컴파일 타임에 알려질 필요는 없지만 정확히 한 번 값을 할당해야 한다. 즉, 상수를 사용하여 한 번 결정된것을 이름으로 여러 곳에서 사용할 수 있다.

var myVariable = 42
myVariable = 50
let myConstant = 42

상수 또는 변수는 할당하려는 값과 동일한 타입이어야 한다. 그러나 타입을 명시적으로 쓰지 않아도 된다.

상수 또는 변수를 만들때 값을 제공하면 컴파일러에서 타입을 추론 할 수 있다. 위의 예제에서, 컴파일러는 초기값이 integer이기 때문에 myVariable이 integer로 추론한다.

만약, 초기값이 충분한 정보가 제공되지 않으면(또는 초기값이 없는 경우), 변수 뒤에 콜론(:)으로 구분하여 입력하여 타입을 지정할 것.

let implicitInteger = 70
let implicitDouble = 70.0
let explicitDouble: Double = 70

EXPERIMENT

명시적인 Float 타입의 상수를 만들고 값은 4로 만들어 보자.

값은 암시적으로 다른 타입으로 변환되지 않는다. 값을 다른 타입으로 변환해야하는 경우, 명시적으로 원하는 타입의 인스턴스로 만들 것.

let label = "The width is"
let width = 94
let widthLabel = label + String(width)

EXPERIMENT

마지막 줄에서 String 변환하는 부분을 제거해보라. 어떤 error가 발생하는가?

문자열에 값을 포함하는 더 간단한 방법이 있다. 괄호 안에 값을 쓰고 괄호 앞에 백 슬래시 (\)를 쓴다.

let apples = 3
let oranges = 5
let appleSummary = "I have \(apples) apples."
let fruitSummary = "I have \(apples + oranges) pieces of fruit."

EXPERIMENT

문자열에 부동 소수점 계산을 포함시키고 인사말에 다른 사람의 이름을 포함하려면 \()를 사용하라.

여러 줄을 차지하는 문자열에는 세 개의 큰 따옴표 (""")를 사용하라. 닫힌 따옴표의 들여 쓰기와 일치하는 한 각 인용 줄의 시작 부분에 들여 쓰기가 제거된다.

let quotation = """
I said "I have \(apples) apples."
And then I said "I have \(apples + oranges) pieces of fruit."
"""

대괄호 ([])를 사용하여 array와 dictionary를 만들고, index나 key를 괄호에 써서 요소에 접근할 수 있다.

마지막 요소 다음에 쉼표를 사용할 수 있습니다.

var shoppingList = ["catfish", "water", "tulips", "blue paint"]
shoppingList[1] = "bottle of water"
var occupations = [
    "Malcolm": "Captain",
    "Kaylee": "Mechanic",
]
occupations["Jayne"] = "Public Relations"

빈 array나 dictionary를 만들기 위해선 초기화 문법을 이용하라.

let emptyArray = [String]()
let emptyDictionary = [String: Float]()

타입 정보를 추론할 수있는 경우는 빈 array를 []로 쓰고 빈 dictionary는 [:]으로 쓸 수 있다.

예를 들어, 변수에 새 값을 설정하거나 function에 인수를 전달할 수 있다.

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와 let을 사용하여 빈 값을 처리할 수 있다. 이런 값들은 선택 사항으로 표시된다. 옵(optional) 값은 값을 포함하거나 빈 값을 표현하는 nil로 지정하기도 한다.

값의 타입 뒤에 물음표(?)를 쓰면 옵션(optional) 값이라는 것을 나타낸다.

var optionalString: String? = "Hello"
print(optionalString == nil)

var optionalName: String? = "John Appleseed"
var greeting = "Hello!"
if let name = optionalName {
   greeting = "Hello, \(name)"
}

EXPERIMENT

optionalName을 nil로 변경하면 greeting 값은 어떻게 되는가?

optionalName이 nil 인 경우 다른 값을 greeting을 할당하는 else 절을 추가하라.

만약 옵션 (optional) 값이 nil이면, 조건문은 false이고 중괄호 안의 코드는 실행되지 않는다. 반대의 경우 중괄호안에서 사용할 수 있도록 let 뒤에 오는 상수에 값이 할당되는 옵션(optional) 값으로 쓸수있다.

옵션값을 처리하는 또 다른 방법은 ?? 연산자를 사용하여 기본 값을 제공하는 방법이 있다. 만약 옵션 값이 없으면, 그 대신 기본 값이 대신 사용된다.

let nickName: String? = nil
let fullName: String = "John Appleseed"
let informalGreeting = "Hi \(nickName ?? fullName)"

switch문에는 정수 타입 값이나 동등 비교연산 뿐만 아니라 어떤 종류의 데이터이든 사용할 수 있고 다양한 비교 연산자들을 사용할 수 있다.

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: 문을 지워보자. 어떤 에러나는가?

일치하는 switch case문에서 코드가 실행된 후, 프로그램은 switch문을 종료한다.

다음 case문이 실행되지 않기 때문에 명시적으로 각 case의 코드의 끝에 switch에 break를 쓸 필요가 없다.

for-in문을 사용하면 각각 키/값 쌍으로 사용할 수 있는 이름들의 쌍을 이용해 dictionary에 있는 요소들을 반복할 수 있다.

dictionary는 순서가 지정되지 않는 collection이라서 키와 값은 임의의 순서로 반복된다.

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

어떤 숫자가 가장 큰 수로 저장되는지 확인하기 위해 다른 변수를 추가하고, 가장 큰 수로 저장된 숫자가 무엇인지 확인해보라.

while을 사용하면 조건이 바뀔 때까지 코드 블록을 반복 할 수 있다. 반복문이 적어도 한번은 실행될 수 있도록 보장하려면 조건문을 반복문의 끝에 작성할 수도 있습니다.

var n = 2
while n < 100 {
   n *= 2
}
print(n)

var m = 2
repeat {
   m *= 2
} while m < 100
print(m)

..< 인덱스를 사용하여 반복문의 인덱스를 유지할 수 있다.

var total = 0

for i in 0..<4 {
    total += i
}

print(total)

.. <를 사용하여 상위 값을 생략하는 범위를 만들고 ...를 사용하여 두 값을 모두 포함하는 범위를 만든다.


함수(Functions)와 클로져(Closures)

func을 사용하여 함수를 선언할 수 있다. 함수를 호출할 때 괄호 안의 인수(argument) 와 이름을 넣을 수 있다.

->를 사용해서 매개변수의 이름과 분리해서 표기하면 함수 반환 값의 타입을 지정할 수 있다.

func greet(person: String, day: String) -> String {
    return "Hello \(person), today is \(day)."
}

greet(person: "Bob", day: "Tuesday")

EXPERIMENT

day 매개변수를 삭제해보라. 인사말에 오늘의 점심 특선을 포함하는 매개변수를 추가해보자.

기본적으로 함수는 매개변수(parameter) 이름을 인수(argument)의 레이블로 사용한다. 매개 변수 이름 앞에 사용자 정의 인수 레이블을 쓰거나 인수 레이블이 없는 _를 쓰라.

func greet(_ person: String, on day: String) -> String {
    return "Hello \(person), today is \(day)."
}
greet("John", on: "Wednesday")

튜플(tuple)을 사용하여 함수에서 여러 값을 반환하는 것처럼 복합 값을 만들수 있다.

튜플의 요소는 이름이나 번호로 참조 할 수 있다.

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)

함수는 중첩 될 수 있다.

중첩(Nested) 함수는 감싸고 있는 함수에서 선언된 변수에 접근할 수 있습니다. 코드가 길어지고 복잡해지는 함수라면 이를 정리하기 위해 중첩 함수를 사용할 수 있다.

func returnFifteen() -> Int {
    var y = 10
    func add() {
        y += 5
    }
    add()
    return y
}
returnFifteen()

함수는 최상위 클래스(first-class) 타입이다. 어떤 함수가 다른 함수를 반환 값 형태로 반환할 수 있다는 것을 의미한다.

func makeIncrementer() -> ((Int) -> Int) {
    func addOne(number: Int) -> Int {
        return 1 + number
    }
    return addOne
}
var increment = makeIncrementer()
increment(7)

함수는 다른 함수를 인수(arguments)로 arguments를 사용할 수 있다.

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)

사실 함수는 나중에 호출될 수 있는 코드 블록인 클로저의 특별한 경우이다.

클로저 코드는 클로저가 작성되었을 때 범위 안에서 사용할 수 있는 변수 및 함수와 같은 항목에 접근할 수 있다.

즉, 클로저가 실행될때 다른 범위에 있어도 마찬가지다.

numbers.map({ (number: Int) -> Int in
    let result = 3 * number
    return result
})

EXPERIMENT

모든 홀수 값을 반환하는 클로저를 작성해보라.

대리자(delegate)의 콜백과 같이 클로저의 형식을 이미 알고있는 경우 매개 변수의 형식, 반환 형식 또는이 두 가지를 모두 생략하거나 선택적으로 생략 할 수 있습니다.

한 줄 짜리 구문을 가진 클로저라면 구문만 가지고도 반환 타입을 추측할 수 있다.

let mappedNumbers = numbers.map({ number in 3 * number })
print(mappedNumbers)

이름 대신 숫자로 파라미터를 참조 할 수 있다. 이 방법은 매우 짧은 클로저에서 특히 유용하다.

함수에 대한 마지막 인수로 전달 된 클로저는 괄호 바로 뒤에 나타날 수 있다.

클로저가 함수의 유일한 인수(argument)일때 괄호를 전부 생략할 수 있다.

let sortedNumbers = numbers.sorted { $0 > $1 }
print(sortedNumbers)

객체와 클래스

클래스를 만들기 위해서는 클래스 이름과 함께class키워드를 사용하면 된다.

클래스 컨텍스트(context) 내부를 제외하고 클래스 안에 속성을 선언하기 위해서는 상수나 변수를 선언하는 것과 똑같은 방식으로 쓰면 된다. 마찬가지로 메서드와 함수도 선언할 수 있다.

class Shape {
    var numberOfSides = 0
    func simpleDescription() -> String {
        return "A shape with \(numberOfSides) sides."
    }
}

EXPERIMENT

let으로 상수 property하나를 추가하고 하나의 argument를 갖는 다른 메소드를 추가하라.

클래스 이름 뒤에 괄호를 넣어 클래스의 인스턴스를 만든다.

.(dot) 구문을 사용하여 인스턴스의 속성 및 메서드에 접근한다.

var shape = Shape()
shape.numberOfSides = 7
var shapeDescription = shape.simpleDescription()

이 버전의 Shape 클래스에는 인스턴스 작성시 클래스를 설정하는 초기화 프로그램이 있다. 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."
    }
}

name 이라는 프로퍼티(property) 와 name 이름의 인자(argument)를 구분하기 위해서 self 키워드가 어떻게 사용되는지 알아보자.

클래스의 인스턴스를 만들 때 초기화자(initializer)에 인자를 전달하는 방식은 함수에 전달하는 방식과 동일하다. 모든 속성은 numberOfSides 처럼 값을 선언 할 때 혹은 name처럼 클래스를 초기화 할 때 처럼 적어도 둘중에 한가지 방법을 통해 값을 할당해줘야 한다.

객체를 할당 해제하기 전에 일부 정리를 수행해야하는 경우 deinit를 사용하여 deinitializer를 만든다.

서브 클래스는 클래스 이름 다음에 수퍼 클래스 이름을 콜론(:)으로 구분하여 포함한다. 클래스가 표준 루트 클래스를 서브 클래스화할 필요가 없으므로 필요에 따라 수퍼 클래스를 포함하거나 생략 할 수 있다.

하위 클래스에서 상위 클래스에서 구현된 메서드를 오버라이드(override) 하려면 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을 만들어보자. 이 클래스는 이니셜 라이저를 통해 radius와 name을 인자로 받는다. Circle 클래스 안에 area, describe 함수를 구현해보자.

저장되어 있는 간단한 속성 외에도 속성은 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)

perimeter의 setter안에서는 newValue라는 이름이 새로운 값을 나타내고 있다. 명시적으로 set 뒤에 괄호를 이용해 이름을 지정해 줄수도 있다.

EquilateralTriangle 클래스의 초기화(initializer)는 세가지 단계를 거친다.

  1. 하위 클래스에서 선언한 속성(property)의 값을 set한다.

  2. 상위 클래스의 초기화(initializer)를 호출한다.

  3. 상위 클래스에 의해 정의된 속성값을 변경한다. 어떤 메서드나 setter, getter를 사용하지 않고도 가능 하다는 것이 중요한점이다.

속성(property)의 값을 계산할 필요는 없지만 새로운 값을 할당하기 전이나 후에 수행해야할 코드가 있다면 willSet, didSet을 사용할 수 있다.

예를 들어 아래 코드에 나오는 클래스에서는 삼각형의 빗면의 길이가 사각형의 옆면의 길이와 항상 동일하다는 것을 보장한다.

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)

옵션 값을 사용할 때는 메서드나 속성(property), 서브스크립트(subscripting) 앞에 ?를 쓸 수 있다. 만약 ? 앞에 값이 nil 이라면 ? 이후에 나오는 모든 것은 무시되고 표현의 값들은 nil을 갖는다.

반면에 값이 있는 경우라면 ? 이후의 모든 그 값을 기준으로 동작한다. 양쪽 경우 모두 옵션 값으로 사용된다.

let optionalSquare: Square? = Square(sideLength: 2.5, name: "optional square")
let sideLength = optionalSquare?.sideLength

열거형(Enumerations)과 구조(Structures)

열거형을 만들기 위해서 enum키워드를 사용하면 된다.

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 값의 원본 값을 비교하는 함수를 만들어보자.

기본적으로, Swift 는 0에서 시작하여 1씩 증가하는 원시 값(raw value)을 할당하지만 만약 명시적으로 특정 값을 지정하여 변경할 수도 있다. 위의 예에서 Ace에는 원시값 1이 명시적으로 지정되고 나머지 원시 값은 순서대로 지정된다. 열거형의 원시 유형(raw type)으로 문자열 또는 부동 소수점 숫자를 사용할 수도 있다. rawValue 속성을 사용하여 열거형 case의 원시값에 접근할 수 있다

init?(rawValue:) 초기화를 이사용하여 원시 값의 열거형의 인스턴스를 만든다. 원시값과 일치하는 열거형을 반환하거나 일치하는 Rank가 없는 경우 nil을 반환한다.

if let convertedRank = Rank(rawValue: 3) {
    let threeDescription = convertedRank.simpleDescription()
}

열거형의 case 값은 실제 값이다. (not just another way of writing their raw values.)

실제로 의미있는 원시값이 아니면, 굳이 제공할 필요 없다.

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에 spades와 clubs일때 "blak", hearts와 diamonds일때 "red"를 리턴하는 color() 메소드를 추가해보자.

Another choice for enumeration cases is to have values associated with the case—these values are determined when you make the instance, and they can be different for each instance of an enumeration case. You can think of the associated values as behaving like stored properties of the enumeration case instance.

예를 들면, 일출, 일몰 시간을 서버에 요청한다고 가정해보자. 서버는 두 시간 모두를 응답하거나 에러 정보를 응답할 수도 있다.

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에 세번째 경우를 추가하고 스위치문에도 추가해보자.

구조체를 만들기 위해 struct 키워드를 사용하자. 구조체는 메소드와 초기화를 포함하여 클래스와 동일한 많은 동작(behaviors)를 지원한다.

구조체와 클래스의 가장 중요한 차이점 중 하나는 구조체는 코드에서 전달될 때 항상 복사되지만 클래스는 참조에 의해 전달된다

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

Add a method to Card that creates a full deck of cards, with one card of each combination of rank and suit.


프로토콜(Protocols)과 확장()Extensions

protocol을 선언하려면 protocol 키워드를 사용하자.

protocol ExampleProtocol {
    var simpleDescription: String { get }
    mutating func adjust()
}

클래스, 열거형, 구조체 모두에 프로토콜을 사용할 수 있다.

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

프로토콜을 사용하는 열거형을 만들어보자.

구조체를 수정하기 위해 사용되는 메서드를 표기하기 위해 SimpleStructure 선언부에 사용되는 mutating 키워드를 살펴보자.

SimpleClass의 선언은 클래스의 메소드가 항상 클래스를 수정할 수 있기 때문에 변형 된 것으로 표시된 메서드가 필요하지 않는다.

extension 키워드를 사용해서 기존의 타입들에 새로운 메서드나 속성들을 비교하기 위한 기능들을 추가할 수 있다. 타입이 선언된 곳 어디서든 혹은 라이브러리나 프레임워크에서 불러온 타입들에 extension 키워드를 사용해 프로토콜을 적용할 수 있다.

extension Int: ExampleProtocol {
    var simpleDescription: String {
        return "The number \(self)"
    }
    mutating func adjust() {
        self += 42
    }
}
print(7.simpleDescription)

EXPERIMENT

extension을 사용해 Double 타입에 absoluteValue 속성을 추가해보자.

프로토콜 이름은 다른 알려진 변수들 처럼 지정할 수 있다. 예를 들면 객체들의 collection을 만들 때, 모든 객체는 다른 타입을 가지지만 하나의 프로토콜을 따른다. 프로토콜 타입인 값들을 가지고 작업할 때 프로토콜 외부에서 메서드를 정의하는 것은 불가능 하다

let protocolValue: ExampleProtocol = a
print(protocolValue.simpleDescription)
// print(protocolValue.anotherProperty)  // Uncomment to see the error

protocalValue 변수가 실행시 SimpleClass 타입이어도 컴파일러는 주어진 ExampleProtocal 타입으로 취급한다. 이것은 프로토콜 관습 외에도 클래스에서 구현된 메서드나 속성에 실수로 접근할 수는 없다는 것을 의미한다.


에러처리(Error Handling)

enum PrinterError: Error {
    case outOfPaper
    case noToner
    case onFire
}

throw를 사용하여 error를 발생시키고, error가 발행살 수 있는 함수를 표시하기 위해 throws를 한다.

만약 function에서 error를 던지면 function은 즉시 반환하고 function을 호출한 코드가 error를 처리한다.

func send(job: Int, toPrinter printerName: String) throws -> String {
    if printerName == "Never Has Toner" {
        throw PrinterError.noToner
    }
    return "Job sent"
}

오류를 처리하는 여러 가지 방법이 있다. 한 가지 방법은 do-catch를 사용하는 것이다. do 블록 안에서 try를 작성하여 error를 던질 수있는 코드를 표시한다. catch 블록 안에 다른 이름을 지정하지 않으면 에러는 자동적으로 주어진 이름으로 된다.

do {
    let printerResponse = try send(job: 1040, toPrinter: "Bi Sheng")
    print(printerResponse)
} catch {
    print(error)
}

EXPERIMENT

printer 이름을 "Never Has Toner"으로 바뀌서 send(job:toPrinter:) function이 error를 던지게 만들어 보자.

특정 오류를 처리하는 여러 catch 블록을 제공 할 수 있다. switch를 사용하는 경우처럼 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 블록 안에 error를 던지기위한 코드를 추가하자. error가 첫 번째 catch 블록에서 처리되도록 하려면 어떤 종류의 error가 발생해야할까? 두 번째와 세 번째 블록은 어떨까?

error를 처리하는 또 다른 방법은 결과를 선택(optional) 사항으로 변환하기 위해 try? 를 사용하는 것이다. 함수가 error를 throw하면 특정 error는 무시되고 결과는 nil이다. 그렇지 않으면 결과는 함수가 리턴 한 값을 포함하는 optional이다.

let printerSuccess = try? send(job: 1884, toPrinter: "Mergenthaler")
let printerFailure = try? send(job: 1885, toPrinter: "Never Has Toner")

defer를 사용하면 함수가 반환되기 직전에 함수의 다른 모든 코드 뒤에 실행되는 코드 블록을 작성할 수 있다. 함수가 오류를 throw하는지 여부에 관계없이 코드가 실행된다. 서로 다른 시간에 실행해야하는 경우에도 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)

제네릭

제네릭 함수나 타입을 만들려면 꺾쇠안에 이름을 쓰면 된다.

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)

클래스, 열거형, 구조체와 마찬가지로 함수나 메서드를 제네릭 형태로 만들 수 있다.

// 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 으로 쓰는 것은 동일하다.

results matching ""

    No results matching ""