티스토리 뷰

swift/문법

25. Swift 문법 - Generics

DevBee 2020. 10. 27. 13:04

1. Generic Function

Generic 을 활용하면 형식에 의존하지 않는 범용 코드를 작성할 수 있고, 코드의 재사용성과 유지보수의 편의성이 높아진다는 장점이 있습니다.

 

코드에는 두 개의 값을 교차하는 코드가 작성되어 있습니다.

func swapInteger(lhs: inout Int, rhs: inout Int) {
    let tmp = lhs
    lhs = rhs
    rhs = tmp
}

var a = 10
var b = 20

swapInteger(lhs: &a, rhs: &b)
a  // 20
b  // 10

 

위 함수는 Int 형식의 두 정수를 교체할 때는 문제없이 동작합니다. 하지만 a, b 두 변수가 Int 가 아니라면 컴파일 에러가 발생합니다. 파라미터의 형식으로 지정된 Int 외에 다른 형식은 받을 수 없기 때문입니다. Int 외에 두 변수를 교체하는 함수가 필요하다면 필요한 형식의 수만큼 개별적으로 구현해야 합니다. 이렇게 구현하는 경우 특별한 에러는 없지만 함수 구현이 중복된다는 문제가 있습니다.

 

Generic 함수를 이용해서 문제를 해결해보겠습니다. 먼저 문법을 살펴보겠습니다.

func name<T>(parameters) -> Type {
    code
}

 

여기에서 T 는 Type Parameter 라고 합니다. Type Parameter 는 함수 내부에서 파라미터 형식이나 리턴형으로 사용됩니다. 물론 함수 body에서 사용하는 것도 가능합니다. 보통은 이름으로 T 를 사용하지만 다른 이름을 사용하는 것도 문제는 없습니다. 다만 형식 이름으로 사용하기 때문에 UpperCamelCase 로 사용합니다. 그리고 문법에서는 <> 사이에 Type Parameter 가 하나만 선언되어 있지만 두 개 이상을 선언하는 것도 가능합니다.

 

앞에서 설명했던 함수를 Generic 함수로 구현해 보겠습니다.

func swapValue<T>(lhs: inout T, rhs: inout T) {
    let tmp = lhs
    lhs = rhs
    rhs = tmp
}

a = 1
b = 2
swapValue(lhs: &a, rhs: &b)
a  // 2
b  // 1

var c = 1.2
var d = 3.4
swapValue(lhs: &c, rhs: &d)
c  // 3.4
d  // 1.2

 

컴파일러는 전달된 형식에 적합한 코드를 자동으로 생성합니다. 전달된 형식이 Int 라면 T 가 전달된 형식인 Int 로 교체되고 Double 이라면 T 가 Double 로 교체된 코드가 생성됩니다. Generic 함수는 형식에 관계없이 하나의 구현으로 모든 자료형을 처리합니다.

 

앞에서 작성한 코드를 개선해보겠습니다. 두 값이 같은 경우에는 값을 교체할 필요가 없기 때문에 두 값이 다른 경우에만 값을 교체하도록 하겠습니다.

func swapValue<T>(lhs: inout T, rhs: inout T) {
    if lhs == rhs { return }

    let tmp = lhs
    lhs = rhs
    rhs = tmp
}

 

위와 같이 작성하면 파라미터로 비교 기능이 구현되지 않은 값도 전달될 수 있기 떄문에 에러가 발생합니다. 타입 파라미터가 대체할 수 있는 형식을 Equatable 프로토콜을 채용한 형식으로 제한하면 문제가 해결됩니다. 여기에 필요한 것이 Type Constraint 입니다. 우리 말로는 형식 제약이라고 합니다. 

 

Type Constraint 의 사용법을 보면 다음과 같습니다.

// 타입 파라미터가 대체할 수 있는 형식이 이 클래스와 이 클래스를 상속 받은 클래스로 제한됩니다.
<TypeParameter: ClassName>

// 타입 파라미터가 대체할 수 있는 형식이 이 프로토콜을 채용한 형식으로 제한됩니다.
<TypeParameter: ProtocolName>
func swapValue<T: Equatable>(lhs: inout T, rhs: inout T) {
    if lhs == rhs { return }
    
    let tmp = lhs
    lhs = rhs
    rhs = tmp
}

 

Generic 함수는 기본적으로 형식에 관계없이 동일한 코드를 실행하지만, 특수화를 통해 특정 형식을 위한 코드를 작성할 수 있습니다.

func swapValue<T: Equatable>(lhs: inout T, rhs: inout T) {
	print("generic version")
    
    if lhs == rhs { return }
    
    let tmp = lhs
    lhs = rhs
    rhs = tmp
}

func swapValue(lhs: inout String, rhs: inout String) {
    print("specialized version")
    
    if lhs.caseInsensitiveCompare(rhs) == .orderedSame {
        return
    }
    
    let tmp = lhs
    lhs = rhs
    rhs = tmp
}

a = 1
b = 2
swapValue(lhs: &a, rhs: &b)

// 위와 같이 String 이 아닌 나머지 형식을 전달하면 첫 번째 함수가 호출됩니다.
// 결과: generic version

var e = "Swift"
var f = "Programming"
swapValue(lhs: &e, rhs: &f)

// 결과: specialiized version

 

특수화를 한 두 번째 함수는 첫 번째 함수를 오버라이딩한 함수로 인식됩니다. 그리고 제네릭 함수보다 우선순위가 높습니다. 그래서 모든 함수가 문자열을 받을 수 있지만 우선순위가 높은 두 번째 함수가 호출됩니다. 

 

2. Generic Types

Swift 에서 Collection 은 모두 구조체로 구현되어 있고 Generic Type 입니다. Collection 에는 동일한 형식의 값만 저장할 수 있다고 알고 있는데 Generic Type 을 이해하면 그 이유가 명확해집니다.

 

먼저 문법을 살펴보겠습니다.

class Name<T> {
    code
}

struct Name<T> {
    code
}

enum Name<T> {
    code
}

 

제네릭 타입은 새로운 형식이 아니라 기존 형식에 타입 파라미터를 추가하면 제네릭 타입으로 선언됩니다. 타입 파라미터는 형식 이름 뒤에 선언합니다. 그리고 문법은 제네릭 함수에서 살펴본 것과 동일합니다. 형식 제약 문법 또한 동일합니다. 제네릭 타입에서는 속성의 자료형, 메소드의 리턴형, 파라미터 형식처럼 형식 내부에서 사용되는 형식들을 타입 파라미터로 대체할 수 있습니다.

 

간단한 예를 살펴보겠습니다.

struct Color<T> {
    var red: T
    var green: T
    var blue: T
}

var c = Color(red: 128, green: 80, blue: 200)
c

 

c 의 형식을 확인해보면 Color<Int> 입니다. Int 는 T 를 대체하는 형식입니다. T 가 Int 로 대체된 이유는 생성자를 호출하면서 파라미터로 Int 형식의 값을 전달했기 때문입니다. 그리고 속성을 확인해보면 모든 형식이 Int 입니다.

c = Color(red: 128.0, green: 80.0, blue: 200.0)

 

위 코드는 에러가 발생합니다. c 는 이미 Color<Int> 형식이기 때문에 Color<Double> 형식인 새로운 인스턴스를 저장할 수 없습니다.

let d: Color = Color(red: 128.0, green: 80.0, blue: 200.0)

 

위와 같이 직접 형식을 지정할 때 형식 이름만 지정하여도 문제되지 않습니다. 이 경우에는 자동으로 Type Parameter 의 형식이 추론되기 때문입니다. 하지만 값을 지정하는 코드를 제거하면 더 이상 형식을 추론할 수 없기 떄문에 에러가 발생합니다. 이 경우에는 Type Parameter 의 형식을 직접 추론해야 합니다.

 

Generic 타입을 확장해 보겠습니다.

extension Color {
    func getComponents() -> [T] {
        return [red, green, blue]
    }
}

 

Generic 타입을 확장할 때는 타입 파라미터를 형식 이름 뒤에 추가하지 않습니다. 따라서 형식에서 선언한 타입 파라미터 이름을 변경하는 것은 불가능합니다. Extension 을 통해 추가한 멤버는 기본적으로 타입 파라미터에 관계없이 모든 컬러 형식에 추가됩니다.

let intColor = Color(red: 1, green: 2, blue: 3)
intColor.getComponents()

let dbColor = Color(red: 1.0, green: 2.0, blue: 3.0)
dbColor.getComponents()

 

Extension 에서도 확장 대상을 제한하는 것이 가능합니다.

extension Color where T: FixedWidthInteger {
    func getComponents() -> [T] {
        return [red, green, blue]
    }
}

 

위에서 where 절을 통해 작성한 FixedWidthInteger 프로토콜은 Int 형식은 채용하고 있지만 Double 형식은 채용하고 있지 않아 Color<Double> 형식에는 확장에서 구현한 메소드가 추가되지 않습니다. 따라서 위에서 작성한 dbColor.getComponents() 에서 에러가 발생합니다.

 

지금은 FixedWidthInteger 프로토콜을 채용한 모든 형식에 해당 메소드가 추가되기 때문에 Int 에만 추가하고 싶다면 where T == Int 로 대상 형식을 직접 지정하면 됩니다.

 

3. Associated Types

이번에는 제네릭 프로토콜을 선언하는 방법을 알아보겠습니다. 제네릭 타입을 선언할 때는 Associated Type 이 필요합니다. 우리말로는 연관 형식이라고 합니다. 연관 형식은 타입 파라미터와 마찬가지로 프로토콜 내에서 실제 형식으로 대체되는 placeholder 입니다.

 

문법을 살펴보면 다음과 같습니다.

associatedtype Name

 

간단한 프로토콜을 생성하고 연관 형식을 선언해보겠습니다.

protocol QueueCompatible {
    associatedtype Element
    
    func enqueue(value: Element)
    func dequeue() -> Element?
}

 

연관 형식은 프로토콜에서 사용하는 placeholder 로 요구사항을 선언하는 것은 아닙니다. 그래서 프로토콜을 채용한 형식에서 다시 연관 형식을 선언할 필요는 없습니다. 연관 형식을 대체하는 실제 형식은 프로토콜을 채용한 형식에서 결정됩니다. 연관 형식이 선언된 프로토콜을 채용하는 형식은 다음과 같은 문법을 통해 연관 형식의 실제 형식을 선언해야 합니다.

typealias AssociatedTypeName = Type

 

간단한 클래스를 생성하고 위에서 작성한 프로토콜을 채용해보겠습니다.

class IntegerQueue: QueueCompatible {
    typealias Element = Int
    
    func enqueue(value: Int) {
        
    }
    
    func dequeue() -> Int? {
        return 0
    }
}

class DoubleQueue: QueueCompatible {
    
    func enqueue(value: Double) {
        
    }
    
    func dequeue() -> Double? {
        return 0
    }
}

 

위에서 작성한 DoubleQueue 클래스와 같이 파라미터 형식과 리턴형을 선언하는 경우 연관 형식이 추론될 수 있습니다. 사용된 실제 형식을 통해 연관 형식을 추론할 수 있기 때문에 연관 형식을 생략해도 아무런 문제가 없습니다.

 

연관 형식에 제약을 추가하는 문법은 타입 파라미터와 동일합니다. 연관 형식을 Equatable 프로토콜을 채용한 형식으로 제한하고 싶다면 다음과 같이 작성하면 됩니다.

protocol QueueCompatible {
    associatedtype Element: Equatable
    
    func enqueue(value: Element)
    func dequeue() -> Element?
}
댓글
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
«   2024/12   »
1 2 3 4 5 6 7
8 9 10 11 12 13 14
15 16 17 18 19 20 21
22 23 24 25 26 27 28
29 30 31
글 보관함