티스토리 뷰

Swift

Swift ) Generic

Zedd0202 2017. 9. 22. 18:06
반응형

안녕하세요 :) Zedd입니다!

백준의 <DFS와 BFS>문제를 Swift로 풀고 있는데, 

다들 아시다시피 BFS는 그래프 전체를 탐색하되, 인접한 노드들을 차례대로 방문한다는 점에서 주로 Queue로 구현되곤 합니다.

DFS를 다 짜고, 이제 BFS를 짜볼까..?하는데



어 그래 Queue가 필요하지

Queue....?

Swift에서 Queue는 못본거 같은데..(C++은 STL로 queue가 있죠?)



네! Swift는 collection타입으로는 배열(array)과 사전(dictionary), 집합(set). 이 세가지만을 지원합니다.

즉, Queue나 Stack은 없음ㅋㅎㅋ

==> Queue나 Stack을 쓰고 싶으면..만들어서 써야합니다 ㅎㅎ

정말 친절하게도~~~Swift에는 Generic이라는 아주 강력한 기능이 있답니다 :)

오늘은 이 Generic(제너릭)을 배워볼거에요 :)


오늘 글은 모두 <Apple의 Swift 문서 - Generic>을 번역..?하는 수준입니다. 

추가로 이해를 돕는 좋은 예제가 있으면 제가 넣도록 할게요 :)





Swift - Generic




(c++의 템플릿의 개념을 아시나요? 템플릿을 아시는 분들이라면 이해가 좀 더 쉬울 수 있어요 :))

자. Generic.


뭔가 포괄적이고 일반적일 것 같은 느낌적인 느낌..

문서에 나와있는 Generic소개를 한번 볼까요?

Apple에 의하면 Generic을 사용하면, 유연하고 재사용성 높은 코드를 작성할 수 있다고 하네요.


Apple 왈 : 

Generic은 Swift의 가장 강력한 도구중에 하나야.

Swift 표준 라이브러리 대부분은 Generic으로 작성되어있어. 

실제로, 네가 Generic을 사용했다는 것을 인식 못했다고 해도, 이때까지 Generic을 쭉 사용해왔어!

그 예로, Swift의 Array(배열)과 Dictionary(사전)은 Generic 콜렉션이지.



아하 


Generic을 가장 빠르게 이해 할 수 있는 예제를 Apple이 제공했습니다.

같이 볼까요?


  1. func swapTwoInts(_ a: inout Int, _ b: inout Int) {
  2. let temporaryA = a
  3. a = b
  4. b = temporaryA
  5. }


자 여기 두 정수의 값을 바꿔주는 함수가 있어요. 



하지만 파라미터에 Double형을 넣으면 에러가 나게 되죠.

swapToInts의 파라미터는 Int형이기 때문입니다. 


아 ㅇㅋ...

그럼 Double형 바꿔주는 함수 하나 더 만들지 모ㅋ

  1. func swapTwoDoubles(_ a: inout Double, _ b: inout Double) {
  2. let temporaryA = a
  3. a = b
  4. b = temporaryA
  5. }


어 String도 바꿔줄 함수 하나 만들쟈


  1. func swapTwoStrings(_ a: inout String, _ b: inout String) {
  2. let temporaryA = a
  3. a = b
  4. b = temporaryA
  5. }


짠~ㅎㅎ



어떠신가요. 결국에는 똑같은 일을 하는데, 타입마다 따로 함수를 만들어줘야 한다니...

이러면 간지가 안나겠죠?


바로 이 때, Generic을 사용하게 됩니다. 


  1. func swapTwoValues<T>(_ a: inout T, _ b: inout T) {
  2. let temporaryA = a
  3. a = b
  4. b = temporaryA
  5. }


이렇게만 해주면, 이 함수 하나로 Int, String, Double변수들의 값을 바꿔줄 수 있습니다. (둘 다 같은 타입일때만)

정말 위에서 말했듯이 Generic을 사용하니,  유연함과 재사용성이 높아지네요!



자 저기 위에서 나온 코드들과 뭐가 달라보이나요?

일단 함수 이름 옆에 있는<T>가 눈에 띄네요. 그리고 swapTwoValues의 파라미터자리에, 타입이 들어갈 부분에 T라는 것이 들어갔네요.

이 T는 뭘까요? T는 타입 파라미터라고 불린답니다.


먼저, 파라미터 부분. 타입이 들어갈 자리에 T가 들어간


여기 먼저 설명드릴게요.


저 T라는 것은 Placeholder 타입 "이름"이에요! String, Int도 타입 "이름"이죠? 

그 대신 그냥 T라는 타입 "이름"이 들어간거에요.

이 T는 swapTwoValues라는 함수가 호출될때마다 "결정"됩니다.

그리고, a와 b는 이 T타입과 반드시 일치해야해요.



그리고, 함수 이름 옆에 적힌 <T>.



Generic함수와 일반함수의 큰 차이점이죠.

Generic함수는 함수 이름옆에, 위에서 말한 Placeholder 타입 이름이 옵니다.

대괄호(<>)로 묶어준 이유는,

Swift에게 함수 정의 내의 Placeholder 타입 이름인 것을 알립니다.

그냥 "T"라는 것은 Placeholder이므로, Swift는 "T"라는 실제 타입을 찾지 않아요.

이래도 문제가 하나도 없다는 것이죠 XD

또한, <T>이렇게 지금은 하나만 들어갔지만, 여러개가 들어갈 수 있답니다. 

이때는 ","로 구분해서 넣어주면 됩니다.




※ 이때, 파라미터에서 a와 b는 반드시 타입이 같아야겠죠? Swift는 무슨언어죠?

네. 안전한 언어이며 타입에 굉장히 민감합니다.

Int와 String을 서로 바꿀 수 없게하며, 만약 이렇게 해도 컴파일 에러가 나게 됩니다.


ㅎㅎ어떠신가요? 조금 이해가 가시나요?

Swift엔 이미 swap이라는 함수가 있죠?



오.. 역시 Generic함수네요..!




위에서 T는 타입 파라미터(Type Parameter)라고 그랬죠? 

우리가 사용하던 타입 파라미터에는 어떤게 있을까요?


네! Dictionary(사전)을 이용해 for문을 돌 때, 저렇게 key와 value로 접근했었죠?

사실,

이렇게 써도 아무문제가 없어요.

하지만, Dictionary자체가 key와 value로 이루어진 쌍들의모임이죠?

"key"와 "value"라는 이 이름 하나 가지고도, 뭐가 출력될지 예상이 가죠?

저 이름에 모든 설명이 들어가있는 것입니다.

이 Dictionary와 "관계"가 있는 "이름"이죠.

자신의 역할이 담겨있는 이름이 좋습니다.

따라서, 저렇게 hello, zedd로 바꿔서 쓰는건.. 좋지않죠.


하지만, 위에서 우리가 구현한 Generic 함수. 

swapTwoValues경우에는 타입파라미터와 정말 의미있는 관계가 있나요?

음..없다고 봐야겠죠?

이럴때는 위에서 사용한 타입파라미터인 T와 같은 단일문자를 사용하는것이 일반적이라고 해요.(T나 V)

또한, Upper camel case를 사용합니다. ex) T, MyTypeParameter




타입제약(Type Constraint)


자, 우리가 위에서 구현한 swapTwoValues를 다시 볼까요?

Generic으로 구현되어 있기 때문에, 모든타입에서 잘 동작할거에요.


하지만, 가끔은 특정한 타입만 Generic함수를 사용하면 좋을 때가 있겠죠?

이럴때를 위해 타입제약(Type Constraint)이 있답니다 :)


이를 통해, 타입파라미터(Type Parameter, swapTwoValues에서는 T)가 특정 클래스로부터 상속되거나,

특정 프로토콜을 준수해야만 Generic함수를 쓸 수 있도록 제약(Constraint)를 걸 수 있습니다.


위에서 Dictionary도 Generic 콜렉션이라고 그랬죠?

key는 Int가 들어올거고..value는 String이고..

각각의 경우마다 Dictionary는 따로 구현되어있지 않아요.

Generic을 사용함으로써 다양한 타입들을 한번에 받을 수 있는것이죠.


사실 Dictionary에서 Key는 아무거나 타입이나 들어갈 것처럼 보이지만, 

Hashable프로토콜을 준수해야만 Key로 들어올 수 있습니다.



즉, 우리가 이때까지 넣었던 Int, String, Double, Bool등은 Hashable 프로토콜을 준수하고 있다는거겠죠?


위와같이 Dictionary는 타입제약(Type Constraint)를 통해 

특정한 클래스를 상속받거나, 특정한 프로토콜을 준수한 타입만이 들어올 수 있도록 구현되어있습니다. 

물론 Generic으로요.


그럼 우리도 한번 만들어볼까요? 타입제약(Type Constraint)이 들어간 Generic함수를요.


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


위 코드가 타입제약이 들어간 Generic함수의 Syntax인데요,

우리 저~~ 위에서 T가 뭐라고 그랬죠? 네. 타입 파라미터 (Type Parameter)라고 그랬죠?

U역시 타입 파라미터 (Type Parameter)네요.


이 T와 U뒤에 뭔가 있네요. T옆에는 특정한 클래스가, U옆엔 특정한 프로토콜이 왔네요.

그리고 T와 U는 콤마(,)로 구분되어 있습니다.


그럼 위 코드는 어떤 것을 의미할까요?


 

여기서 T에게 타입제약(Type Constraint)를 준 것인데요.

여기서 T는 SomeClass의 하위클래스여야 한다는 제약을 건거에요.



그리고 U는 SompeProtocol을 준수해야하는 타입제약(Type Constraint)을 건것입니다. 


그러니까, 정말 T와 U에게 타입제약(Type Constraint)을 줬네요..

이 타입제약을 예제를 통해 볼까요?


  1. func findIndex(ofString valueToFind: String, in array: [String]) -> Int? {
  2. for (index, value) in array.enumerated() {
  3. if value == valueToFind {
  4. return index
  5. }
  6. }
  7. return nil
  8. }

위 findIndex함수는 String배열과, 찾고싶은 String하나를 파라미터를 주면, 

해당 String이 있으면 그 인덱스를 반환해주고, 없으면 nil을 반환해주는 함수에요. 


아, 근데 나는 지금 [1,2,3,4,5,6]이런 Int배열이 있는데, 여기서 내가 찾고싶은 Int값이 몇번째에 있는지 알고싶어!


이럴 수 있겠죠?

근데 저희는 방금 Generic을 배웠죠?


Generic으로 만들어주지..크킄ㅋ....킄...

해서



  1. func findIndex<T>(of valueToFind: T, in array:[T]) -> Int? {
  2. for (index, value) in array.enumerated() {
  3. if value == valueToFind {
  4. return index
  5. }
  6. }
  7. return nil
  8. }


이렇게 만들었어요. 저~~기 위에서 했던 swapValues와 비슷하죠?

 T라는 타입파라미터 이름을 정해놓으면, 이 findIndex가 불릴때마다 타입이 결정된다고 했죠?

ㅋ아 나 넘 천재아냐?



응 오류~~

이 오류가 나는 이유는,

저  value == valueToFind라는 코드때문인데요,

Swift의 모든 타입이 저렇게 ==라는 연산자로 비교될 수 있지 않기 때문이죠.


Swift가 가능한 모든 타입 T에 대해 이 코드가 작동한다는 것을 보장할 수 없기 때문에,

컴파일에러가 나게 되는 것입니다. 


이 코드가 작동하도록 해주는 것이 바로 저번시간에 배운 Equatable입니다. 

Equatable에 대한 자세한 개념은 <Equatable>글을 참고해주세요!

위 글을 읽고오셨다는 가정하에 설명을 진행할거에요 :)





자!! 읽고 오셨죠?

그럼 이제 


  1. func findIndex<T>(of valueToFind: T, in array:[T]) -> Int? {
  2. for (index, value) in array.enumerated() {
  3. if value == valueToFind {
  4. return index
  5. }
  6. }
  7. return nil
  8. }

이 코드를 어떻게 바꿔야 할지 감이오시나요?


  1. func findIndex<T: Equatable>(of valueToFind: T, in array:[T]) -> Int? {
  2. for (index, value) in array.enumerated() {
  3. if value == valueToFind {
  4. return index
  5. }
  6. }
  7. return nil
  8. }

이렇게 T라는 타입파라미터가 Equatable 프로토콜을 준수하도록 해주면, 

에러없이 잘 작동하는것을 볼 수 있습니다. 

Equatable을 준수하는 모든 타입은 위 findIndex함수를 안전하게 사용할 수 있는 것이죠.


타입제약에 대해 조금은 이해가 가시나요?



이렇게 간단하게(?) Generic을 알아보았는데요!!!! 

Generic에 대해 더 많은 설명이 있지만...너무 길어질 것 같아서 ㅠㅠㅠ

저도 잘 이해하게 되면 또 글을 쓸게요 :)

BFS하나로 여기까지 왔네요....

아무튼 도움이 되었으면 좋겠어요! 그리고 Generic에 대해서는 꼭 아는게 좋은 것 같아요 :)

안녕!!




반응형

'Swift' 카테고리의 다른 글

Swift ) split에 대한 고찰  (1) 2017.09.27
함수(Function) VS 메소드(Method)  (3) 2017.09.26
Swift ) Equatable  (4) 2017.09.18
Swift ) 크기가 정해진 2차원 배열 만들기  (0) 2017.09.18
Swift ) 랜덤함수 차이  (0) 2017.09.11