호댕의 iOS 개발

[Swift 기본문법] Generic & Result 공식문서 보기 본문

Software Engineering/Swift

[Swift 기본문법] Generic & Result 공식문서 보기

호르댕댕댕 2022. 4. 26. 22:13

 

Swift Language Guide앨런 Swift 문법 마스터 강의를 바탕으로 작성한 글입니다. 

 

앨런 Swift문법 마스터 스쿨 (온라인 BootCamp - 2개월과정) - 인프런 | 강의

Swift문법을 제대로 이해, 활용해보고자 하는 철학을 바탕으로 과정이 설계되었습니다. 코딩에 대해 1도 모르는 비전공자를 시작으로 네카라쿠배에 입사할 수 있는 초고급 수준까지 올리는 것을

www.inflearn.com

 

Generics — The Swift Programming Language (Swift 5.7)

Generics Generic code enables you to write flexible, reusable functions and types that can work with any type, subject to requirements that you define. You can write code that avoids duplication and expresses its intent in a clear, abstracted manner. Gener

docs.swift.org

 

🧐 Generic의 필요성

일단 제네릭은 어떤 타입에서도 유연하고 재사용가능한 함수나 타입을 만들기 위해 사용한다. 

 

func swapTwoInts(_ a: inout Int, _ b: inout Int) {
    let temporaryA = a
    a = b
    b = temporaryA
}

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

만약 이런 메서드가 있다고 생각해보자. 

만약 String을 swap하고 싶다면 swapTwoStrings라는 메서드를 추가로 구현해야 할 것이다. 

만약 Double도 이렇게 구현하고 싶다면?

 

타입을 제외하고 거의 동일한 코드를 계속 반복해서 작성해야 한다. 

 

따라서 등장한 것이 Generic이다. 

func swapTwoValues<T>(_ a: inout T, _ b: inout T) {
    let temporaryA = a
    a = b
    b = temporaryA
}

타입 파라미터를 지정해주고 이를 타입이 필요한 곳에 사용해주면 된다. 

(타입 파라미터의 경우 항상 Upper Camel Case를 사용해야 한다, 이는 value가 아니며 타입을 나타낸다)

 

이는 함수를 호출하면서 실제 필요한 타입을 작성해주면 해당 타입으로 치환되어 작동한다. 

 

💡 실제 Swift에서 제네릭이 사용된 부분

이런 제네릭 문법은 실제로 Array, Set, Dictionary 같은 Collection Type이나 map같은 고차함수 등에서도 다양하게 사용되고 있다. 

 

// Array 선언부
@frozen public struct Array<Element> { }

Array가 선언된 부분처럼 타입에도 제네릭을 사용해줄 수 있다. 

 

⌨️  Generic 사용 방법

🔶 Generic 타입의 Extension

제네릭 타입을 확장할 때에는 extension 정의부에 타입 파라미터를 작성하지 않는다. 그래도 Body에선 타입 파라미터를 사용할 수 있으며, 해당 타입 파라미터의 이름은 기존에 정의된 곳에 있는 타입 파라미터를 참조한다. 

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

extension Stack {
    var topItem: Element? {
        return items.isEmpty ? nil : items[items.count - 1]
    }
}

 

🔶 Generic 타입의 제약

위에서 나왔던 예시들에선 어떤 타입에서든 작업이 가능했다. 

하지만 제네릭 함수와 타입을 조금 더 구제적인 타입으로 제약할 수 있다. 이는 타입 파라미터가 반드시 특정 Class를 상속받거나, 프로토콜을 준수하도록 구현한다. 

 

가장 대표적인 예시는 바로 Dictionary이다. 

@frozen public struct Dictionary<Key, Value> where Key : Hashable { }

Dictionary에선 key의 값이 항상 Hashable 프로토콜을 채택하도록 하여 유일한 값이 되도록 한 것이다. 

 

위처럼 제약을 where절을 통해 걸 수도 있지만 아래처럼 제약을 부여할 수도 있다. 

func someFunction<T: SomeClass, U: SomeProtocol>(someT: T, someU: U) {
    // function body goes here
}

이렇게 되면 T 타입의 경우 항상 SomeClass와 상속관계가 있는 클래스만 사용이 가능하며, U타입의 경우 SomeProtocol에 정의해놓은 것들을 따라야 한다. 

 

🔶 Associated Type

프로토콜을 정의할 때 종종 하나 이상의 associatedtype을 선언하면 유용하다. 

이는 프로토콜 내에서 사용되는 Type의 placeholder name을 정의할 수 있도록 한다. 

 

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

그리고 해당 프로토콜을 채택하여 사용할 때에는 아래 코드처럼 typealias를 사용해주면 된다. 

struct IntStack: Container {
    // 원래 구현부
    var items: [Int] = []
    mutating func push(_ item: Int) {
        items.append(item)
    }
    mutating func pop() -> Int {
        return items.removeLast()
    }
    // 프로토콜 준수하기 위해 작성한 부분
    typealias Item = Int
    mutating func append(_ item: Int) {
        self.push(item)
    }
    var count: Int {
        return items.count
    }
    subscript(i: Int) -> Int {
        return items[i]
    }
}

typealias를 생략하더라도 다른 부분에서 명시적으로 타입을 작성해준다면 이는 생략도 가능하다. 

 

여기서 중요한 것은 프로토콜에선 타입 파라미터를 통해 제네릭을 사용하지 않고 associatedtype으로 선언을 해야한다는 것이다. 

 

🔶 Generic Subscript

함수, 타입에도 Generic을 사용했던 것처럼 Subscript에도 Generic을 사용할 수 있다. 

이는 함수에서 사용하는 것과 거의 유사하다. 

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

여기서는 Indices라는 타입 파라미터를 사용해 Generic을 구현했고, Indices는 Sequence 프로토콜을 채택하여 for-in 문이나 contains 같은 메서드를 사용할 수 있게 된다. 

 

👨🏻‍⚖️ Result 타입

Result 타입도 제네릭으로 선언되어 있다. 

 

@frozen enum Result<Success, Failure> where Failure : Error

또한 Result 타입은 열거형으로 되어 있으며 case로는 success와 failure를 가지고 있고 각 타입에 해당하는 연관값을 가지게 된다

@frozen public enum Result<Success, Failure> where Failure : Error {

    /// A success, storing a `Success` value.
    case success(Success)

    /// A failure, storing a `Failure` value.
    case failure(Failure)
}

Failure는 타입 제약으로 반드시 Error 프로토콜을 준수하는 타입이 와야 한다. 

여기서 @(attribute)frozen은 만들 수 있는 타입의 종류가 변하지 않는다고 선언하는 것이다. 

즉, 미래의 라이브러리에서 선언이 추가, 제거, 재정렬되지 않고 열거형의 케이스나 구조체의 저장 프로퍼티가 추가되지 않는다는 것을 명시해주는 것이다. 

따라서 이를 사용하게 되면 열거형을 사용할 때 default를 사용해주지 않아도 된다.

 

 

기존 에러 핸들링을 하는 방식을 보게 되면 에러를 throw로 던지고 함수에는 명시적으로 에러가 발생할 수 있다는 것을 보여주기 위해 throws 키워드를 반환 타입 작성 전에 적도록 되어 있었다. 

func vend(itemNamed name: String) throws {
    guard let item = inventory[name] else {
        throw VendingMachineError.invalidSelection
    }

    guard item.count > 0 else {
        throw VendingMachineError.outOfStock
    }

    guard item.price <= coinsDeposited else {
        throw VendingMachineError.insufficientFunds(coinsNeeded: item.price - coinsDeposited)
    }

    coinsDeposited -= item.price

    var newItem = item
    newItem.count -= 1
    inventory[name] = newItem

    print("Dispensing \(name)")
}

위의 코드처럼 말이다. 

 

이런 함수를 사용하려면 항상 try를 붙여줘야 하고, do-catch 문을 통해 에러도 처리를 해줘야 했다. 

 

하지만 Result 타입을 사용할 경우 함수를 정의하는 부분에 어떤 에러 타입을 사용할 지 명시적으로 작성이 가능하다. 

enum JSONParserError: Error, LocalizedError {
    case decodingFail
    case encodingFail
    
    var errorDescription: String? {
        switch self {
        case .decodingFail:
            return "디코딩에 실패했습니다."
        case .encodingFail:
            return "인코딩에 실패했습니다."
        }
    }
}

func encode(from item: Item?) -> Result<Data, JSONParserError> {
    guard let item = item else {
        return .failure(.encodingFail)
    }

    let encoder = JSONEncoder()
    encoder.keyEncodingStrategy = .convertToSnakeCase
    encoder.dateEncodingStrategy = .formatted(DateFormatter.shared)

    guard let encodedData = try? encoder.encode(item) else {
        return .failure(.encodingFail)
    }

    return .success(encodedData)
}

에러 처리도 switch 문을 통해 간단하게 성공, 실패의 경우로 나누어 처리를 할 수 있다. 

Comments