오늘한 일

  • Swift 문법 학습
    • Protocols
    • Extensions
    • Closures
  • 특강 수강 (Zedd - 새로운 지식을 나의 지식으로 만드는 방법)

학습 내용

Protocols

프로토콜은 기능이나 특정 작업에 적합한 메서드, 프로퍼티 및 기타 요건을 정의하는 청사진입니다. 클래스, 구조체 또는 열거형에 채택되어 실제 구현을 위한 요구사항 (자격 요건)을 제공합니다. 타입이 프로토콜의 요구사항을 충족시키는 경우 프로토콜을 준수한다라고 표현합니다. 프로토콜은 자격요건만을 명시하는 것이 원칙이기 때문에 구체적인 구현은 프로토콜이 아니라 프로토콜을 채택한 타입, 프로토콜을 채택한 타입의 익스텐션 또는 해당 프로토콜의 익스텐션에서 수행해야 합니다. 프로토콜의 익스텐션을 통해 프로토콜이 요구하는 기능을 구현한 경우를 프로토콜 초기 구현이라 표현합니다.

Protocol Syntax

// 프로토콜 선언 방식
protocol SomeProtocol {
    // protocol definition goes here
}

// 프로토콜 채택 방법
truct SomeStructure: FirstProtocol, AnotherProtocol {
    // structure definition goes here
}

// 클래스는 상속할 부모 클래스를 프로토콜보다 먼저 작성합니다.
class SomeClass: SomeSuperclass, FirstProtocol, AnotherProtocol {
    // class definition goes here
}

추가 내용은 포스팅으로 작성 중이며 완성 시 링크를 통해 내용 대체할 예정입니다.

Extensions

기존 클래스, 구조체, 열거형 또는 프로토콜과 같은 타입에 새로운 기능을 추가하도록 도와주는 문법입니다. Retroactive modeling으로 알려진 원천 소스코드에 접근할 수 없는 경우에도 사용이 가능하며, Objective-C의 categories와 유사합니다. Swift의 Extension을 통해 아래를 수행할 수 있습니다.

  • 연산 인스턴스 프로퍼티 및 연산 타입 프로퍼티 추가
  • 인스턴스 메서드 및 타입 메서드 정의
  • 신규 이니셜라이저 제공
  • 서브스크립트 정의
  • 신규 중첩 타입 (nested type) 정의 및 사용
  • 기존 타입이 프로토콜을 준수하도록 지원

Note: extension은 타입에 신규 기능을 추가할 수 있다는 면에서 클래스의 상속과 유사하다고 여겨질 수 있으나, 기능을 재정의(override)할 수는 없습니다.

Extension의 선언과 프로토콜 채택

extension 키워드 작성 후 공백을 삽입하고 여타 타입을 선언할 때와 마찬가지로 UpperCamelCase로 타입 이름을 설정합니다. 채택하고자 하는 프로토콜은 콜론(:) 작성 후 공백을 삽입하고 채택할 프로토콜의 이름을 작성합니다. Extension은 class의 상속과 달리 기존 타입에 정의된 메서드를 재정의할 수 없습니다.

extension SomeType: SomeProtocol, AnotherProtocol {
    // new functionality to add to SomeType goes here
    // implementation of protocol requirements goes here
}

Note:

  • 기존 타입에 extension을 통해 신규 기능을 추가한 경우, extension이 선언되기 이전에 생성된 인스턴스에서도 신규 기능 사용이 가능합니다.
  • Extension을 통해 기존 타입에 연산 프로퍼티를 추가할 수는 있지만, 저장프로퍼티 또는 프로퍼티 감시자는 추가할 수 없습니다.
  • Convinience Initializer (보조 또는 편의 이니셜라이저) 추가는 가능하지만 Designated Initializer (지정 이니셜라이저)를 추가할 수 없습니다.

추가 내용은 포스팅으로 작성 중이며 완성 시 링크를 통해 내용 대체할 예정입니다.

Closures

Closures는 코드 블럭으로 C와 Objective-C의 blocks와 다른 언어의 lambdas와 유사합니다. 클로저는 정의한 곳의 문맥에 따라 어떠한 상수나 변수를 캡쳐 (capture)해 저장할 수 있습니다. Swift는 이러한 캡쳐에 따른 모든 메모리 관리를 알아서 수행해줍니다.

전역 함수와 중첩 함수는 클로저의 특수한 사례입니다. 클로저는 아래 세 가지 형태 중 하나의 형태를 띱니다.

  • 전역 함수: 이름은 있지만 어떠한 값도 캡쳐하지 않는 클로저
  • 중첩 함수: 이름이 있고 소속된 함수에서 값도 캡쳐하는 클로저
  • 주위 문맥을 통해 값을 캡쳐할 수 있는 가벼운 문법으로 작성된 이름 없는 클로저

Swift의 클로저는 일반적인 상황에서 간결한 스타일로 최적화된 표현이 가능합니다. 최적화는 아래와 같은 요소가 가능함을 의미합니다.

  • 문맥에서 매개변수와 반환값의 타입 추론
  • 단일 표현(single-expression) 클로저의 암시적 반환
  • 전달인자 이름 단축 표현
  • 후행 클로저 (Trailing Closures) 문법

클로저 표현 문법

Swift의 클로저 문법은 일반적으로 아래의 형태를 띱니다.

{ (parameters) -> return type in
    statements
}

매개변수 (parameters), 반환 타입 (return type)과 매개변수를 통해 처리할 내용을 기술하는 statements로 구성되어 있습니다. 매개변수는 in-out 매개변수를 가질 수 있지만, 기본값은 지정할 수 없습니다. 가변 매개변수 (Variadic parameters)를 명명하면 가변 매개변수도 사용할 수 있습니다. 튜플 (Tuples) 또한 매개변수 타입과 반환 타입 표현에 사용할 수 있습니다.

Swift Array 타입의 내장 메서드인 sorted(by:) 메서드를 통해 클로저 표현에 대해 자세히 알아보겠습니다. 먼저 예시에서 사용할 배열을 선언해주겠습니다.

let names = ["Chris", "Alex", "Ewa", "Barry", "Daniella"]

sorted(by:) 메서드는 정렬을 위해 클로저를 요구하는데, 위의 예시의 배열은 [String] 타입이므로 (String, String) -> Bool 타입의 클로저를 요구합니다. 타입의 형태가 익숙하지 않을 수 있는데, 상기 기술한 바와 같이 함수 또한 클로저이므로 아래와 같이 함수를 정의하여 해당 클로저에 사용할 수 있습니다.

func backward(_ s1: String, _ s2: String) -> Bool {
    return s1 > s2
}
var reversedNames = names.sorted(by: backward)
// reversedNames is equal to ["Ewa", "Daniella", "Chris", "Barry", "Alex"]

클로저를 통해 동일한 내용을 작성하면 아래와 같이 표현할 수 있습니다.

reversedNames = names.sorted(by: { (s1: String, s2: String) -> Bool in
    return s1 > s2
})

클로저의 본문 (body)이 짧으므로 아래와 같이 한 줄로 작성할 수 있습니다.

reversedNames = names.sorted(by: { (s1: String, s2: String) -> Bool in return s1 > s2 } )

문맥에서 매개변수와 반환값의 타입 추론

sorted(by:) 메서드에 분류를 위한 클로저를 인자로 전달했으므로 Swift는 매개변수의 타입과 반환값의 타입을 추론할 수 있습니다. 위의 예시에서 [String] 타입이 sorted(by:) 메서드를 호출하므로 클로저의 전달인자의 타입은 (String, String) -> Bool임을 알 수 있습니다. 이는 아래와 같이 클로저에서 해당 부분이 생략될 수 있음을 의미합니다.

reversedNames = names.sorted(by: { s1, s2 in return s1 > s2 } )

단일 표현(single-expression) 클로저의 암시적 반환

단일 표현 클로저는 return 키워드를 생략하여도 단일 표현 (예시에서의 s1 > s2)에 대한 결과를 암시적으로 반환합니다. 결과적으로 위의 예시를 아래와 같이 표현할 수 있습니다. 상기 타입 추론에서 언급했듯이 타입을 명시하지 않아도 반환값에 대한 타입 추론이 가능하므로 return 키워드를 생략하여도 반환값이 있음을 알 수 있기에 표현에 모호한 점이 없습니다.

reversedNames = names.sorted(by: { s1, s2 in s1 > s2 } )

전달인자 이름 단축 표현

Swift는 클로저의 전달인자를 나타낼 수 있는 이름으로 $0, $1, $2 등과 같은 단축형 표현을 제공합니다. 그러므로 지금까지의 예시를 아래와 같이 나타낼 수 있습니다.

reversedNames = names.sorted(by: { $0 > $1 } )

위의 예시에서 $0$1클로저의 첫번째와 두번째 String 전달인자를 나타냅니다. 실제 정의된 배열 요소의 인덱스와 관계가 없습니다. 이 개념이 혼동된다면 지금까지 왜 이러한 단축 표현이 가능했고, $0, $1이 단축 표현이 사용되지 않은 클로저 표현에서 어떤 요소를 나타내는지 다시 천천히 파악해보면 좋을 것입니다.

연산자 메서드 (Operator Methods)

Swift의 String 타입이 지원하는 string-specific implementation을 이용하면 > 또는 <와 같은 부등호 연산자를 통해 두 개의 String을 비교하여 Bool 값을 반환시킬 수 있으므로 아래와 같은 표현이 가능합니다.

reversedNames = names.sorted(by: >)

후행 클로저 (Trailing Closures)

함수의 마지막 인자로 클로저를 전달해야 하고, 클로저 표현이 길다면 후행 클로저로 작성하는 것이 좋을 수 있습니다. 후행 클로저를 함수 호출을 위한 소괄호 (parentheses) 다음에 작성하더라도 후행 클로저가 함수의 인자라고 인식합니다. 아래 예시를 통해 설명한 내용을 이해할 수 있습니다.

func someFunctionThatTakesAClosure(closure: () -> Void) {
    // function body goes here
}

// Here's how you call this function without using a trailing closure:

someFunctionThatTakesAClosure(closure: {
    // closure's body goes here
})

// Here's how you call this function with a trailing closure instead:

someFunctionThatTakesAClosure() {
    // trailing closure's body goes here
}

축약 표현을 배울 때 사용했던 예시를 아래와 같이 후행 클로저로 작성할 수 있습니다.

reversedNames = names.sorted() { $0 > $1 }

만약 클로저가 함수나 메서드의 유일한 전달인자이고 후행 클로저로 표현한다면 함수 또는 메서드의 호출 시 소괄호를 생략할 수 있습니다.

reversedNames = names.sorted { $0 > $1 }

아래 map(_:) 메서드의 용례를 통해 이해를 명확히 해봅시다. 메서드를 호출할 때 소괄호를 생략한 모습을 확인할 수 있습니다.

let digitNames = [
    0: "Zero", 1: "One", 2: "Two",   3: "Three", 4: "Four",
    5: "Five", 6: "Six", 7: "Seven", 8: "Eight", 9: "Nine"
]
let numbers = [16, 58, 510]

let strings = numbers.map { (number) -> String in
    var number = number
    var output = ""
    repeat {
        output = digitNames[number % 10]! + output
        number /= 10
    } while number > 0
    return output
}
// strings is inferred to be of type [String]
// its value is ["OneSix", "FiveEight", "FiveOneZero"]

함수가 복수의 클로저를 요구한다면 첫번째 후행 클로저는 전달인자 레이블을 생략할 수 있습니다. 먼저 아래 예시 함수를 먼저 보시죠.

func loadPicture(from server: Server, completion: (Picture) -> Void, onFailure: () -> Void) {
    if let picture = download("photo.jpg", from: server) {
        completion(picture)
    } else {
        onFailure()
    }
}

위의 예시 함수를 클로저 표현을 활용해 아래와 같이 호출할 수 있습니다. 함수는 loadPicture(from:completion:onFailure:)였습니다. 상기 언급한 바와 같이 첫번째 전달인자 (completion handler)의 레이블이 생략되었음을 확인할 수 있습니다.

loadPicture(from: someServer) { picture in
    someView.currentPicture = picture
} onFailure: {
    print("Couldn't download the next picture.")
}

특강 - 새로운 지식을 나의 지식으로 만드는 방법 (Zedd)

별도 포스팅 후 링크 남기겠습니다.

profile
합리적인 해법 찾기를 좋아합니다.

1개의 댓글

comment-user-thumbnail
2021년 3월 29일

zedd라니ㅠㅜㅠㅜ

답글 달기