본문 바로가기

Swift

Swift(4) - Generic(제네릭)이란 무엇일까?

오랜만에 Swift 관련 내용을 정리하는데요. 이번에는 제네릭에 관한 내용을 정리해볼까 합니다. 

시작해볼게요!


Generic(제네릭)

  • '포괄적인' 이라는 뜻을 가진 제네릭은 Swift의 강력한 기능 중 하나로 소개되고 있습니다.
  • 제네릭을 이용한다면 타입에 유연하게 대처하는 것이 가능해집니다. (이 내용은 아래 코드와 함께 부연 설명을 하도록 하겠습니다!)
  • 제네릭으로 구현한 기능과 타입은 재사용 에 용이하며, 코드의 중복을 줄일 수 있어 깔끔한 표현 이 가능합니다!
  • 제네릭의 예시로는 Array, Dictionary, Set 등이 있는데요! 예를 들어 배열을 생성할 때 상황에 맞게 Int 형 혹은 String 타입을 요소로 갖는 배열을 만드는 것이 가능했던 이유가 다 제네릭 덕분입니다!!
  • 아직 제네릭을 구현하는 방법이나 제네릭을 이해하기는 조금 어려우실거라 생각이 들어 코드를 통해 예시를 들어보겠습니다:)
// 제네릭을 사용하지 않았을때
func swapTwoInts(_ a: inout Int, _ b: inout Int) {
    let temporaryA: Int = a
    a = b
    b = temporaryA
}

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

swapTwoInts(&numberOne, &numberTwo)
print("\(numberOne), \(numberTwo)")

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


var stringOne = "Minho"
var stringTwo = "iOS"

swapTwoStrings(&stringOne, &stringTwo)
print("\(stringOne), \(stringTwo)")

  • 이러한 출력값이 나오는 것을 확인하실 수 있으실 텐데요. 이 함수들은 변수의 값을 서로 변경해주는 함수임을 알 수 있습니다. 
  • 하지만 똑같은 기능이지만 타입이 다르다는 이유로 2개의 함수를 구현해야 하는데요! (그렇다면 제네릭을 이용하면 어떻게 될까요????)
func swapTwoValues<T>(_ a: inout T, _ b: inout T) {
    let temporaryA: T = a
    a = b
    b = temporaryA
}

swapTwoValues(&numberOne, &numberTwo)
print("\(numberOne), \(numberTwo)")

swapTwoValues(&stringOne, &stringTwo)
print("\(stringOne), \(stringTwo)")
  • 제네릭을 이용하여 구현한다면 각각의 타입마다 메서드를 작성해주지 않아도 됩니다! (굉장히 편리하고 유용한 기능이죠??)
  • 주의!!!!  ab의 타입이 같아야 합니다. a와 b가 같은 타입이 아닌 경우에, 그 값들을 바꿔주는 것은 불가능합니다. Swift는 타입에 안전한(type-safe) 언어이기 때문에 String타입의 변수와 Double타입의 변수의 값을 각각 다른 값으로 바꿔주는 것을 허용하지 않습니다. 이를 시도하려고 하면 컴파일 오류가 발생하게 됩니다!!

 

  • 혹시 제네릭으로 구현한 메서드와 타입을 지정해준 메서드의 차이점을 아시겠나요?? 제네릭으로 구현한 메서드의 뒤에는 <T> 를 추가해준 것이고 매개변수의 타입도 달랑 T 라고만 쓰여 있는 것을 확인하실 수 있습니다.
  • 제네릭 함수는 실제 타입(Int, Double, String 등)을 써주는 대신에 플레이스 홀더 (위에서는 T)를 사용합니다!
    • 플레이스 홀더가 무엇인지 궁금하실 텐데요! 
    • 플레이스 홀더(T) 는 타입의 종류를 지정해주지는 않지만 어떤 타입이 들어오게 될 것이라는 것을 의미합니다!!(사용자가 어떤 타입을 사용하냐에 따라 달라진다는 의미이겠죠??)
    • 제네릭 함수의 플레이스 홀더를 지정하는 방법은 <> 안에 플레이스 홀더 이름을 나열하면 됩니다!

 

  • 제네릭 함수 뿐만 아니라 제네릭 타입 을 구현하는 것도 가능합니다!
  • 제네릭 타입을 구현하면 사용자 정의 타입인 구조체, 클래스, 열거형 등이 어떤 타입과도 연관되어 동작이 가능합니다. 이는 Array와 Dictionary 타입이 자신의 요소로 모든 타입을 대상으로 동작하는 것과 비슷하다고 생각하시면 됩니다ㅎㅎ
  • 제네릭 타입 구현 예시로. 제가 Stack을 공부하며 Swift로 구현하셨던 거 기억나시나요? 기억이 안 나신다면 아래를 참고해주세요!
 

자료구조(1) - 스택(Stack)이란 무엇일까?

안녕하세요! 알고리즘 공부를 위해 자료구조를 시작하려고 합니다. 학교에서 수업을 들은 이후(부끄럽지만) 다시 공부를 하지 않았기 때문에 다시 꾸준히 공부해서 자료구조에 관한 내용도 간간이 적도록 하겠습니..

minosaekki.tistory.com

  • 제가 구현한 스택은 Int 타입에 대해서만 동작이 가능합니다. 이번에는 제네릭을 이용하여 Int 타입 이외에도 다양한 타입이 가능하게 해 보겠습니다!!
import Foundation

class Stack<T> {

    var arr = [T]()

    func push(_ element: T) {
        arr.append(element)
    }
    
    @discardableResult func pop() -> T? {
        guard !arr.isEmpty else {
            print("Stack이 비었기 때문에 Pop 연산이 불가합니다.")
            return nil
        }
        
        return arr.removeLast()
    }

    func printSelf() {

        print("  현재스택   ")
        print("--- top ---")

        arr.reversed().forEach {
            print($0)
        }

        print("-----------")
    }
}

var stringStack = Stack<String>()
var intStack = Stack<Int>()

 

  • 이렇게 제네릭으로 구현한다면 원하는 타입에 맞춰서 함수를 사용하는 것이 가능해집니다!

 

타입 제약(Type Constraints)

  • 지금까지 살펴본 제네릭 기능의 타입 매개변수는 실제 사용지에 타입에 대한 제약이 없었습니다.
  • 하지만 종종 제네릭 함수가 처리해야 할 기능이 특정 타입에 한정 되어야만 처리가 가능하다던지, 제네릭  타입을 특정 프로토콜을 따르는 타입 만 사용할 수 있도록 제약을 해야 하는 상황이 발생하기도 합니다!
  • 이러한 경우 타입 제약을 사용할 수 있는데요. 타입 제약은 클래스 타입 또는 프로토콜로만 가능 합니다 !!! (구조체 혹은 열거형등의 타입은 타입 제약으로 사용이 불가능합니다)
  • 정리하자면 제네릭으로 들어올 수 있는 타입에 특정 타입을 준수하거나 혹은 상속받는 타입만 가능하도록 제약을 걸어준다고 말할 수 있습니다.
func swapTwoValue<(T: BinaryInteger>(_ a: inout T, _ b: inout T) {
  // 함수 구현
}
  • 위에서 구현하였던 swap  기능이 있는 함수인데요. 코드를 보시면 매개변수 뒤에 콜론을 붙이고 제약 조건으로 중어질 타입을 명시한 걸 알 수 있습니다.
  • 혹 한 개 외에 여러 제약을 추가하고 싶다면 콤마가 아닌 where 절 을 사용해야 합니다.
func swapTwoValue<(T: BinaryInteger>(_ a: inout T, _ b: inout T) where T: FloatingPoint, T: Equatable {
  // 함수 구현
}
  • 위의 경우에는 T는 BinaryInteger, FloatingPoint, Equatable 프로토콜을 모두 준수해야 한다는 의미를 가지고 있습니다!

 

  • 하지만 저런 조건을 만족하는 경우는 없으니 타입 제약을 사용하는 다른 예시를 들어보겠습니다!
func substractTwoValue<T>(_ a: T, _ b: T) -> T {
  return a - b
}

예를 들어서 이러한 함수가 있을 때 return 값이 뺄셈이 있으므로 뺄셈이 가능한 타입이어야만 연산이 가능하다는 한계를 가지고 있습니다.

func substractTwoValue<T: BinaryInteger>(_ a: T, _ b: T) -> T {
  return a - b
}

하지만 이렇게 BinaryInteger 프로토콜을 준수해야 한다는 타입 제약을 걸어 놓으면 적절한 타입을 전달받는 것이 가능해집니다!

 

이렇게 제네릭에 대해서 정리를 해보았는데요. 더 많은 내용이 있지만 간단한 제네릭을 사용하는데 필요한 내용 위주로 정리해보았습니다. 읽어주셔서 감사합니다 :)


오늘은.. 여기까지..