BOID

[swift] 제네릭이란?(2/2) 본문

swift 시작기

[swift] 제네릭이란?(2/2)

HoonIOS 2021. 3. 18. 17:48

안녕하세요 HoonIOS입니다 :)

저번에는 제네릭의 개념과 제네릭 함수, 타입, 타입 확장에 대해 알아봤는데요, 이번에는 타입 제약 즉 특정 타입에 한정되게 제약을 두는 것과 프로토콜과 서브 스크립트에서 제네릭을 사용하는 것에 대해 알아보겠습니다.

 

제네릭의 타입 제약

- 앞 포스팅에서는 제네릭 타입 제약을 받지 않는 제네릭 함수를 구현해 보았는데, 만약 특정 타입에서만 한정되어야 할 처리가 있다던가, 제네릭 타입을 특정 프로토콜을 따른느 타입만 사용할 수 있도록 제약을 두어야 하는 상황이 있으면, 이때 타입 제약을 통해서 제약사항을 지정해 줄 수 있습니다.

- 타입 제약은 클래스 또는 프로토콜에서만 줄수 있습니다. 이 말은 즉 타입 매개변수 자리에 사용할 실제 타입이 특정 클래스를 상속받는 타입이어야 한다든지, 특정 프로토콜을 준수하는 타입이어야 한다는 제약을 줄 수 있다는 말입니다.

- 따라서 위의 조건으로 인해 열거형, 구조체 타입은 제네릭 타입제약을 사용할 수 없다는 말이 되겠습니다

- 표준 라이브러리인 Dictionary 라이브러리를 통해 예를 들어 설명을 해보겠습니다.

public struct Dictionary<key: Hashable,Value>: Collection, ExpressibleByDictionaryLiteral{/*...*/}

- 위 Dictionary 제네릭 코드를 보면 플레이이스 홀드(key) 뒤에 보면 클론( : )을 붙인 다음 Hashable이라고 명시되어 있는 것을 확인할 수 있는데 이것을 이 타입 매개변수인 key는 Hashable 프로토콜을 준수한다는 뜻이 됩니다.

- 위 예제처럼 제네릭 타입에 제약을 주고싶으면 타입 매개변수 뒤에 클론을 붙인 후 원하는 클래스 타입 또는 프로토콜을 명시해주면 됩니다.

- 여러 제약을 추가하고 싶으면 콤마로 구분해주는게 아니라 where 절을 사용하여 제약 조건을 추가해 줄 수 있습니다. (where 절은 추후에 포스팅할 예정입니다.)

 

- 예시를 통해 추가 제약조건을 주는 방법을 알아보겠습니다.

func swapTwoValues<T: BinaryInteger>( _ a: inout T, _ b: inout T) where T: FloatingPoint {

	//함수 구현
    
}

- 위 swapTowValue라는 함수에 T의 제약조건을 BinaryInteger과 where을 통해 FloatingPoint라는 프로토콜 제약조건을 추가해 주었습니다. 따라서 이 두 프로토콜을 준수하는 타입만 사용할 수 있습니다.

 

- 그렇지만 BinaryInteger은 apple doucumentation을 보면 An integer type with a binary representation. 라고 명시되는 것을 볼 수 있는데요 정수형 타입이라는 것을 볼 수 있습니다.

- FloatingPoint 프로토콜은 A floating-point numeric type.인 즉 부동소수점이라고 명시되어 있습니다.

- 따라서 BinaryInteger, FloatingPoint 프로토콜을 만족하는 기본타입은 없어 함수를 중복 정의하거나, 새로운 프로토콜 타입을 정의해서 사용하는 등 다른 방법을 사용해야 합니다 ㅎㅎㅎㅎㅎㅎㅎㅎ

 

- 그러면 이제 기본 조건을 사용할수 있는 타입 제약 예시를 한번 들어보겠습니다.

func substract TwoValue<T>( _ a: T, _ b: T) -> T {
	return a-b
}

- 위 예시를 보면 위 제네릭 함수는 결과값이 a-b이므로 그에 맞는 타입이 들어가야 되는 조건이 있어야 됩니다. 다시 말해 뺄셈 연산자를 사용할 수 있는 타입이 있어야 한다는 말입니다!

 

- 위 코드를 아래와 같이 제약 조건을 한번 걸어줘보겠습니다.

func substract TwoValue(T: BinaryInteger>( _ a: T, _ b: T) -> T {
	return a - b
}

- T의 타입을 BinaryInteger 프로토콜을 준수하는 타입으로 한정해주어 뺄셈 계산이 가능하게 해 주었습니다.

- 위 예제처럼 타입 제약은 함수 내부에서 실행해야 할 연산에 따라 적절한 타입을 전달받을 수 있도록 제약을 둘 수 있습니다.

  • 여기서 잠깐 표준 라이브러리에서 정의되어 있는 프로토콜 중 타입 제약에 자주 사용될만한 프로토콜에 대해 알아보겠습니다.!
    - Hashable, Equatable, Comparable, Indexable, IteratorProtocol, Error, Collection, CustomStringConvertible 등이 있습니다.
    혹시 이거 말고 자주 쓰는 프로토콜 있으면 말해주세요!

프로토콜의 연관 타입

- 연관 타입은 프로토콜에서 사용할 수 있는 플레이스 홀더 이름입니다.

- 다시 말해서, 제네릭에서는 어떤 타입이 들어있을지 모를 때, 타입 매개변수를 통해 '종류는 알 수 없지만, 어떤 타입이 여기에 쓰일 거예요'라고 표현해주면 연관 타입은 타입 매개변수의 그 역할을 프로토콜에서 수행할 수 있도록 만들어진 기능입니다.

- 프로토콜을 정의할 때, 연관 타입을 함께 정의하면 유용할 때가 있어요!

- 그냥 말로 설명하면 모르니깐 예제를 사용해 보겠습니다.

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

- 프로토콜을 보면 associatedType인 연관 타입에 존재하지 않는 타입인 itemType을 연관 타입으로 지정하여 프로토콜 정의에서 타입 이름으로 활용합니다.

- 제네릭의 타입 매개변수와 유사한 기능으로, 프로토콜 정의 내부에서 사용할 타입이 '그 어떤 것이어도 상관없지만, 하나의 타입임은 분명하는 의미로 무조건 하나 타입은 있습니다'라는 뜻입니다.

- 그럼 이제 Container 프로토콜에 있는 itemType의 타입이 구현해야 할 기능을 생각해 보겠습니다

  • 새로운 아이템을 컨테이너에 append(_:) 메서드를 통해 추가할 수 있어야 합니다.
  • 아이템 개수를 확인할 int형 변수 count를 구현해야 합니다.
  • 아이템 개수를 확인할 수 있도록 Int타입의 값을 갖는 count프로퍼티를 구현해야 한다.
  • Int 타입의 인덱스 값으로 특정 인덱스에 해당하는 아이템을 가져올 수 있는 서브 스크립트를 구현해야 합니다.

- 위 Container프로토콜을 클래스에 적용해 프로토콜에 충족할 수 있는 예제를 작성해 보겠습니다.

class MyContainer: Container {
    var items: Array<Int> = Array<Int>()
    
    var count: Int {
    	return self.items.count
    }
    
    func append( _ item: Int) {
    	items.append(item)
    }
    
    subscript(i: Int) -> Int {
    	return items[i]
    }
}

- 위 MyContainer 클래스를 보시면 Container 프로토콜을 충족한 것을 확인할 수 있고 ItemType은 실제 타입인 Int타입으로 구현해 준 것을 확인할 수 있습니다.

- 프로토콜에서 ItemType이라는 연관 타입만 지정했고, 특정 타입을 지정하지 않았으므로 프로토콜의 요구사항을 모두 충족하는 데는 큰 문제는 없습니다.

- 실제 프로토콜 정의를 준수하기 위해서는 연관 타입을 하나의 타입으로 일관성 있게 구현해주면 됩니다.

 

-이제 Stack에다가 Container 프로토콜을 정의해 보겠습니다.

 

struct Stack<Element>: Container {
    var items = [Element]()
    mutating func push(_ item: Element) {
    	items.append(item)
    }
    
    mutating func pop() -> Element {
    	return items.removeLast()
    }
    //Container 프로토콜 구현하기
    mutating func append(_ item: Element) {
    	self.push(item)
    }
    
    var count: Int {
    	return self.items.count
    }
    
    subscript(i: Int) -> Element {
    	return items[i]
    }
}
    

- Stack 구조체에서 Container에서 정의했던 ItemType이라는 연관 타입 대신에 Element라는 타입 매개변수를 사용했습니다. 그럼에도 Stack 구조체는 Container 프로토콜을 완벽히 준수했습니다.

따라서, 구조체에서 타임 매개변수를 다시 정의해서 그 매개변수를 사용했던 상관없다는 것을 알 수 있습니다.

서브 스크립트에서 제네릭 사용

- 서브 스크립트도 제네릭을 활용하여 타입에 큰 제한 없이 유현하게 구현할 수 있습니다. 물론 타입 제약을 사용하여 제네릭을 활용하는 타입에 제약을 줄 수도 있습니다.

 

- extension(확장) Stack에서 제네릭이 있는 서브 스크립트를 적용해 보겠습니다.

extension Stack {
	subscript<Indices: Sequence>(indices: Indicies) -> [Element] where Indicies.Iterator.Element == Int {
    	var result = [ItemType]()
        
        for index in indicies {
        	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]

- 위 코드의 subscript함수를 해석해보면 서브 스크립트는 indices라는 플레이스 홀더를 사용하여 매개변수르르 제네릭하게 받아들일수 있습니다.

- subscript의 매개변수인  indicies는 Sequence프로토콜을 준수하는 타입으로 제약이 추가되어 있습니다. 또, where조건을 보시면 Indicies타입 Iterator의 Element 타입이 Int타입이어야 하는 제약이 추가되었습니다.

- 서브스크립트는 Indicies 타입의 indicies라는 매개변수(indices: Indicies)로 인덱스 값을 받을 수 있습니다. 그 결과 indicies 시퀀스의 인덱스 값에 해당하는 스택 요소의 값을 배열로 받아볼 수 있습니다.

예를 들어, 위에 print값을 보면은 [0... 2]의 인덱스를 통해 [1,2,3] 값을 받는 것을 확인할 수 있습니다.

 

 

총 두 번의 포스팅을 통해서 제네릭을 설명해 보았는데요, 공부하면서 느낀 거지만 제네릭을 유용하게 사용하면 간단하고 쉽게 코딩을 할 수 있겠다는 생각을 했습니다.

앞으로 코딩할 때 제네릭을 사용하도록 노력해봐야겠습니다 ㅎㅎ

 

긴 글 읽어주셔서 감사합니다 :) 오늘도 좋은 하루 되시고 내용이 도움이 되었으면 좋겠네요 ㅎㅎ

 

 

반응형
Comments