제네릭(Generic)을 이용해 코드를 구현하면 어떤 타입에도 유연하게 대응할 수 있습니다.
제네릭으로 구현한 기능과 타입은 재사용하기도 쉽고, 코드의 중복을 줄일 수 있기에 깔끔하고 추상적인 표현이 가능합니다.

// 제네릭을 사용하지 않은 예
func swapTwoInts(_ a: inout Int, _ b: inout Int) {
    let temporaryA: Int = a
    a = b
    b = temporaryA
}

func swapTwoStrings(_ a: inout String, _ b: inout String) {
    let temporaryA: String = a
    a = b
    b = temporaryA
}


var numberOne: Int = 5
var numberTwo: Int = 10

swapTwoInts(&numberOne, &numberTwo)

var stringOne: String = "A"
var stringTwo: String = "B"

swapTwoStrings(&stringOne, &stringTwo)

Double 타입의 값 교환을 원한다면 또 다른 함수를 구현해야 합니다. 그리고 타입마다 다른 함수를 써줘야 하는 불편함도 있습니다.

22.1 제네릭 함수

제네릭 함수를 사용하면, 같은 타입인 두 변수의 값을 교환한다는 목적을 타입에 상관없이 할 수 있도록 단 하나의 함수로 구현할 수 있습니다.

// 제네릭을 사용한 예
func swapTwoValues<T>(_ a: inout T, _ b: inout T) {
    let temporaryA: T = a
    a = b
    b = temporaryA
}

swapTwoValues(&numberOne, &numberTwo)

swapTwoValues(&stringOne, &stringTwo)

swapTwoValues(&numberOne, &stringOne) // ERROR! 같은 타입끼리만 교환 가능

제네릭 함수는 실제 타입 이름을 써주는 대신에 플레이스홀더(Placeholder, 위 예제 함수에서는 T)를 사용합니다.
플레이스홀더 타입 T의 실제 타입은 함수가 호출되는 그 순간 결정됩니다.

또한 플레이스홀더 타입 T는 함수의 매개변수의 타입으로 사용할 수 있으며,
함수의 반환 타입으로 사용할 수도 있으며,
함수 내부 변수의 타입 지정을 위해 사용할 수도 있습니다.

타입 매개변수의 이름은 타입 이름이기도 하므로 대문자 카멜케이스를 사용하여 표현합니다.
ex) T, U, V, Key, Value, Element

22.2 제네릭 타입

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

모든 타입을 대상으로 동작할 수 있는 스택 기능을 구현해보겠습니다.

// 제네릭을 사용한 Stack 구조체 타입
struct Stack<Element> {
    var items = [Element]()
    mutating func push(_ item: Element) {
        items.append(item)
    }
    mutating func pop() -> Element {
        return items.removeLast()
    }
}

// Element의 타입으로 Double을 사용 
var doubleStack: Stack<Double> = Stack<Double>()

doubleStack.push(1.0)
print(doubleStack.items)    // [1.0]
doubleStack.push(2.0)
print(doubleStack.items)    // [1.0, 2.0]
doubleStack.pop()
print(doubleStack.items)    // [1.0]

// Element의 타입으로 String을 사용 
var stringStack: Stack<String> = Stack<String>()

stringStack.push("1")
print(stringStack.items)    // ["1"]
stringStack.push("2")
print(stringStack.items)    // ["1", "2"]
stringStack.pop()
print(stringStack.items)    // ["1"]

만약 요소로 모든 타입을 수용할 수 있도록 구현하려고 한다면

  1. Stackitem 배열을 Any 타입으로 정의하는 방법
  2. 제네릭을 사용하여 Element의 타입으로 Any를 사용하는 방법
// 1. Stack의 item 배열을 Any 타입으로 정의하는 방법
struct AnyStack {
    var items = [Any]()
    mutating func push(_ item: Any) {
        items.append(item)
    }
    mutating func pop() -> Any {
        return items.removeLast()
    }
}

var anyStack: AnyStack = AnyStack()

anyStack.push(1.0)
print(anyStack.items)   // [1.0]
anyStack.push("2")
print(anyStack.items)   // [1.0, "2"]
anyStack.push(3)
print(anyStack.items)   // [1.0, "2", 3]
anyStack.pop()
print(anyStack.items)   // [1.0, "2"]
// 2. 제네릭을 사용하여 Element의 타입으로 Any를 사용하는 방법 
struct Stack<Element> {
    var items = [Element]()
    mutating func push(_ item: Element) {
        items.append(item)
    }
    mutating func pop() -> Element {
        return items.removeLast()
    }
}

var anyStack: Stack<Any> = Stack<Any>()

anyStack.push(1.0)
print(anyStack.items)   // [1.0]
anyStack.push("2")
print(anyStack.items)   // [1.0, "2"]
anyStack.push(3)
print(anyStack.items)   // [1.0, "2", 3]
anyStack.pop()
print(anyStack.items)   // [1.0, "2"]

두 가지 방법 중, 제네릭을 사용했을 때 훨씬 유연하고 광범위하게 사용할 수 있으며, Element의 타입을 정해주면 그 타입에만 동작하도록 제한할 수 있어 더욱 안전하고 의도한 대로 기능을 사용하도록 유도할 수 있습니다.

22.3 제네릭 타입 확장

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

extension Stack<Element> { // error

extension Stack {
    var topElement: Element? { // 기존의 제네릭 정의에 명시된 Element 
        return self.items.last
    }
}

22.4 타입 제약

타입 제약(Type Constraints)은 타입 매개변수가 가져야 할 제약사항을 지정할 수 있는 방법입니다.
타입 매개변수 자리에 사용할 실제 타입이 특정 클래스를 상속받은 타입이어야 한다든지, 특정 프로토콜을 준수하는 타입이어야 한다는 등의 제약을 줄 수 있습니다.
타입 제약은 클래스 타입 또는 프로토콜로만 줄 수 있습니다. (열거형, 구조체 타입 X)

// T는 BinaryInteger 프로토콜을 준수하는 타입   
func swapTwoValues<T: BinaryInteger>(_ a: inout T, _ b: inout T) {
  // ...
}

여러 제약을 추가하고 싶다면 where 절을 활용합니다.

// T는 BinaryInteger 프로토콜과 FloatingPoint 프로토콜을 준수하는 타입   
func swapTwoValues<T: BinaryInteger>(_ a: inout T, _ b: inout T) where T: FloatingPoint {
  // ...
}

Tip 타입 제약에 자주 사용할 만한 프로토콜에는Hashable , Equatable, Comparable, Indexable, IteratorProtocol, Error, Collection, CustomStringConvertible 등이 있습니다.

22.5 프로토콜의 연관 타입

연관 타입(Associated Type)은 프로토콜에서 사용할 수 있는 플레이스홀더 이름입니다.
어떤 타입이 들어올지 모를 때, 연관 타입을 통해 '종류는 알 수 없지만, 어떤 타입이 여기에 쓰일 것이다'라고 표현해주는 역할을 프로토콜에서 수행할 수 있도록 만들어진 기능입니다.

protocol Container {
    associatedtype ItemType // 연관 타입

    mutating func append(_ item: ItemType)

    var count: Int { get }

    subscript(i: Int) -> ItemType { get }
}

struct DoubleStack: Container {

    var items = [Double]()
    mutating func push(_ item: Double) {
        items.append(item)
    }
    mutating func pop() -> Double {
        return items.removeLast()
    }

    // Container 프로토콜 준수를 위한 구현
    mutating func append(_ item: Double) {
        self.push(item)
    }

         var count: Int {
           return items.count
    }

    subscript(i: Int) -> Double {
        return items[i]
    }
}

만약 ItemType을 어떤 타입으로 사용할지 조금 더 명확히 해주고 싶다면,
구현부에 typealias ItemType = Double 라고 타입 별칭을 지정해줄 수 있습니다.

struct DoubleStack: Container {
    typealias ItemType = Double

    var items = [ItemType]()
    mutating func push(_ item: ItemType) {
        items.append(item)
    }
    mutating func pop() -> ItemType {
        return items.removeLast()
    }

    // Container 프로토콜 준수를 위한 구현
    mutating func append(_ item: ItemType) {
        self.push(item)
    }
    var count: Int {
        return items.count
    }
    subscript(i: Int) -> ItemType {
        return items[i]
    }
}

책에서는 count 타입과 서브스크립트 매개변수 i 의 타입 또한 ItemType 으로 명시되어있는데, 아이템의 개수와 인덱스 값을 의미하므로 Int 로 명시해주는 것이 더 좋을 것같아 예제를 조금 수정했습니다.

22.6 제네릭 서브스크립트

제네릭 메서드를 구현할 수 있었던 것처럼 서브스크립트도 제네릭을 활용할 수 있습니다.

extension Stack {
    subscript<Indices: Sequence>(indices: Indices) -> [Element]
        where Indices.Iterator.Element == Int {
            var result = [ItemType]()
            for index in indices {
                result.append(self[index])
            }
            return result
    }
}

var integerStack: Stack<Int> = Stack<Int>()
integerStack.append(1)
integerStack.append(2)
integerStack.append(3)
integerStack.append(4)
integerStack.append(5)

print(integerStack[0...2])  // [1, 2, 3]

야곰, 스위프트 프로그래밍

 

' > Swift' 카테고리의 다른 글

17. 서브스크립트  (0) 2021.01.30

클래스, 구조체, 열거형에는 컬렉션, 리스트, 시퀀스 등 타입의 요소에 접근하는 단축 문법인 서브스크립트(Subscript)를 정의할 수 있다.

// 함수 사용
someArray.element(at: index)
someDictionary.value(forKey: key)

// 서브스크립트 사용
someArray[index]
someDictionary[key]

17.1 서브스크립트 문법

서브스크립트 정의 문법

subscript(index: Int) -> Int {
    get {
        // 적절한 서브스크립트 결괏값 반환
    }

    set(newValue) { // 매개변수를 따로 명시해주지 않으면 설정자의 암시적 전달인지 newValue 사용 가능
        // 적절한 설정자 역할 수행
    }
}

읽기 전용 서브스크립트 정의 문법

subscript(index: Int) -> Int {
    get {
        // 적절한 서브스크립트 결괏값 반환
    }
}

subscript(index: Int) -> Int {
    // 적절한 서브스크립트 결괏값 반환
}

17.2 서브스크립트 구현

서브스크립트는 자신이 가지는 컬렉션, 리스트, 시퀀스 등의 요소를 반환하고 설정할 때 주로 사용합니다.

함수와 마찬가지로 서브스크립트는 여러 개의 매개변수를 가질 수 있고, 매개변수 기본값을 가질 수 있습니다.

그렇지만 입출력 매개변수(in-out parameters)[각주:1]는 가질 수 없습니다.

struct Student {
    var name: String
    var number: Int
}

class School {

    var number: Int = 0
    var students: [Student] = [Student]()

    func addStudent(name: String) {
        let student: Student = Student(name: name, number: self.number)
        self.students.append(student)
        self.number += 1
    }

    func addStudents(names: String...) {
        for name in names {
            self.addStudent(name: name)
        }
    }

    subscript(index: Int = 0) -> Student? {
        if index < self.number {
            return self.students[index]
        }
        return nil
    }
}

let highSchool: School = School()
highSchool.addStudents(names: "MiJeong","JuHyun", "JiYoung", "SeongUk", "MoonDuk")

print(highSchool[1]?.name) // Optional("JuHyun")
print(highSchool[]?.name)  // Optional("MiJeong")

17.3 복수 서브스크립트

클래스와 구조체는 필요한 만큼 얼마든지 서브스크립트를 구현할 수 있습니다.

서브스크립트를 여러 개 구현해도 외부에서 서브스크립트를 사용할 때 전달한 값의 타입을 유추하여 적절한 서브스크립트를 선택하여 실행합니다.

이렇게 여러 서브스크립트를 한 타입에 구현하는 것을 서브스크립트 중복 정의(Subscript Overloading)라고 합니다.

struct Student {
    var name: String
    var number: Int
}

class School {

    var number: Int = 0
    var students: [Student] = [Student]()

    func addStudent(name: String) {
        let student: Student = Student(name: name, number: self.number)
        self.students.append(student)
        self.number += 1
    }

    func addStudents(names: String...) {
        for name in names {
            self.addStudent(name: name)
        }
    }

    // 1.
    subscript(index: Int) -> Student? {
        get {
            if index < self.number {
                return self.students[index]
            }
            return nil
        }

        set {
            guard var newStudent: Student = newValue else {
                return
            }

            var number: Int = index

            if index > self.number {
                number = self.number
                self.number += 1
            }

            newStudent.number = number
            self.students[number] = newStudent
        }
    }

    // 2.
    subscript(name: String) -> Int? {
        get {
            return self.students.filter{ $0.name == name }.first?.number
        }

        set {
            guard var number: Int = newValue else {
                return
            }

            if number > self.number {
                number = self.number
                self.number += 1
            }


            let newStudent: Student = Student(name: name, number: number)
            self.students[number] = newStudent
        }
    }

    // 3.
    subscript(name: String, number: Int) -> Student? {
        return self.students.filter{ $0.name == name && $0.number == number }.first
    }
}

let highSchool: School = School()
highSchool.addStudents(names: "MiJeong","JuHyun", "JiYoung", "SeongUk", "MoonDuk")

// 1.
// subscript(index: Int) -> Student?
print(highSchool[1]) // Optional(Student(name: "JuHyun", number: 1))

// 2.
// subscript(name: String) -> Int?
print(highSchool["MiJeong"]) // Optional(0)
print(highSchool["DongJin"]) // nil

highSchool[0] = Student(name: "HongEui", number: 0)
highSchool["MangGu"] = 1

print(highSchool["JuHyun"])      // nil
print(highSchool["MangGu"])      // Optional(1)

// 3.
// subscript(name: String, number: Int) -> Student?
print(highSchool["SeongUk", 3])  // Optional(Student(name: "SeongUk", number: 3))
print(highSchool["HeeJin", 3])   // nil

이처럼 서브스크립트는 메서드인듯 아닌듯, 연산 프로퍼티인 듯 아닌 듯 중간 형태를 띄며 인스턴스 이름 뒤에 대괄호만 써서 편리하게 내부 값에 접근하고 설정해줄 수 있습니다.

17.4 타입 서브스크립트

타입 서브스크립트는 인스턴스가 아니라 타입 자체에서 사용할 수 있는 서브스크립트입니다.

타입 서브스크립트를 구현하려면 정의할 때 static 키워드를 붙여주면 됩니다. 클래스의 경우에는 class 키워드를 사용할 수도 있습니다.[각주:2]

enum School: Int {
    case elementary = 1, middle, high, university

    static subscript(level: Int) -> School? {
        return Self(rawValue: level)
        // return School(rawValue: level)와 동일
    }
}

let school: School? = School[2]
print(school)    // School.middle

야곰, 스위프트 프로그래밍

  1. 값이 아닌 참조를 전달하려면 입출력 매개변수를 사용 (C언어의 포인터와 유사함) [본문으로]
  2. 클래스에서 static 키워드와 class 키워드의 차이:
    static 으로 정의하면 상속 후 메서드 재정의가 불가능
    class 으로 정의하면 상속 후 메서드 재정의가 가능 [본문으로]

' > Swift' 카테고리의 다른 글

22. 제네릭  (0) 2021.01.30

+ Recent posts