야곰의 스위프트 프로그래밍 - 스위프트 고급

스위프트 고급

타입 중첩

타입 내부에 타입을 정의하고 구현할 수 있으며, 이렇게 구현된 타입을 중첩 타입이라고 함

  • 클래스나 구조체 내부에서 자신의 역할을 구분짓고 자신의 내부에서만 사용 가능하게 할 수 있음
  • 득정 데이터 타입들을 하나의 클래스나 구조체에 구현하여 외부와의 혼선을 피할 수 있음

타입의 목적성을 명확히 하는 데 도움을 줄 수 있음

패턴

이러이러한 것을 표현하고 싶다면 이러이러한 패턴을 통해 표현하면 된다.

대부분의 패턴은 switch, if, guard, for 등의 키워드가 두 개 이상 합을 이뤄 동작하며, switch 구문에서 강력한 힘을 발휘함

case를 사용하는 패턴은 case 뒤에 패턴이 위치함

  • 값을 해체(추출)하거나 무시하는 패턴
    • 와일드카드 / 식별자 / 값 바인딩 / 튜플 패턴
  • 패턴 매칭을 위한 패턴
    • 열거형 케이스 / 옵셔널 / 표현 / 타입캐스팅 패턴

와일드카드 패턴

와일드카드 식별자 _를 사용하여 이 자리에 무엇이 오든 상관 없음을 나타냄. 와일드카드 식별자가 위치한 곳의 값은 무시됨

for _ in 0...2 {
print("HO")
}
let info = ("presto", 24, 0)
switch info {
case (_, 24, _): print(info)
case (_, _, _): print("~~")
}

식별자 패턴

변수 또는 상수의 이름에 알맞는 값을 어떤 값과 매치시키는 패턴

let value: Int = 42

value의 타입인 Int와 할당하려는 값인 42의 타입이 매치된다면 value42라는 값의 식별자가 되므로 식별자 패턴이 성립함

42라는 값을 value라는 상수로 식별함

값 바인딩 패턴의 일종

값 바인딩 패턴

변수 또는 상수의 이름에 매치된 값을 바인딩

식별자 패턴은 매칭되는 값을 새로운 이름의 변수 또는 상수에 바인딩하는 것

  • 튜플의 요소를 해체하여 그에 대응하는 식별자 패턴에 각각의 요소를 바인딩
let info = ("presto", 24, true)
switch info {
// 다음과 같은 형식으로 튜플의 요소을 해체하여 대응하는 식별자 패턴에 각 요소를 바인딩할 수 있음
case let (nickname, age, isMale): print(nickname, age, isMale)
case (let nickname, let age, let isMale): print(nickname, age, isMale)
case (let nickname, _, let isMale): print(nickname, isMale)
}

튜플 패턴

소괄호 내에 쉼표로 분리하는 리스트. 그에 상응하는 튜플 타입과 값을 매치함

let (x, y): (Int, Int) = (2, 3)
print(x, y)
let name = "presto"
let age = 24
let isMale = true
switch (name, age, isMale) {
// name, age isMale이 이에 상응하는 튜플 타입과 값이 매치됨
case ("presto", _, _): print(1)
case (_, _, true): print(2)
}
let points: [(Int, Int)] = [(0, 0), (0, 1)]
// points의 요소와 (x, y)가 매치됨
for (x, y) in points {
print(x, y)
}

열거형 케이스 패턴

값을 열거형 타입의 case와 매치시킴

switch 구문의 case 레이블 / if, while, guard, for-in 구문의 case 조건에서 볼 수 있음

연관 값이 있는 열거형 케이스와 매치하려고 한다면 열거형 케이스 패턴에는 반드시 튜플 패턴이 함께해야 함

let someValue = 30
// if (0...100).contains(someValue)
if case 0...100 = someValue {
print("0 <= \(someValue) <= 100")
}
let anotherValue = "ABC"
// if "ABC" == anotherValue
if case "ABC" = anotherValue {
print(anotherValue)
}

옵셔널 패턴

옵셔널Optional 또는 암시적 추출 옵셔널ImplicitlyUnwrappedOptional 열거형에 감싸져 있는 값을 매치시킬 때 사용

식별자 패턴 뒤에 물음표를 넣어 표기하며 열거형 케이스 패턴과 동일한 위치에 자리함

  • 옵셔널은 열거형으로 구현되어 있음
public enum Optional<Wrapped>: ExpressibleByNilLiteral {
case none
case some(Wrapped)
}
let optional: Int? = 100
// if let value = optional
if case .some(let value) = optional {
print(value) // 100
}
// if let value = optional
if case let value? = optional {
print(value) // 100
}
func hasValue(_ optional: Int?) -> Bool {
// guard case let value? = optional else { return false }
// guard let value = optional else { return false }
guard case .some(let value) = optional else { return false }
return true
}
let numbers = [1, nil, 4, 6, nil]
// 옵셔널 저장하는 배열의 `for-in` 구문을 통한 순환에서 `nil`이 아닌 값을 찾는 데 유용하게 사용됨
for case let number? in numbers {
print(number, terminator: " ")
}
// 1 4 6

타입캐스팅 패턴

타입캐스팅을 하거나 타입을 매치시킴

is 패턴은 is 타입_이름과 같이 작성하여 값의 타입이 is 우측에 작성된 타입 또는 그 타입의 자식클래스 타입이면 값과 매치시킴

as 패턴은 패턴 as 타입_이름과 같이 작성하여 값의 타입이 as 우측에 작성된 타입 또는 그 타입의 자식클래스 타입이면 값과 매치시키고, 매치된 값의 타입은 as 패턴이 원하는 타입으로 캐스팅됨

let value: Any = 100
switch value {
case is String: print("It is String")
case let value as Int: print(value)
default: break
}
// 100

표현 패턴

표현식의 값을 평가한 결과를 이용

switch 구문의 case 레이블에서만 사용 가능

Swift 표준 라이브러리의 패턴 연산자인 ~= 연산자의 반환값이 true이면 매치시킴

  • 같은 타입의 두 값을 비교할 때 ~= 연산자는 == 연산자를 사용함

연산자를 중복 정의하거나 재정의, 또는 자신이 만든 타입에 ~= 연산자를 구현해주어 자신이 원하는대로 패턴을 완성시킬 수 있음

~= 연산자 재정의 또는 중복 정의하기

  • 첫 번째 인자에 case 레이블에 작성될 패턴이 위치함

  • 두 번째 인자에 switch문의 조건에 작성될 값이 위치함

  • func ~=(pattern: String, value: Person) -> Bool {
        return pattern == value.name
    }
    
    <!--8-->
    

프로토콜 익스텐션에 where절을 사용하여 해당 익스텐션이 특정 프로토콜을 준수하는 타입에만 적용될 수 있도록 제약을 줄 수 있음

  • // SelfPrintable 프로토콜을 준수하는 타입 중 Container 프로토콜도 준수하는 타입에만 해당 익스텐션이 적용
    extension SelfPrintable where Self: Container
    
    

    - 여러 프로토콜을 제시하려면 쉼표로 구분해줌

    타입 매개변수와 연관 타입의 제약을 추가하는 데 `where`절을 사용할 수 있음

    ```swift
    func doubled<T>(value: T) -> T where T: BinaryInteger {
    return value * 2
    }
    // 위의 표현과 아래의 표현 같음
    func doubled<T: BinaryInteger>(value: T) -> T {
    return value * 2
    }
func compareTwoSequences<S1, S2>(a: S1, b: S2) where S1: Sequence, S2: Sequence, S1.SubSequence: Equatable, S1.SubSequence == S2.SubSequence { ... }
// 위의 표현과 아래의 표현 같음
func compareTwoSequences<S1: Sequence, S2: Sequence>(a: S1, b: S2) where S1.SubSequence: Equatable, S1.SubSequence == S2.Iterator.Element { ... }
protocol Container {
associatedtype ItemType where ItemType == BinaryInteger
...
}
// 위의 표현과 아래의 표현 같음
protocol Container where ItemType == BinaryInteger {
associatedtype ItemType
...
}

where절을 다른 패턴과 조합하여 원하는 추가 요구사항을 자유롭게 더할 수 있음. 익스텐션과 제네릭에 사용하여 프로토콜 또는 타입에 대한 제약을 추가할 수 있음.

조건문이나 논리 연산으로 구현한 코드보다 훨씬 더 명확하고 간편하게 사용할 수 있다.

ARC

Automatic Reference Counting

Swift는 프로그램의 메모리 사용을 관리하기 위해 ARC라는 메모리 관리 기법을 사용함

참조 타입인 클래스의 인스턴스에만 적용되는 개념

ARC란

GC와의 차이

  • 컴파일 시 참조 카운팅
    • 컴파일 시 인스턴스의 해제 시점이 결정되어 인스턴스가 언제 메모리에서 해제될지 예측할 수 있음
    • 메모리 관리를 위한 추가적인 시스템 자원이 불필요함

ARC의 작동 규칙을 모르고 사용하면 인스턴스가 메모리에서 영원히 해제되지 않을 가능성이 있음.

  • 클래스의 인스턴스를 생성할 때마다 ARC는 그 인스턴스에 대한 정보를 저장하기 위한 메모리 공간을 따로 또 할당함
  • 그 공간에는 인스턴스의 타입 정보와 함께 그 인스턴스와 관련된 저장 프로퍼티의 값 등을 저장함
  • 인스턴스가 더이상 필요 없는 상태가 되면 인스턴스가 차지하던 메모리 공간을 다른 용도로 활용할 수 있도록 ARC가 메모리에서 인스턴스를 없앰
  • 인스턴스가 지속해서 필요한 상황에서 ARC는 인스턴스가 메모리에서 해제되지 않도록 인스턴스 참조 여부를 계속 추적함
    • 다른 인스턴스의 프로퍼티나 변수, 상수 등이 어느 한 곳에서 인스턴스를 참조한다면 ARC가 해당 인스턴스를 해제하지 않고 유지해야 하는 명분이 됨

강한참조

인스턴스가 계속해서 메모리에 남아있어야 하는 명분을 만들어 주는 것

인스턴스는 참조 횟수가 0이 되는 순간 메모리에서 해제됨

강한참조를 사용하여 인스턴스를 다른 인스턴스의 프로퍼티나 변수, 상수 등에 할당하면 참조 횟수가 1 증가함

강한참조를 사용하는 프로퍼티, 변수, 상수 등에 nil을 할당해주면 원래 자신에게 할당되어 있던 인스턴스의 참조 횟수가 1 감소함

기본적으로 강한참조함

강한참조 지역변수(상수)가 사용된 범위의 코드 실행이 종료되면 그 지역변수(상수)가 참조하던 인스턴스의 참조 횟수가 1 감소함

강한참조 순환 문제

인스턴스가 서로가 서로를 강한참조함. 변수가 참조하던 클래스의 인스턴스에 접근할 방법이 없으나 인스턴스는 해제되지 않은 채 메모리에 존재하여 메모리 누수가 발생함

약한참조

자신이 참조하는 인스턴스의 참조 횟수를 증가시키지 않음

weak 키워드를 참조 타입의 프로퍼티나 변수의 선언 앞에 명시하면 그 프로퍼티나 변수는 자신이 참조하는 인스턴스를 약한참조함

자신이 참조하던 인스턴스가 메모리에서 해제된다면 nil이 할당될 수 있으므로 옵셔널 변수만 약한참조를 할 수 있음

미소유참조

약한참조와 마찬가지로 인스턴스의 참조 횟수를 증가시키지 않음

unowned 키워드를 참조 타입의 프로퍼티나 변수의 선언 앞에 명시하면 그 프로퍼티나 변수는 자신이 참조하는 인스턴스를 미소유참조함

자신이 참조하는 인스턴스가 항상 메모리에 존재할 것이라는 전제를 기반으로 동작하며, 자신이 참조하는 인스턴스가 메모리에서 해제되더라도 nil을 할당해주지 않음. 그러므로 옵셔널 변수가 아니어도 미소유참조를 사용할 수 있음

미소유참조를 하면서 메모리에서 해제된 인스턴스에 접근하려 할 때 런타임 에러가 발생함

클로저의 강한참조 순환

클로저가 인스턴스의 프로퍼티일 때나, 클로저의 값 획득 특성 때문에 발생함

클로저를 클래스의 인스턴스 프로퍼티로 할당하면 클로저의 참조가 할당되고, 이 때 참조 타입과 참조 타입이 서로 강한참조를 하기 때문에 강한참조 순환 문제가 발생함

  • 클로저, 클래스는 참조 타입

클로저의 획득 목록을 통해 해결 가능

클로저는 자신이 호출되면 언제든지 자신 내부의 참조들을 사용할 수 있도록 참조 횟수를 증가시켜 메모리에서 해제되는 것을 방지함. 이 때 자신을 프로퍼티로 갖는 인스턴스의 참조 횟수도 증가시킴. 이렇게 강한참조 순환이 발생함

획득목록

클로저 내부에서 참조 타입을 획득하는 규칙을 제시해줄 수 있는 기능

때에 따라서, 혹은 각 관계에 따라서 참조 방식을 클로저에 제안할 수 있음

클로저 내부의 매개변수 목록 이전 위치에, 참조 방식과 참조할 대상을 대괄호로 둘러싼 목록 형식으로 작성. 획득목록 뒤에는 in 키워드를 명시

획득목록에 명시한 요소가 참조 타입이 아니라면 해당 요소들은 클로저가 생성될 때 초기화됨

var a = 0
var b = 0
let closure = { [a] in
// a 변수는 클로저가 생성됨과 동시에 획득목록 내에서 다시 a라는 이름의 상수로 초기화된 것
print(a, b)
b = 20
}
a = 10
b = 10
closure() // 0 10
print(b) // 20

획득목록에 명시한 요소가 참조 타입이라면 일반적으로 기대하는 것처럼 동작함. 참조 타입이기 때문.

하지만 참조 방식 (강한획득 / 약한획득 / 미소유획득)을 명시할 수 있음

class Class {
var value: Int = 0
}
var x: Class? = Class()
var y = Class()
let closure = { [weak x, unowned y] i
// x를 약한참조, y를 미소유참조
print(x?.value, y.value)
}
x = nil
y.value = 10
closure() // nil 10
// closure 호출 시점에 x는 메모리에서 해제됨

일반적으로 약한참조를 사용함. 미소유참조는 강제 옵셔널 래핑같은 느낌

이처럼 클로저의 획득 특성 때문에 클로저가 프로퍼티로 사용될 경우 발생할 수 있는 강한참조 문제는 클로저의 획득목록을 통해 해결할 수 있음

오류처리

오류처리란

프로그램이 오류를 일으켰을 때 이것을 감지하고 회복시키는 일련의 과정

오류의 표현

Error 프로토콜을 준수하는 타입의 값을 통해 표현됨

오류를 표현하기 위한 타입 (주로 열거형)은 이 프로토콜을 채택함

  • 연관 값을 통해 오류에 관한 부가 정보를 제공할 수 있음

미리 정의한 오류가 발생하는 경우에 throw 구문을 사용

오류 포착 및 처리

오류를 던짐 / 던져진 오류를 처리하기 위한 코드 작성 필요

Swift에서 오류를 처리하기 위한 방법들

  • 함수에서 발생한 오류를 해당 함수를 호출한 코드에 알리기
  • do-catch 구문을 이용하여 오류 처리하기
  • 옵셔널 값으로 오류 처리하기
  • 오류가 발생하지 않을 것이라고 확신하기

함수에서 발생한 오류 알리기

함수에서 발생한 오류를 해당 함수를 호출한 코드에 알리기

try 키워드로 던져진 오류를 받을 수 있음 (try / try? / try!)

함수, 메소드, 이니셜라이저의 매개변수 목록 뒤에 throws 키워드를 사용하여 오류를 던질 수 있음. 이러한 함수는 호출했을 때 동작 도중 오류가 발생하면 자신을 호출한 코드에 오류를 던져서 오류를 알릴 수 있음

func throwError() throws -> String { ... }
  • throws 함수나 메소드는 같은 이름의 throws가 명시되지 않은 함수나 메소드와 구분됨
  • throws를 포함한 함수, 메소드, 이니셜라이저는 일반 함수, 메소드, 이니셜라이저로 재정의할 수 없음
    • 일반 함수, 메소드, 이니셜라이저는 throws를 포험한 함수, 메소드, 이니셜라이저로 재정의 가능

throws 함수를 호출하는 함수도 throws를 명시해 주어야 함

try 키워드 이후 오류를 던질 수 있는 함수, 메소드 또는 이니셜라이저의 호출 구문을 작성

오류를 받은 코드가 적절히 오류를 처리해주지 않는다면 오류 발생 이후 코드는 작동되지 않음

do-catch 구문을 이용하여 오류처리

do {
try 오류_발생_가능코드
오류가_발생하지_않으면_실행할_코드
} catch 오류_패턴_1 {
처리_코드
} catch 오류_패턴_2 where 추가_조건 {
처리_코드
}

do 절 내부의 코드에서 오류를 던지면 catch절에서 오류를 받아 적절하게 처리함

catch 뒤에 오류의 종류를 명시하지 않고 코드 블록을 생성하면 블록 내부에 암시적으로 error라는 이름의 지역 상수가 오류의 내용으로 들어옴

옵셔널 값으로 오류처리

try? 표현을 통해 동작하더 코드가 오류를 던지면 그 코드의 반환값은 nil이 됨

이를 옵셔널 바인딩과 결합하여 오류를 처리할 수도 있음

오류가 발생하지 않을 것이라고 확신하는 방법

try! 표현을 사용. 코드가 오류를 던지면 런타임 에러 발생

다시던지기

rethrows 키워드를 사용하여 함수나 메소드는 자신의 매개변수로 전달받은 함수가 오류를 던진다는 것을 나타낼 수 있음

최소 하나 이상의 오류 발생 가능한 함수를 매개변수로 전달받아야 함

enum SomeError: Error {
case someError
}
func throwError() throws {
throw SomeError.someError
}
func function(callback: () throws -> Void) rethrows {
try callback() // someError
}
do {
try function(callback: throwError)
} catch {
print(error)
}

다시던지기 함수 또는 메소드는 기본적으로 스스로 오류를 던지지 못함

  • 자신 내부에 직접적으로 throw 구문을 사용할 수 없음
  • catch절 내부에서 throw 구문을 작성할 수 있음
    • 다시던지기 함수의 매개변수로 전달받은 오류던지기 함수만 호출하고 결과로 던져진 오류만 제어 가능

부모클래스의 다시던지기 메소드는 자식클래스에서 던지기 메소드로 재정의할 수 없음

부모클래스의 던지기 메소드는 자식클래스에서 다시던지기 메소드로 재정의할 수 있음

프로토콜 요구사항 중 다시던지기 메소드가 있다면, 던지기 메소드를 구현하는 것으로 요구사항을 충족시킬 수 없음

프로토콜 요구사항 중 던지기 메소드가 있다면, 다시던지기 메소드를 구현하는 것으로 요구사항을 충족시킬 수 있음

후처리 defer

defer 구문을 사용하여 현재 코드 블록을 나가기 전에 꼭 실행해야 하는 코드를 작성할 수 있음

  • 어떠한 방식으로 코드 블록을 빠져나가든지 defer 구문은 무조건 실행됨

오류처리 상황 뿐만 아니라 모든 경우의 코드 블록 어디에서든 사용 가능

파일 입출력시 defer 구문에 파일을 닫는 코드를 작성해 두면 코드의 양을 줄이고 프로그래머의 실수를 방지할 수 있음

defer 구문 내부에 break, return 같은 구문을 빠져나갈 수 있는 코드 또는 오류를 던지는 코드를 작성할 수 없음

여러 개의 defer 구문이 하나의 블록 내부에 속해 있다면 맨 마지막에 작성된 구문부터 역순으로 실행됨

func function() {
print(1)
defer {
print(2)
}
do {
defer {
print(3)
}
print(4)
}
defer {
print(5)
}
print(6)
}
function()
// 1
// 4
// 3
// 6
// 5
// 2