이 포스팅은 Swift 시리즈 20 편 중 10 번째 글 입니다.

  • Part 1 - 01: Optional, Any, AnyObject, nil
  • Part 2 - 02: struct, class, enum
  • Part 3 - 03: Closure
  • Part 4 - 04: Property
  • Part 5 - 05: 상속, 생성자, 소멸자
  • Part 6 - 06: 옵셔널 체이닝과 nil 병합
  • Part 7 - 07: 타입 캐스팅
  • Part 8 - 08: assert, guard
  • Part 9 - 09: Protocol
  • Part 10 - This Post
  • Part 11 - 11: 오류처리
  • Part 12 - 12: 고차함수
  • Part 13 - 13: ARC(Automatic Reference Counting)
  • Part 14 - 14: Access control, Access modifier
  • Part 15 - 15: Generics
  • Part 16 - 16: Optional에 대한 깊은 이해
  • Part 17 - 17: Lazy Variables
  • Part 18 - 18: Enumeration
  • Part 19 - 19: Initialization
  • Part 20 - 20: Concurrency
▼ 목록 보기

익스텐션

  • 강력한 기능!
  • 구조체, 클래스, 열거형, 프로토콜 타입에 새로운 기능을 추가할 수 있음
  • 기능을 추가하려는 타입의 구현된 소스 코드를 알지 못하거나, 볼 수 없어도
  • 타입만 알고있다면 타입의 기능을 확장할 수 있음!

추가 가능 기능

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

다만, 재정의는 불가하다.

상속 vs 익스텐션

비슷해보이나 굉장히 다르다. 상속은 클래스 타입에서만 가능하다. 익스텐션은 구조체, 클래스, 프로토콜 등에 적용을 할 수 있다. 또 클래스의 상속은 특정 타입을 물려받아 하나의 새로운 타입을 정의하고 추가기능을 구현하는 수직확장이지만, 익스텐션은 기존의 타입에 기능을 추가하는 수평 확장이다. 상속을 받으면 기존 기능을 재정의할 수 있지만, 익스텐션은 재정의가 불가하다.

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

익스텐션의 활용

  • 기존 타입에 기능을 추가하는 방법도 있음
  • 하지만 외부 라이브러리나 프레임워크를 가져다 썼다면 원본 소스를 수정하지 못함
  • 외부에서 가져온 타입에 원하는 기능을 추가하고자 할 때, 익스텐션을 사용함
  • 상속을 받지 않아도 되고, 구조체, 열거형에도 추가가 가능하여 매우 편리함
  • 모든 타입에 적용이 가능함
  • 모든 타입 : 구조체, 열거형, 클래스, 프로토콜, 제네릭 타입
  • 프로토콜과 함께 사용하면 굉장히 강력함.
  • Protocol Oriented Programming에 대해 알아볼 것!

정의

  • extension 키워드 사용하여 정의
extension 확장타입이름 {
    /* define */
}
  • 익스텐션을 또 이런 기능도 있다.
  • 기존의 존재하는 타입이 추가적으로 다른 프로토콜을 채택할 수 있도록 확장할 수 있다.
  • 내부 소스코드로 안가고, 추가로 확장해서 사용하는 것
extension 확장타입이름: 프로토콜1, 프로토콜2, 프로토콜3 {
    /* define */
}
  • 스위프트 라이브러리에는 익스텐션이 굉장히 많이 사용된다.
  • Double 타입에는 수많은 프로퍼티와 메서드, 생성자가 정의되어 있다.
  • 그러면 당연히 수많은 프로토콜을 채택하고 있지 않을까? 준수할게 많겠지.
  • 그런데 실제 정의를 가보면 모든 것이 다 정의되어 있지는 않다.
  • 스위프트 표준 라이브러리 타입의 기능은 대부분 익스텐션으로 구현되어 있다.

구현

하나씩 추가해보자.

연산 프로퍼티 추가

  • 아래 익스텐션은 Int 타입에 두 개의 연산 프로퍼티를 추가한 것
  • Int 타입의 인스턴스가 홀수인지 짝수 인지 판별하여 Bool 타입으로 알려주는 연산 프로퍼티임
  • 이렇게 추가해주면 Int 타입의 모든 인스턴스에서도 사용가능
  • static 키워드로 타입 연산 프로퍼티도 추가할 수 있음
extension Int {
    var isEven: Bool {
        print(self)
        return self % 2 == 0
    }
    var isOdd: Bool {
        return self % 2 == 1
    }
}
print(1.isEven) // false
print(2.isEven) // true
print(1.isOdd)  // true
print(2.isOdd)  // false

var number: Int = 3
print(number.isEven) // false
print(number.isOdd) // true

number = 2
print(number.isEven) // true
print(number.isOdd) // false

신기한게 self로 접근하는데, 이부분 정의를 보아도 아직 해결하지 못했다.

메서드 추가

  • Int 타입에 인스턴스 메서드인 multiply 메서드를 추가해보자.
  • 여러 기능을 블록으로 나누어서 구현해도 문제가 없다.
  • 관련 기능 별로 하나의 익스텐션 블록에 묶는게 좋아보인다.
extension Int {
    func multiply(by n: Int) -> Int {
        return (self * n)
    }
}

print(3.multiply(by: 2))  // 6
print(4.multiply(by: 5))  // 20

number = 3
print(number.multiply(by: 2))   // 6
print(number.multiply(by: 3))   // 9

생성자 추가

  • 인스턴스를 초기화할 때 초기화에 필요한 데이터를 받을 수 있도록 여러 종류의 생성자를 만들 수 있음
  • 타입 정의부에는 추가하지 않았더라도 익스텐션을 통해 추가가가능함
  • 익스텐션으로 클래스 타입에 편의 이니셜라이저는 추가가능하나
  • 지정 이니셜라이저는 추가할 수 없다.
  • 지정 이니셜라이저와 디이니셜라이저는 클래스 타입의 구현부에 위치해야 함
  • 이건 참조 타입이라 그런 것. 참조 타입은 상관없음
  • 그럼 이걸 하기 위해서는 확장하고 싶은 타입의 프로퍼티에 대해서 이해하고 있어야 하겠군
extension String {
    init(int: Int) {
        self = "\(int)"
    }
    
    init(double: Double) {
        self = "\(double)"
    }
}

let stringFromInt: String = String(int: 100)
// "100"

let stringFromDouble: String = String(double: 100.0)
// "100.0"

Reference