확장

확장

이미 존재하는 타입에 새로운 기능을 더하고 싶을 때가 있어요. 그런데 그 타입이 내 코드가 아니라서 소스에 손을 댈 수 없을 때는 어떡할까요? 이럴 때 쓰는 게 바로 확장(Extension) 이에요.

확장은 클래스, 구조체, 열거형, 프로토콜 같은 기존 타입에 새로운 기능을 추가해요. 원래 소스 코드에 접근할 수 없는 타입도 확장할 수 있는데, 이걸 소급 모델링(retroactive modeling) 이라고 불러요. 확장은 Objective-C의 카테고리와 비슷한데, 한 가지 차이가 있어요. Objective-C 카테고리와 달리 Swift 확장에는 이름이 없어요.

출처: The Swift Programming Language

본문

Swift에서 확장은 이런 일들을 할 수 있어요:

  • 계산된 인스턴스 프로퍼티와 계산된 타입 프로퍼티 추가
  • 인스턴스 메서드와 타입 메서드 정의
  • 새로운 이니셜라이저 제공
  • 서브스크립트 정의
  • 새로운 중첩 타입 정의와 사용
  • 기존 타입이 프로토콜을 따르도록(conform) 만들기

Swift에서는 프로토콜 자체를 확장해서 그 요구사항의 구현을 제공하거나, 준수하는 타입이 함께 활용할 수 있는 추가 기능을 더할 수도 있어요. 자세한 내용은 프로토콜 확장을 참고해요.

Note: 확장은 타입에 새로운 기능을 더할 수는 있어도, 기존 기능을 오버라이드할 수는 없어요.

확장 문법

확장은 extension 키워드로 선언해요:

extension SomeType {
    // new functionality to add to SomeType goes here
}

확장은 기존 타입이 프로토콜을 하나 이상 채택하도록 만들 수도 있어요. 프로토콜 준수를 추가할 때는 클래스나 구조체에서 쓸 때와 똑같은 방식으로 프로토콜 이름을 적으면 돼요:

extension SomeType: SomeProtocol, AnotherProtocol {
    // implementation of protocol requirements goes here
}

이렇게 프로토콜 준수를 추가하는 방법은 확장으로 프로토콜 준수 추가하기에 설명되어 있어요.

확장으로 기존 제네릭 타입을 확장할 수도 있고, 제네릭 타입을 확장해서 조건부로 기능을 추가할 수도 있어요. 각각 제네릭 타입 확장하기제네릭 where 절이 있는 확장에서 다뤄요.

Note: 기존 타입에 새로운 기능을 추가하려고 확장을 정의하면, 그 기능은 해당 타입의 모든 기존 인스턴스에서 바로 사용할 수 있어요. 확장이 정의되기 전에 만들어진 인스턴스라도 마찬가지예요.

계산된 프로퍼티

확장은 기존 타입에 계산된 인스턴스 프로퍼티와 계산된 타입 프로퍼티를 추가할 수 있어요. 아래 예시는 Swift에 내장된 Double 타입에 계산된 인스턴스 프로퍼티 다섯 개를 추가해서, 거리 단위를 다루는 기본 기능을 제공해요:

extension Double {
    var km: Double { return self * 1_000.0 }
    var m: Double { return self }
    var cm: Double { return self / 100.0 }
    var mm: Double { return self / 1_000.0 }
    var ft: Double { return self / 3.28084 }
}
let oneInch = 25.4.mm
print("One inch is \(oneInch) meters")
// Prints "One inch is 0.0254 meters".
let threeFeet = 3.ft
print("Three feet is \(threeFeet) meters")
// Prints "Three feet is 0.914399970739201 meters".

이 계산된 프로퍼티들은 Double 값을 특정 길이 단위로 다룬다는 뜻을 담아요. 구현이 계산된 프로퍼티긴 하지만, 이 프로퍼티들의 이름을 점 문법으로 부동소수점 리터럴 값 뒤에 붙일 수 있어요. 그 리터럴 값을 거리 변환에 쓰는 방식이에요.

이 예시에서는 1.0이라는 Double 값이 "1미터"를 나타낸다고 봐요. 그래서 m 계산 프로퍼티는 self를 그대로 반환해요. 1.m이라는 표현이 1.0이라는 Double 값을 계산한다고 보는 거죠.

다른 단위는 미터 단위 값으로 표현하려면 약간의 변환이 필요해요. 1킬로미터는 1,000미터와 같으니까 km 계산 프로퍼티는 값에 1_000.00을 곱해 미터 단위 숫자로 바꿔요. 비슷하게 1미터에는 3.28084피트가 있으므로 ft 계산 프로퍼티는 기본 Double 값을 3.28084로 나눠서 피트를 미터로 변환해요.

이 프로퍼티들은 읽기 전용 계산 프로퍼티라서, 간결하게 하려고 get 키워드 없이 표현했어요. 반환 타입은 Double이라서, Double이 허용되는 곳이면 어디든 수학 계산에 쓸 수 있어요:

let aMarathon = 42.km + 195.m
print("A marathon is \(aMarathon) meters long")
// Prints "A marathon is 42195.0 meters long".

Note: 확장은 새로운 계산 프로퍼티를 추가할 수는 있어도, 저장 프로퍼티를 추가하거나 기존 프로퍼티에 프로퍼티 옵저버를 붙일 수는 없어요.

이니셜라이저

확장은 기존 타입에 새로운 이니셜라이저를 추가할 수 있어요. 덕분에 다른 타입이 여러분이 만든 커스텀 타입을 이니셜라이저 파라미터로 받아들이도록 확장하거나, 타입의 원래 구현에는 없던 초기화 옵션을 추가로 제공할 수 있어요.

확장은 클래스에 새로운 편의 이니셜라이저를 추가할 수 있지만, 지정 이니셜라이저나 디이니셜라이저를 추가할 수는 없어요. 지정 이니셜라이저와 디이니셜라이저는 항상 원래 클래스 구현에서 제공해야 해요.

모든 저장 프로퍼티에 기본값을 제공하고 커스텀 이니셜라이저를 정의하지 않은 값 타입에 확장으로 이니셜라이저를 추가한다면, 확장의 이니셜라이저 안에서 그 값 타입의 기본 이니셜라이저와 멤버와이즈 이니셜라이저를 호출할 수 있어요. 이건 값 타입의 이니셜라이저 위임에서 설명하듯, 원래 구현에서 이니셜라이저를 직접 작성했다면 안 되는 경우예요.

또 다른 모듈에서 선언된 구조체에 확장으로 이니셜라이저를 추가한다면, 그 새 이니셜라이저는 정의하는 모듈의 이니셜라이저를 호출하기 전까지는 self에 접근할 수 없어요.

아래 예시는 기하학적 사각형을 나타내는 커스텀 Rect 구조체를 정의해요. 예시는 또 SizePoint라는 두 보조 구조체를 정의하는데, 둘 다 모든 프로퍼티에 0.0의 기본값을 제공해요:

struct Size {
    var width = 0.0, height = 0.0
}
struct Point {
    var x = 0.0, y = 0.0
}
struct Rect {
    var origin = Point()
    var size = Size()
}

Rect 구조체는 모든 프로퍼티에 기본값을 제공하니까, 기본 이니셜라이저에서 설명하듯 기본 이니셜라이저와 멤버와이즈 이니셜라이저를 자동으로 받아요. 이 이니셜라이저들은 새로운 Rect 인스턴스를 만드는 데 쓸 수 있어요:

let defaultRect = Rect()
let memberwiseRect = Rect(origin: Point(x: 2.0, y: 2.0),
    size: Size(width: 5.0, height: 5.0))

Rect 구조체를 확장해서, 특정 중심점과 크기를 받는 이니셜라이저를 추가로 제공해 보아요:

extension Rect {
    init(center: Point, size: Size) {
        let originX = center.x - (size.width / 2)
        let originY = center.y - (size.height / 2)
        self.init(origin: Point(x: originX, y: originY), size: size)
    }
}

이 새 이니셜라이저는 먼저 주어진 center 점과 size 값에 따라 적절한 origin 점을 계산해요. 그다음 구조체의 자동 멤버와이즈 이니셜라이저인 init(origin:size:)를 호출해서 새 origin과 size 값을 해당 프로퍼티에 저장해요:

let centerRect = Rect(center: Point(x: 4.0, y: 4.0),
    size: Size(width: 3.0, height: 3.0))
// centerRect's origin is (2.5, 2.5) and its size is (3.0, 3.0)

Note: 확장으로 새 이니셜라이저를 제공할 때도, 초기화가 끝난 뒤 각 인스턴스가 완전히 초기화되도록 보장할 책임은 여전히 여러분에게 있어요.

메서드

확장은 기존 타입에 새로운 인스턴스 메서드와 타입 메서드를 추가할 수 있어요. 아래 예시는 Int 타입에 repetitions이라는 인스턴스 메서드를 추가해요:

extension Int {
    func repetitions(task: () -> Void) {
        for _ in 0..<self {
            task()
        }
    }
}

repetitions(task:) 메서드는 () -> Void 타입의 인자를 하나 받는데, 이건 파라미터가 없고 값을 반환하지 않는 함수를 뜻해요.

이 확장을 정의하고 나면 어떤 정수에서든 repetitions(task:) 메서드를 호출해서, 그 횟수만큼 작업을 수행할 수 있어요:

3.repetitions {
    print("Hello!")
}
// Hello!
// Hello!
// Hello!

mutating 인스턴스 메서드

확장으로 추가한 인스턴스 메서드는 인스턴스 자신을 수정(mutate)할 수도 있어요. self나 그 프로퍼티를 수정하는 구조체·열거형 메서드는, 원래 구현의 mutating 메서드처럼 mutating으로 표시해야 해요.

아래 예시는 Swift의 Int 타입에 원래 값을 제곱하는 square라는 mutating 메서드를 추가해요:

extension Int {
    mutating func square() {
        self = self * self
    }
}
var someInt = 3
someInt.square()
// someInt is now 9

서브스크립트

확장은 기존 타입에 새로운 서브스크립트를 추가할 수 있어요. 이 예시는 Swift에 내장된 Int 타입에 정수 서브스크립트를 추가해요. 이 서브스크립트 [n]은 숫자의 오른쪽에서 n번째 자리의 십진수를 반환해요:

  • 123456789[0]9를 반환
  • 123456789[1]8을 반환

… 이런 식으로 이어져요:

extension Int {
    subscript(digitIndex: Int) -> Int {
        var decimalBase = 1
        for _ in 0..<digitIndex {
            decimalBase *= 10
        }
        return (self / decimalBase) % 10
    }
}
746381295[0]
// returns 5
746381295[1]
// returns 9
746381295[2]
// returns 2
746381295[8]
// returns 7

Int 값에 요청한 인덱스만큼의 자릿수가 없으면, 서브스크립트 구현은 수를 왼쪽에 0으로 채운 것처럼 취급해서 0을 반환해요:

746381295[9]
// returns 0, as if you had requested:
0746381295[9]

중첩 타입

확장은 기존 클래스, 구조체, 열거형에 새로운 중첩 타입을 추가할 수 있어요:

extension Int {
    enum Kind {
        case negative, zero, positive
    }
    var kind: Kind {
        switch self {
        case 0:
            return .zero
        case let x where x > 0:
            return .positive
        default:
            return .negative
        }
    }
}

이 예시는 Int에 새로운 중첩 열거형을 추가해요. Kind라는 이 열거형은 특정 정수가 나타내는 수의 종류를 표현해요. 구체적으로는 그 수가 음수인지, 0인지, 양수인지를 표현해요.

이 예시는 또 Intkind라는 계산된 인스턴스 프로퍼티를 추가하는데, 이 프로퍼티는 그 정수에 맞는 Kind 열거형 케이스를 반환해요.

이 중첩 열거형은 이제 어떤 Int 값에서든 쓸 수 있어요:

func printIntegerKinds(_ numbers: [Int]) {
    for number in numbers {
        switch number.kind {
        case .negative:
            print("- ", terminator: "")
        case .zero:
            print("0 ", terminator: "")
        case .positive:
            print("+ ", terminator: "")
        }
    }
    print("")
}
printIntegerKinds([3, 19, -27, 0, -6, 0, 7])
// Prints "+ + - 0 - 0 + ".

printIntegerKinds(_:) 함수는 Int 값 배열을 입력받아 그 값들을 차례로 순회해요. 배열의 각 정수마다 그 정수의 kind 계산 프로퍼티를 살펴보고, 알맞은 설명을 출력해요.

Note: number.kind는 이미 Int.Kind 타입이라는 걸 알고 있어요. 그래서 switch 문 안에서 Int.Kind의 케이스 값들을 .negative처럼 축약형으로 쓸 수 있어요. Int.Kind.negative라고 쓸 필요가 없죠.

더 알아보기