1. 1. 확장
    1. 1.1. 서브스크립트
    2. 1.2. 상속
      1. 1.2.1. 클래스 상속
      2. 1.2.2. 재정의
        1. 1.2.2.1. 프로퍼티 재정의
      3. 1.2.3. 프로퍼티 감시자 재정의
      4. 1.2.4. 서브스크립트 재정의
      5. 1.2.5. 재정의 방지
    3. 1.3. 클래스의 이니셜라이저 - 상속과 재정의
      1. 1.3.1. 지정 이니셜라이저와 편의 이니셜라이저
      2. 1.3.2. 클래스의 초기화 위임
      3. 1.3.3. 2단계 초기화
      4. 1.3.4. 이니셜라이저 상속 및 재정의
        1. 1.3.4.1. 이니셜라이저 자동 상속
      5. 1.3.5. 요구 이니셜라이저
    4. 1.4. 타입캐스팅
      1. 1.4.1. 기존 언어의 타입 변환과 스위프트의 타입 변환
      2. 1.4.2. 스위프트 타입캐스팅
      3. 1.4.3. 데이터 타입 확인
      4. 1.4.4. 다운캐스팅
      5. 1.4.5. Any, AnyObject의 타입캐스팅
    5. 1.5. 프로토콜
      1. 1.5.1. 프로토콜이란
      2. 1.5.2. 프로토콜 요구사항
        1. 1.5.2.1. 프로퍼티 요구
        2. 1.5.2.2. 메소드 요구
        3. 1.5.2.3. 가변 메소드 요구
        4. 1.5.2.4. 이니셜라이저 요구
      3. 1.5.3. 프로토콜의 상속 클래스 전용 프로토콜
      4. 1.5.4. 프로토콜 조합과 프로토콜 준수 확인
      5. 1.5.5. 프로토콜의 선택적 요구
      6. 1.5.6. 프로토콜 변수와 상수
      7. 1.5.7. 위임을 위한 프로토콜
    6. 1.6. 익스텐션
      1. 1.6.1. 익스텐션 문법
      2. 1.6.2. 익스텐션으로 추가할 수 있는 기능
    7. 1.7. 제네릭
      1. 1.7.1. 제네릭 함수
      2. 1.7.2. 제네릭 타입
      3. 1.7.3. 제네릭 타입 확장
      4. 1.7.4. 타입 제약
      5. 1.7.5. 프로토콜의 연관 타입
      6. 1.7.6. 제네릭 서브스크립트
    8. 1.8. 프로토콜 지향 프로그래밍
      1. 1.8.1. 프로토콜 초기구현
      2. 1.8.2. 맵, 필터, 리듀스 직접 구현하기
        1. 1.8.2.1.
      3. 1.8.3. 필터
      4. 1.8.4. 리듀스
      5. 1.8.5. 기본 타입 확장

야곰의 스위프트 프로그래밍 - 확장

확장

서브스크립트

클래스, 구조체, 열거형에서 컬렉션, 리스트, 시퀀스 등 타입 요소에 접근하는 단축 문법

별도의 설정자, 접근자를 구현하지 않아도 인덱스를 통해 값을 설정하거나 접근할 수 있음

  • 인덱스라고 해서 반드시 Int 타입인 것은 아님. 어떤 타입이든 사용 가능

여러 개의 서브스크립트 정의 가능 / 다른 타입을 인덱스로 갖는 여러 개의 서브스크립트 중복 정의 가능

subscript(_ index: Int) -> Int {
get {
// 적절한 서브스크립트 결과값 반환
}
set(newValue) {
// 적절한 설정자 역할 수행
}
}

newValue의 타입은 서브스크립트의 반환 타입과 동일하다.

읽기 전용 서브스크립트를 정의하려면 get 키워드만 사용하여 정의하거나 키워드를 사용하지 않고 정의하면 된다.

인스턴스의 이름 뒤에 대괄호로 감싼 값을 써주어 사용한다. 값은 서브스크립트의 매개 변수에 정의한 타입을 가져야 한다.

상속

클래스는 메소드나 프로퍼티를 다른 클래스로부터 상속받을 수 있다.

상속받은 메소드나 프로퍼티, 서브스크립트 등을 재정의하는 것 또한 가능하다.

상속받은 프로퍼티에 프로퍼티 감시자를 구현할 수 있다. 그것이 연산 프로퍼티든 저장 프로퍼티든.

기반 클래스 : 다른 클래스로부터 상속받지 않은 클래스

클래스 상속

수직적. 기반클래스를 다른 클래스에서 물려받는 것

다른 클래스를 상속받으면 똑같은 기능을 구현하기 위하여 코드를 다시 작성할 필요가 없어 코드를 재사용하기 용이하고 더불어 기능을 확장할 때 기존 클래스를 변경하지 않고도 새로운 추가 기능을 정의할 수 있다.

자식클래스는 부모클래스의 모든 특성(인스턴스 메소드, 타입 메소드, 인스턴스 프로퍼티, 타입 프로퍼티, 서브스크립트 등)을 포함하면서 추가적인 기능을 구현할 수 있다.

이를 위해 클래스 디자인을 초기에 잘 하는 것이 좋겠지만… 그것이 힘들다면 리팩토링이라도 잘 하는게 중요하겠다.

재정의

super 키워드를 사용하여 부모 클래스의 특성에 접근할 수 있다.

  • 타입 메소드 내에서 부모 클래스의 타입 프로퍼티와 타입 메소드에 접근 가능
  • 인스턴스 메소드 내에서 부모 클래스의 인스턴스 프로퍼티와 인스턴스 메소드, 서브스크립트에 접근 가능
    • super.메소드_이름()
    • super.프로퍼티_이름
    • super[index]

프로퍼티 재정의

프로퍼티 자체를 재정의하는 것이 아님

프로퍼티의 접근자, 설정자, 프로퍼티 감시자 등을 재정의하는 것을 의미한다.

  • 그러므로 저장 프로퍼티로 재정의할 수는 없다.

프로퍼티를 상속받은 자식클래스에서는 조상클래스의 프로퍼티의 이름과 타입만을 알 뿐, 종류(저장, 연산 등)은 알지 못하므로 재정의할 수 있는 것.

읽기 쓰기 모두 가능했던 연산 프로퍼티를 읽기 전용 프로퍼티로 재정의할 수 없다.

  • 읽기 전용 프로퍼티를 읽고 쓰기 모두 가능한 프로퍼티로 재정의할 수 있다.
  • 읽기 쓰기 모두 가능한 프로퍼티의 설정자만 따로 재정의할 수 없다. 접근자와 설정자를 모두 재정의 해주어야 하며, 이 경우 접근자에서 특별한 기능 변경이 없다면 super.프로퍼티_이름을 반환해주면 된다.

프로퍼티 감시자 재정의

조상클래스에 정의한 프로퍼티가 연산 프로퍼티인지 저장 프로퍼티인지는 상관 없다.

  • 상수 저장 프로퍼티, 읽기 전용 연산 프로퍼티의 경우 프로퍼티 감시자 재정의가 불가능하다.
    • 상수 저장 프로퍼티는 값이 바뀔 일이 없음
    • 읽기 전용 연산 프로퍼티는 설정자가 없으므로 값을 설정할 수 없음

프로퍼티 감시자를 재정의하더라도 조상클래스에 정의한 프로퍼티 감시자도 동작한다.

프로퍼티의 접근자와 프로퍼티 감시자는 동시에 재정의할 수 없으며, 재정의하는 접근자에 프로퍼티 감시자 역할을 구현해야 한다.

서브스크립트 재정의

서브스크립트도 매개변수와 반환형이 다르면 다른 서브스크립트로 취급되므로 자식클래스에서 재정의하려는 서브스크립트는 부모클래스의 서브스크립트의 매개변수와 반환타입과 같아야 한다.

다만 자동완성이 안되는 것에 주의하자.

재정의 방지

final 키워드를 명시하여 재정의를 할 수 없도록 제한할 수 있다.

  • final class 클래스명과 같이 정의된 클래스는 상속되거나 재정의될 수 없다.

클래스의 이니셜라이저 - 상속과 재정의

값 타입의 이니셜라이저는 상속을 고려할 필요가 없었으나 클래스는 상속이 가능하므로 상속받았을 때 이니셜라이저를 어떻게 재정의하는지도 중요하다.

두 종류의 이니셜라이저가 존재하고, 다양한 패턴의 이니셜라이저가 생길 수 있어 클래스 디자인에 더 많은 고민이 필요하다.

지정 이니셜라이저와 편의 이니셜라이저

지정 이니셜라이저

  • 주요 이니셜라이저. 일반적인 의미의 이니셜라이저
  • 이니셜라이저가 정의된 클래스의 모든 프로퍼티를 초기화해야함
  • 클래스에 하나 이상 정의함
  • 조상클래스에 정의된 지정 이니셜라이저가 자손클래스의 지정 이니셜라이저 역할을 충분히 해낸다면 자손클래스는 지정 이니셜라이저를 갖지 않을 수 있음
    • 자손클래스에서 새로 선언한 프로퍼티가 옵셔널 타입일 때 등의 경우

편의 이니셜라이저

  • convenience 키워드를 init 앞에 명시하여 편의 이니셜라이저 정의
  • 초기화를 좀 더 쉽게 할 수 있도록 도와주는 역할
  • 지정 이니셜라이저를 내부에서 호출
    • 부모클래스의 이니셜라이저를 호출할 수 없음
  • 클래스 설계자의 의도대로 외부에서 사용하길 원하거나 인스턴스 생성 코드를 작성하는 수고를 덜 때 유용하게 사용 가능

클래스의 초기화 위임

  • 자식클래스의 지정 이니셜라이저는 부모클래스의 지정 이니셜라이저를 반드시 호출해야 한다.
  • 편의 이니셜라이저는 자신을 정의한 클래스의 다른 이니셜라이저를 반드시 호출해야 한다.
  • 편의 이니셜라이저는 궁극적으로는 지정 이니셜라이저를 반드시 호출해야 한다.

어떤 이니셜라이저는 지정 이니셜라이저에게 초기화를 반드시 위임한다.

편의 이니셜라이저는 어떤 이니셜라이저에게 초기화를 반드시 위임한다.

2단계 초기화

Swift에서 클래스 초기화는 2단계로 이루어진다.

  • 프로퍼티를 초기화하기 전에 프로퍼티 값에 접근하는 것을 막아 초기화를 안전하게 할 수 있게 함
  • 다른 이니셜라이저가 프로퍼티의 값을 변경하는 것을 방지함

안전확인

  • 자식클래스의 지정 이니셜라이저가 부모클래스의 이니셜라이저를 호출하기 전에 자신의 프로퍼티를 모두 초기화했는지 확인
  • 자식클래스의 지정 이니셜라이저는 상속받은 프로퍼티에 값을 할당하기 전에 반드시 부모클래스의 이니셜라이저를 호출해야 함
    • 부모클래스의 이니셜라이저가 부모클래스의 프로퍼티 값을 초기화할 가능성이 있음
  • 편의 이니셜라이저는 자신의 클래스에 정의한 프로퍼티를 포함하여 그 어떤 프로퍼티라도 값을 할당하기 전에 다른 이니셜라이저를 호출해야 함
    • 클래스 내에 있는 다른 이니셜라이저가 프로퍼티 값을 초기화할 가능성이 있음
  • 초기화 1단계를 마치기 전까지 이니셜라이저는 인스턴스 메소드를 호출할 수 없음. 인스턴스 프로퍼티의 값을 읽어들일 수도없음. self 프로퍼티를 자신의 인스턴스를 나타내는 값으로 활용할 수 없음

클래스의 인스턴스는 초기화 1단계를 마치기 전에는 유효하지 않다. 1단계를 마쳐야 비로소 유효한 인스턴스가 된다.

1단계

  1. 클래스가 지정 또는 편의 이니셜라이저를 호출함
  2. 해당 클래스의 새로운 인스턴스를 위한 메모리가 할당됨. 그 메모리는 아직 초기화되지 않은 상태
  3. 지정 이니셜라이저는 클래스에 정의된 모든 저장 프로퍼티에 값이 있는지 확인. 현재 클래스 부분까지의 저장 프로퍼티를 위한 메모리가 초기화됨
  4. 지정 이니셜라이저는 부모클래스의 이니셜라이저가 같은 동작을 행할 수 있도록 초기화를 양도함
  5. 부모클래스는 상속 체인을 따라 최상위 클래스에 도달할 때까지 이 작업을 반복
    • 최상위 클래스에 도달했을 때 최상위 클래스까지 모든 저장 프로퍼티에 값이 있다고 확인하면 해당 인스턴스의 메모리는 모두 초기화된 것이고, 이로써 1단계가 완료됨

호출한 클래스에서 최상위 클래스까지 안전확인을 거치며 거슬러 올라감

2단계

  1. 최상위 클래스로부터 최하위 클래스까지 상속 체인을 따라 내려오면서 지정 이니셜라이저들이 인스턴스를 제각각 사용자 정의함. 이 단계에서 self를 통해 프로퍼티 값을 수정할 수 있고, 인스턴스 메소드를 호출하는 등의 작업을 진행할 수 있음
  2. 마지막으로 각각의 편의 이니셜라이저를 통해 self를 통한 사용자 정의 작업을 진행할 수 있음

1단계를 통해 도달한 최상위 클래스에서 사용자 정의를 하고, 아래로 내려가면서 사용자 정의함

이니셜라이저 상속 및 재정의

기본적으로 Swift의 이니셜라이저는 부모클래스의 이니셜라이저를 상속받지 않는다. 부모클래스의 이니셜라이저는 자식클래스에 최적화되어 있지 않기 때문이며, 최적화되었다고 판단되는 특정 상황에서는 부모클래스의 이니셜라이저가 상속되기도 한다.

부모클래스의 이니셜라이저와 동일한 이니셜라이저를 자식클래스에서 사용하려면 재정의하여 구현한다.

부모클래스의 편의 이니셜라이저와 동일한 이니셜라이저를 자식클래스에서 구현하려면 override 키워드를 명시하지 않는다. 편의 이니셜라이저는 부모클래스의 이니셜라이저를 호출할 수 없기 때문이다.

부모클래스의 실패 가능한 이니셜라이저(init?)를 자식 클래스에서 재정의하고 싶을 때는 필요에 따라서 실패하지 않는 이니셜라이저로 재정의할 수 있다.

이니셜라이저 자동 상속

자식클래스에서 프로퍼티 기본값을 모두 제공한다고 가정할 때,

  • 자식클래스에서 별도의 지정 이니셜라이저를 구현하지 않는다면 부모클래스의 지정 이니셜라이저가 자동 상속됨
    • 자식클래스에서 자신만의 지정 이니셜라이저를 구현하였다면, 부모클래스의 지정 이니셜라이저는 자동 상속되지 않음
  • 위의 규칙에 따라 자식클래스가 부모클래스의 지정 이니셜라이저를 자동 상속받은 경우, 또는 부모클래스의 지정 이니셜라이저를 모두 재정의하여 부모클래스와 동일한 지정 이니셜라이저를 모두 사용할 수 있는 상황이라면, 부모 클래스의 편의 이니셜라이저가 자동 상속됨

요구 이니셜라이저

required 키워드를 클래스의 이니셜라이저 앞에 명시하여 해당 클래스를 상속받은 자식클래스가 반드시 해당 이니셜라이저를 재정의해주어야 한다. 이 경우 재정의하는 부분에서 override 키워드가 아닌 required 키워드를 사용하여 재정의한다.

UIView에서 이니셜라이저를 재정의할 때 init(frame:)을 재정의해주면 init(coder:)도 재정의해주어야 함. 자식클래스에서 별도의 지정 이니셜라이저를 구현해주어 이니셜라이저 자동 상속이 이루어지지 않고, init(coder:)는 요구 이니셜라이저이기 때문이다.

부모클래스의 이니셜라이저를 자신의 클래스에서 요구 이니셜라이저로 변경할 수 있으며, 이 경우 required override 키워드를 명시한다.

편의 이니셜라이저도 required convenience 키워드로 요구 편의 이니셜라이저가 되게 할 수 있다.

타입캐스팅

Swift는 강타입 언어. 데이터 타입 안전을 위해 각기 다른 타입끼리의 값 교환을 엄격하게 제한한다. 암시적 형변환 또한 지원하지 않는다.

기존 언어의 타입 변환과 스위프트의 타입 변환

let a = "13"
let b = Int(a)

위의 코드의 Int(a)은 타입캐스팅 또는 형변환이 아니다. Int는 구조체로 정의되어 있고, 새로운 구조체 인스턴스를 생성하는 것이다.

bInt?형을 갖고, 이 경우 실패 가능한 이니셜라이저로 구현되어 있음을 알 수 있다.

스위프트 타입캐스팅

Swift의 타입캐스팅은 인스턴스의 타입을 확인하거나 자신을 다른 타입의 인스턴스인 것처럼 행세할 수 있는 방법으로 사용할 수 있다.

is 연산자와 as 연산자로 구현됨 : 값의 타입을 확인하거나 다른 타입으로 전환할 수 있음, 프로토콜을 준수하는지 확인할 수 있음

참조 타입에서 주로 사용됨

데이터 타입 확인

is 연산자를 사용하여 인스턴스가 어떤 클래스 (혹은 어떤 클래스의 자식클래스)의 인스턴스인지 확인할 수 있음

모든 데이터 타입에 사용 가능

메타 타입 타입

  • 타입의 타입. 타입 자체가 하나의 타입으로 또 표현할 수 있음
  • 타입의 이름 뒤에 .Type를 붙여 메타 타입을 나타냄
    • 프로토콜의 경우 프로토콜 이름 뒤에 .Protocol을 붙여 프로토콜의 메타 타입을 나타냄
  • 타입의 이름 뒤에 .self를 붙여 타입을 값으로 표현한 값을 반환
protocol Protocol { }
class Class: Protocol { }
let intType: Int.Type = Int.self
let classType: Class.Type = Class.self
let protocolType: Protocol.Protocol = Protocol.self
  • type(of:) 함수를 사용하여 인스턴스의 타입을 표현한 값을 구할 수 있음
    • type(of: someInstance).self와 같이 작성하여 someInstance의 타입을 값으로 표현한 값을 반환
let a = 1.self			// a는 Int 타입
let b = String.self // b는 String.Type 타입 (String 메타 타입 타입)
print(a) // 1
print(b) // String

다운캐스팅

as 연산자를 사용하여 자식클래스보다 더 상위에 있는 부모클래스의 타입을 자식클래스의 타입으로 캐스팅

as? 연산자는 다운캐스팅 실패시 nil을 반환하고 성공시 옵셔널 타입의 인스턴스를 반환

as! 연산자는 다운캐스팅 실패시 런타임 에러를 발생시키고 성공시 해당 타입의 인스턴스를 반환

as 연산자는 컴파일러가 다운캐스팅이 성공할 것임을 확신할 때 사용 가능

  • 캐스팅하려는 타입이 같은 타입이거나 부모클래스 타입

캐스팅은 실제로 인스턴스를 수정하거나 값은 변경하는 작업이 아님. 인스턴스는 메모리에 똑같이 남아 있음

인스턴스를 사용할 때 어떤 타입으로 다루고 접근해야 하는지 판단하는 것을 도와주는 것

Any, AnyObject의 타입캐스팅

Any는 함수 타입을 포함한 모든 타입을 뜻함

AnyObject는 클래스 타입만을 뜻함

가령 API를 사용할 때 반환 타입이 AnyAnyObject라면, 이것이 어떤 데이터 타입인지 확인하고 사용해야 할 필요가 있다.

is 연산자나 as 연산자 등으로 데이터 타입을 확인하고, 적절하게 처리하여 사용 가능하다.

프로토콜

Swift의 Protocol Oriented Programming을 실현하기 위한 수단

프로토콜이란

특정 역할을 하기 위한 메소드, 프로퍼티, 기타 요구사항 등의 청사진

구조체, 열거형, 클래스는 프로토콜을 채택하여 특정 기능을 실행하기 위한 프로토콜의 요구사항을 실제로 구현할 수 있음

프로토콜 준수 : 어떤 프로토콜의 요구사항을 모두 따르는 타입은 해당 프로토콜을 준수한다.

프로토콜은 정의를 하고 제시를 할 뿐 스스로 기능을 구현하지는 않는다.

프로토콜 요구사항

프로퍼티 요구

프로토콜은 자신을 채택한 타입이 어떤 프로퍼티를 구현해야 하는지 요구할 수 있음

프로토콜은 그 프로퍼티의 종류는 신경쓰지 않고, 다만 프로토콜을 채택한 타입이 프로퍼티의 이름과 타입만 맞도록 구현해주면 됨

프로퍼티를 읽기 전용으로 할지 읽고 쓰기가 모두 가능하게 할지는 프로토콜이 결정해야 함

var 키워드를 사용한 변수 프로퍼티로 정의

  • 읽고 쓰기가 모두 가능한 프로퍼티에는 프로퍼티의 정의 뒤에 { get set }을 명시
  • 읽기 전용 프로퍼티에는 프로퍼티의 정의 뒤에 { get }을 명시

타입 프로퍼티를 요구하기 위해 static 키워드를 사용하여 정의.

protocol Protocol {
// 이 프로토콜을 채택한 타입은 상수 저장 프로퍼티나 읽기 전용 연산 프로퍼티, 읽고 쓰기가 모두 가능한 연산 프로퍼티로 구현 가능
var property1: String { get }
// 이 프로토콜을 채택한 타입은 읽고 쓰기가 모두 가능한 연산 프로퍼티로 구현 가능
var property2: Int { get set }
static var property3: String { get }
static var property4: Int { get set }
}

메소드 요구

프로토콜은 자신을 채택한 타입이 어떤 인스턴스 메소드나 타입 메소드를 구현해야 하는지 요구할 수 있음

실제 구현부인 중괄호 부분은 제외. 메소드 이름, 반환형, 매개변수 등만 작성. 가변 매개변수 허용

매개변수 기본값을 지정할 수 없음

타입 메소드를 요구하기 위해 static 키워드를 사용하여 정의. 실제 구현시 staticclass 키워드 중 어느 것을 사용하여 구현해 주어도 무방함

protocol Protocol {
func method1(data: Any)
func method2()
static func method3() -> Bool
}

프로토콜 타입의 인스턴스는 해당 프로토콜을 준수하는 타입의 인스턴스라고 생각할 수 있음

델리게이트 패턴을 구현할 때 프로토콜 타입의 변수에 해당 프로토콜을 준수하는 인스턴스를 할당할 수 있음

tableView.delegate = self

가변 메소드 요구

메소드가 값 타입 인스턴스 내부의 값을 변경하려고 할 때 mutating 키워드를 명시하여 메소드가 인스턴스 내부의 값을 변경한다는 것을 확실히 해줌

클래스(참조 타입 인스턴스) 구현에서는 mutating 키워드를 명시하지 않아도 됨.

값 타입 인스턴스 구현에서는 mutating 키워드를 명시해 주어야 함

이니셜라이저 요구

프로토콜은 자신을 채택한 타입이 준수할 특정 이니셜라이저를 요구할 수 있음

이니셜라이저의 매개변수를 지정할 뿐, 중괄호를 포함한 이니셜라이저 구현은 프로토콜 내에서 이루어지지 않음

protocol Protocol {
var name: String { get }
init(name: String)
}

클래스 타입에서 위 프로토콜이 요구하는 이니셜라이저를 구현할 때 이니셜라이저가 지정 이니셜라이저인지 편의 이니셜라이저인지는 중요하지 않음

  • 그러나 required 키워드를 명시하여 요구 이니셜라이저로 구현해주어야 함
    • 프로토콜이 이니셜라이저 구현을 요구함
    • 클래스가 final 키워드로 상속받을 수 없는 클래스로 정의되었다면 required 식별자를 명시할 필요가 없음
  • 특정 클래스에 프로토콜이 요구하는 이니셜라이저가 이미 구현되어 있는 상황에서 해당 클래스를 상속받은 클래스가 프로토콜을 준수하려 한다면, required override 키워드를 명시하여 프로토콜이 요구하는 이니셜라이저를 구현해 주어야 함

프로토콜은 실패 가능한 이니셜라이저를 요구할 수 있으며, 이 때 프로토콜을 준수하는 타입은 이것을 실패 가능한 이니셜라이저로, 실패하지 않는 이니셜라이저로 구현해도 무방함

프로토콜의 상속 클래스 전용 프로토콜

프로토콜은 하나 이상의 프로토콜을 상속받아 기존 프로토콜의 요구사항보다 더 많은 요구사항을 추가할 수 있음

프로토콜의 상속 리스트의 맨 처음에 class 키워드를 추가하여 프로토콜이 클래스 타입에만 채택될 수 있도록 제한할 수 있음

  • 델리게이트 패턴을 구현하기 위한 프로토콜이 위와 같이 정의되는 예시

프로토콜 조합과 프로토콜 준수 확인

하나의 매개변수가 여러 프로토콜을 모두 준수하는 타입이어야 한다면 하나의 매개변수에 여러 프로토콜을 한번에 조합하여 요구할 수 있고, 하나의 매개변수가 프로토콜을 둘 이상 요구할 수 있음

  • & 연산자로 프로토콜을 조합하여 표현함

특정 클래스와 프로토콜을 조합하여 클래스의 인스턴스 역할을 할 수 있는지 함께 확인할 수 있다.

  • 구조체, 열거형 타입은 조합할 수 없음
  • 클래스 타입은 단 한 가지 타입만 조합할 수 있음
// 인자에 SomeProtocol과 AnotherProtocol 프로토콜을 모두 준수하는 것만 넣을 수 있음
func function(_ a: SomeProtocol & AnotherProtocol)
// Class의 인스턴스 역할을 수행하며, SomeProtocol과 AnotherProtocol 프로토콜을 모두 준수하는 인스턴스만 할당 가능
var variable: Class & SomeProtocol & AnotherProtocol

isas 연산자를 사용하여 대상이 프로토콜을 준수하는지 확인할 수 있고, 특정 프로토콜로 캐스팅할 수 있음

  • 프로토콜도 하나의 타입이므로 프로토콜로 캐스팅 가능함

프로토콜의 선택적 요구

먼저 @objc 속성을 명시하여 해당 프로토콜을 Objective-C에서 사용 가능하도록 만들어야 한다.

이후 프로퍼티나 메소드 정의 시 optional 키워드를 명시하여 선택적으로 요구할 수 있는 프로퍼티나 메소드를 정의한다.

  • 이 경우 프로퍼티나 메소드의 타입 자체가 옵셔널이 됨
@objc protocol Protocol {
@objc optional func fly()
}
class Class: NSObject, Protocol { }
let a = Class()
// 선택적 요구사항인 메소드를 호출하므로 옵셔널 체인을 이용하여 호출. 응답 없음
a.fly?()

프로토콜 변수와 상수

프로토콜은 프로토콜 이름만으로 자기 스스로 인스턴스를 생성하고 초기화할 수 없다.

하지만 특정 프로토콜을 준수하는 타입의 인스턴스를 할당할 수는 있다.

위임을 위한 프로토콜

Delegation : 클래스나 구조체가 자신의 책임이나 임무를 다른 타입의 인스턴스에게 위임하는 디자인 패턴

책무를 위임. 다른 인스턴스에게 자신이 해야 할 일을 위임.

사용자의 특정 행동에 반응하기 위해 사용되기도 하며, 비동기 처리에도 많이 사용함

익스텐션

익스텐션 기능으로 구조체, 클래스, 열거형, 프로토콜 타입에 새로운 기능을 추가할 수 있음

  • 연산 타입 프로퍼티 / 연산 인스턴스 프로퍼티
  • 타입 메소드 / 인스턴스 메소드
  • 이니셜라이저
  • 서브스크립트
  • 중첩 타입
  • 특정 프로토콜을 준수할 수 있도록 기능 추가

익스텐션은 타입에 새로운 기능을 추가하는 것. 기존에 존재하는 기능을 재정의할 수 없음

상속 익스텐션
확장 수직적 수평적
사용 클래스 클래스 / 구조체 / 열거형 / 프로토콜 / 제네릭 등 모든 타입
재정의 재정의 가능 재정의 불가능

외부에서 가져온 타입에 내가 원하는 기능을 추가하려 할 때 익스텐션을 활용할 수 있음

익스텐션 문법

기존에 존재하는 타입이 추가로 다른 프로토콜을 채택할 수 있도록 확장할 수 있음

extension Class: UITableViewDelegate { }

익스텐션으로 추가할 수 있는 기능

저장 프로퍼티는 추가할 수 없음. 기존 프로퍼티에 프로퍼티 감시자를 추가할 수 없음.

클래스 타입에 지정 이니셜라이저를 추가할 수 없음. 편의 이니셜라이저는 추가할 수 있음

  • 클래스 타입에서 지정 이니셜라이저와 디이니셜라이저는 반드시 클래스 구현부에 위치해야 함

값 타입에 사용자 정의 이니셜라이저를 정의할 때 기본 이니셜라이저나 멤버와이즈 이니셜라이저를 사용하고 싶다면 익스텐션을 사용하여 사용자 정의 이니셜라이저를 구현하면 된다.

  • 모든 저장 프로퍼티에 기본값이 있음
    • 모든 저장 프로퍼티에 기본값이 있어야 기본 이니셜라이저 init()을 호출할 수 있음
    • 그렇지 않으면 멤버와이즈 이니셜라이저만 호출 가능
  • 타입에 기본 이니셜라이저와 멤버와이즈 이니셜라이저 외 추가 사용자 정의 이니셜라이저가 없을 때

제네릭

중요!

어떠한 타입에도 유연하게 대응 / 제네릭으로 구현한 기능과 타입은 재사용하기 쉬움 / 코드 중복을 줄여 깔끔하고 추상적인 표현이 가능함

제네릭이 필요한 타입 또는 메소드의 이름 뒤 <제네릭을_위한_타입_매개변수>를 명시해준다.

제네릭을_사용하고자_하는_타입_이름<타입_매개변수>
제네릭을_사용하고자_하는_함수_이름<타입_매개변수>(함수의_매개변수...)

Any를 사용하면 되지 않을까?

  • Any는 모든 타입을 나타내는 타입이지만, 타입에 매우 엄격하므로 인자를 항상 Any로 캐스팅 시켜주든지 해야 함
  • 올바른 방법이 아니다.

제네릭 함수

public func swap<T>(_ a: inout T, _ b: inout T)

제네릭 함수는 실제 타입 이름을 명시하는 대신 플레이스홀더를 사용함. 위에서는 T

플레이스홀더는 타입의 종류를 알려주지는 않지만 어떤 타입이라는 것은 알려줌

  • ab의 매개변수 타입이 같다는 것은 알 수 있음

T의 실제 타입은 함수가 호출되는 순간 결정됨

  • Int 타입 변수가 전달되었다면 T == Int
  • String 타입 변수가 전달되었다면 T == String

플레이스홀더 타입 T는 타입 매개변수의 한 예로 들 수 있음

  • 플레이스홀더 타입의 이름을 지정하고 명시함
  • 함수 이름 뒤 <> 안쪽에 위치함 ex) <T>
  • 타입 매개변수는 함수의 매개변수 타입, 함수의 반환형, 함수 내부 변수의 타입 지정에 사용될 수 있음
    • 타입 매개변수는 함수를 호출할 때마다 실제 타입으로 치환됨
  • 여러 개의 타입 매개변수를 갖기 위해 <T, U, V>와 같은 형식으로 작성할수도 있음

타입 매개변수는 대부분 의미 있는 이름을 가지나, 특별히 관계의 의미를 이름으로 표현하기 어려울 때는 관용적으로 T, U, V 등의 대문자 한 글자로 표현함

  • Dictionary<Key, Value>
  • Array<Element>
  • func swap<T>
  • 타입 매개변수 이름 또한 타입 이름이므로 대문자 캐멀케이스 사용

제네릭 타입

제네릭 타입을 구현하여 사용자 정의 타입인 구조체, 클래스, 열거형 등이 어떤 타입과도 연관되어 동작할 수 있게 함

struct Stack<Element> {
var items: [Element] = []
mutating func push(_ item: Element) {
items.append(item)
}
mutating func pop() -> Element {
return items.removeLast()
}
}
// Element == Int
let intStack = Stack<Int>()
// Element == String
let stringStack = Stack<String>()

타입의 인스턴스를 생성할 때 <> 안에 실제 사용될 타입을 명시함. 해당 타입에만 동작하도록 제한할 수 있어 더욱 안전하고 의도한 대로 기능을 사용하도록 유도할 수 있음

제네릭 타입 확장

제네릭을 사용하는 타입에 기능을 추가하고자 익스텐션을 사용한다면, 익스텐션 정의에 타입 매개변수를 명시하지 않아야 한다. 대신 원래의 제네릭 정의에 명시한 타입 매개변수를 익스텐션에서 사용할 수 있다.

extension Stack {
var top: Element? {
return items.last
}
var bottom: Element? {
return items.first
}
var count: Int {
return items.count
}
}

타입 제약

타입 매개변수가 가져야 할 제약사항을 지정할 수 있음

  • 제네릭 함수가 처리해야 할 기능이 특정 타입에 한정될 수 있음
  • 제네릭 타입을 특정 프로토콜을 준수하는 타입만 사용할 수 있게 한정할 수 있음

클래스 타입 또는 프로토콜로만 타입 제약을 줄 수 있음

  • 특정 클래스를 상속받는 타입인가
  • 특정 프로토콜을 준수하는 타입인가

타입 매개변수를 명시하는 자리 뒤에 :을 붙이고 타입 제약을 걸 클래스나 프로토콜을 명시함

func swapTwoValues<T: BinaryInteger>(_ a: inout T, _ b: inout T) { ... }
struct Stack<Element: Hashable> { ... }

여러 제약을 추가하기 위해 where절을 사용할 수 있음

  • <> 안에 쉼표로 여러 개의 제약 조건을 나열하는 것이 아님
func swapTwoValues<T: BinaryInteger>(_ a: inout T, _ b: inout T) where T: FloatingPoint, T: Equatable { ... }

타입 제약에 자주 사용할 만한 프로토콜

  • Hashable / Equatable / Comparable / Indexable / IteratorProtocol / Error / Collection / CustomStringConvertible

타입 매개변수마다 제약 조건을 달리하여 구현할 수 있음

func function<Key: Hashable, Value>(key: Key, value: Value) -> Dictionary<Key, Value> { ... }

프로토콜의 연관 타입

프로토콜에서 사용할 수 있는 플레이스홀더 이름

associatedtype 키워드 사용

protocol Container {
associatedtype ItemType
var count: Int { get }
mutating func append(_ item: ItemType)
subscript(i: Int) -> ItemType { get }
}

실제 프로토콜 정의를 준수하기 위해 구현할 때는 ItemType을 하나의 타입으로 일관성 있게 구현하면 됨

ItemType을 어떤 타입으로 사용할지 명확히 하고 싶다면 typealias ItemType = Int와 같이 명시해줄 수 있음

제네릭 타입에서는 연관 타입과 타입 매개변수를 대응시킬 수 있음 typealias ItemType = Element

  • typealias 해주는 것이 자동완성에 유리한 것 같음
// typealias 하지 않아 ItemType을 하나의 타입으로 일관성 있게 구현
class MyContainer: Container {
var items: [Int] = []
var count: Int {
return items.count
}
func append(_ item: Int) {
items.append(item)
}
subscript(i: Int) -> Int {
return items[i]
}
}
// typealias 하여 ItemType을 프로토콜 구현 시 사용할 수도 있음
class YourContainer: Container {
typealias ItemType = String
var items: [ItemType] = []
var count: Int {
return items.count
}
func append(_ item: String) {
items.append(item)
}
subscript(i: Int) -> ItemType {
return items[i]
}
}
// 제네릭 타입에서 연관 타입과 타입 매개변수를 대응시킴
struct Stack<Element>: Container {
typealias ItemType = Element
var items: [ItemType] = []
mutating func append(_ item: Element) {
items.append(item)
}
var count: Int {
return items.count
}
subscript(i: Int) -> ItemType {
return items[i]
}
}

제네릭 서브스크립트

서브스크립트도 제네릭을 활용하여 타입에 큰 제한 없이 유연하게 구현할 수 있고, 타입 제약을 사용하여 제네릭을 활용하는 타입에 제약을 줄 수 있음

// 익스텐션으로 기능을 추가할 때 타입 매개변수를 다시 정의할 필요 없음
extension Stack {
subscript<Indices: Sequence>(indices: Indices) -> [ItemType] where Indices.Iterator.Element == Int { ... }
}
// Stack()[0...2]와 같이 서브스크립트 호출 가능

프로토콜 지향 프로그래밍

Swift의 표준 라이브러리에서 타입과 관련된 것들은 대부분 구조체로 구현되어 있음

상속도 되지 않는 구조체가 이토록 다양한 공통 기능을 갖게 된 것은 프로토콜, 익스텐션, 제네릭 덕분임

프로토콜 초기구현

특정 프로토콜을 정의하여 여러 타입에서 이 프로토콜을 준수하게 만들 때, 타입마다 같은 메소드, 프로퍼티, 서브스크립트 등을 구현해야 하는 경우 코드의 중복, 유지보수 등에서 문제를 발생시킬 것이다.

프로토콜 초기구현 : 프로토콜의 익스텐션에서 프로토콜이 요구하는 기능을 실제로 구현할 수 있음

프로토콜 초기구현을 해두었어도 특정 프로토콜을 준수하는 타입에 해당 프로토콜을 구현할 수 있음

  • 타입에 프로토콜이 요구하는 기능이 구현되어 있다면 그 기능을 호출
  • 그렇지 않다면 프로토콜 초기구현으로 구현된 기능을 호출
// 프로토콜 정의
protocol SelfPrintable {
func printSelf()
}
// Container 프로토콜을 준수하는 곳에서의 SelfPrintable 프로토콜 추기구현
extension SelfPrintable where Self: Container {
func printSelf() {
print(items)
}
}
// Container 프로토콜은 SelfPrintable 프로토콜을 준수함
protocol Container: SelfPrintable {
associatedtype ItemType
var items: [ItemType] { get set }
var count: Int { get }
mutating func append(_ item: ItemType)
subscript(i: Int) -> ItemType { get }
}
// Container 프로토콜 초기구현
extension Container {
mutating func append(_ item: ItemType) {
items.append(item)
}
var count: Int {
return items.count
}
subscript(i: Int) -> ItemType {
return items[i]
}
}
// Popable 프로토콜은 Container 프로토콜을 준수함
protocol Popable: Container {
mutating func pop() -> ItemType?
mutating func push(_ item: ItemType)
}
// Popable 프로토콜 초기구현
extension Popable {
mutating func pop() -> ItemType? {
return items.removeLast()
}
mutating func push(_ item: ItemType) {
items.append(item)
}
}
// Container 프로토콜은 items 프로퍼티를 제외하고 모두 초기구현 되어있어 현 상황에서 다시 구현해줄 필요는 없음
struct Stack<Element>: Container {
typealias ItemType = Element
var items: [Element] = []
}

let stack = Stack<Int>()
stack.printSelf() // []
  • 각각의 요소 타입은 제네릭을 통해 사용할 때 결정하므로 타입에 대해 매우 유연하게 동작할 수 있음
    • 클래스의 경우 다중상속이 불가하므로 부모클래스의 기능으로 부족하다면 자식클래스에서 다시 구현해야 함
    • 프로토콜 초기구현을 한 프로토콜을 채택하면 상속도, 초기 구현도 필요 없음
    • 여러 프로토콜을 채택할 수 있음

프로토콜 지향 프로그래밍의 핵심 컨셉트 중 하나

  • 프로토콜 초기구현을 통해 기능을 구현한다면 프로토콜 채택만으로 타입에 기능을 추가하여 사용할 수 있음

맵, 필터, 리듀스 직접 구현하기

자신이 갖는 요소 Element 타입을 어떠한 타입으로 변환시키는 함수를 전달받아 구현하기

func map<T>(transform: (Element) -> T) -> Stack<T> {
var transformed = Stack<T>()
for item in items {
transformed.items.append(transform(item))
}
return transformed
}

필터

전달받은 함수와 일치하는 조건의 경우만 반환하게 구현하기

func filter(includesElement: (Element) -> Bool) -> Stack<Element) {
var filtered = Stack<Element>()
for item in items {
if includesElement(item) {
filtered.items.append(item)
}
}
return filtered
}

리듀스

초기값을 전달받고, 전달받은 함수를 수행하여 초기값에 할당하는 식으로 구현하기

func reduce<T>(_ initial: T, combine: (T, Element) -> T) -> T {
var result = initial
for item in items {
result = combine(result, item)
}
return result
}

기본 타입 확장

사용자 정의 프로토콜을 정의하고, 프로토콜 초기구현으로 구현해 놓은 프로토콜을 익스텐션을 사용하여 기본 타입에 기능을 추가할 수 있음