ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [Apple Dev Reference - Swift] Generics (of Swift2.2)
    앱등이에게 살충제를 뿌린다./Apple Dev Reference 2016. 4. 24. 15:18


    <메타몽>

    Generics

    제네릭 코드를 사용하면 좀 더 유연하고 재사용률이 높은 함수를 만들 수 있습니다. 제네릭을 통해 코드 중복을 방지할 수 있고, 함수의 역할을 좀 더 명백히 전달할 수도 있습니다.


    제네릭은 Swfit의 가장 큰 강점 중 하나입니다. 수 많은 Swift라이브러리들이 이 제네릭을 사용하고 있습니다. 못 알아채셨을 수도 있는데, Swift 언어를 익히시는 동안 계속 제네릭을 사용해왔습니다. 예를 들어, Swift의 Array와 Dictionary는 둘 다 제네릭 컬렉션입니다. Array를 만들 때, Int타입 Array나 String타입 Array를 만들기도 하고, 또 여러분이 직접 만든 클래스 타입으로도 수 많은 종류의 Array타입을 만들어 보셨을겁니다. Dictionary에서도 유사하게 무수한 타입으로 만들어 보셨을겁니다. 아시다시피 Array와 Dictionary를 만듬에 있어서 타입에 대한 제한은 전혀 없습니다.



    The Problem That Generics Solve

    일반적인 non-generic swapTwoInts(_:_:)함수인 를 보겠습니다. 이 함수는 두개의 Int값을 서로 바꾸는 기능을 합니다.



    이 함수는 inout 파라미터(call-by-reference)를 받아서 두 값을 서로 바꿔주는 기능을 하고 있습니다. (In-Out에 대한 자세한 설명)

    swapTwoInts(_:_:)는 b의 값을 a에 넣고, a의 값을 b에 넣어주고 있습니다. 이 함수는 두 파라미터가 모두 Int일 때 사용하는 함수입니다.



    swapTwoInts(_:_:)는 분명 쓸모있는 함수지만 Int타입만 사용할 수 있다는 점이 아쉽게 느껴집니다. 만약 두 개의 String을 바꾸고 싶다면? 아니면 두 개의 Double을 바꾸고 싶다면? 그렇다면 우리는 또 각각의 경우에 해당하는 함수를 만들어야 합니다. 아마도 swapTwoStrings(_:_:)와 swapTwoDoubles(_:_:)가 되겠네요.


    아시겠지만 swapTwoInts(_:_:)swapTwoStrings(_:_:)swapTwoDoubles(_:_:) 세 함수 모두 내부 구현이 완전히 동일(identical)합니다. 유일한 차이점은 파라미터의 타입이 각각 IntStringDouble으로 다르다는 것입니다.

    만약 하나의 함수가 모든 타입의 파라미터를 받을 수 있고 두 파라미터의 값을 바꿔주는 역할을 한다면 훨씬 유연하고 멋진 함수가 될 것 같은데요. 제네릭과 함께라면 가능합니다. (제네릭을 사용한 함수는 아래에 구현됩니다.)


    Note : 세 함수 모두 a와 b의 타입은 같아야 합니다. a와 b가 다른 타입이라면 두 변수의 값을 바꾸는건 불가능하겠지요. Swift는 type-safe언어이기에 String을 Double로 대입하는 등의 연산은 불가능합니다. 이러한 연산은 컴파일에러를 발생시킵니다.




    Generic Functions

    제네릭 함수는 어떤 타입의 매개변수에도 잘 작동합니다. 아래는 위의 swapTwoInts(_:_:)을 제네릭으로 구현한 swapTwoValues(_:_:)입니다. 


    swapTwoValues(_:_:)의 구현부분은 위의 swapTwoInts(_:_:)와 완전히 동일합니다. 하지만 첫 번째 라인이 조금 다르네요. 비교를 해보도록 하죠.


    제네릭 함수에서는 실제 변수 타입이 아닌 placeholder type name(예시에서는 T)를 사용하고 있습니다. placeholder type name인 T는 특정한 타입을 가리키는 것이 아니라 '어떤 타입이 오든 상관없지만 a와 b는 같은 타입을 가져야한다.'라는 의미를 갖습니다. T가 어떤 타입이 될지는 swapTwoValues(_:_:)이 호출되는 시점에 결정됩니다.


    또 다른 차이점으로는 제네릭 함수의 이름의 끝에는 placeholder type name(T)이 꺾쇠에(<T>) 둘러쌓인 채 있다는 겁니다. 이 꺾쇠는 Swift에게 'T는 swapTwoValues(_:_:)내에서 placeholder type name로 사용될거야' 라고 알려줍니다. 그 결과 Swift에서는 T라고 칭해진 타입이 실제로 어떤 타입이 될지는 신경쓰지 않고 넘어가게 됩니다.


    swapTwoValues(_:_:)는 swapTwoInts와 동일한 방식으로 사용할 수 있습니다. swapTwoValues(_:_:)가 호출될 때마다 Swift의 타입추론(type inference)에 의해 T가 결정되고 함수가 제대로 작동하게 됩니다. 아래의 예제에서 두 번의 swapTwoValues(_:_:)는 각각 Int과 String으로 타입추론을 하고 있습니다.


    Note : 위에서 구현한 swapTwoValues(_:_:)함수는 swap이라는 함수로 이미 스위프트 standard library에 존재하고 있습니다. swapTwoValues(_:_:)기능을 사용하고 싶으시면 굳이 이 메소드를 만들지 마시고 기존에 존재하고 있는 swap(_:_:)함수를 사용해주세요.





    Type Parameters

    위에서 사용한 swapTwoValues(_:_:)의 placeholder type name인 T는 type parameter의 예시입니다. type parameter는 placeholder type name를 결정하는 요소인데요, 함수의 이름 바로 오른쪽에 꺾쇠(<>)에 쌓인채 적으면 됩니다.(예를 들면 <T>)


    type parameter를 적어주셨다면 함수의 파라미터의 타입이나(swapTwoValues(_:_:)의 a와 b처럼 말이죠) 함수의 리턴타입이나 함수 내부에서 타입 추론을 위해서 으로 사용할 수 있습니다. 물론 함수가 호출되는 시점에는 이 type parameter가 실제의 데이터 타입으로 대치됩니다. 예를 들어 위의 swapTwoValues(_:_:)에서 T가 처음엔 Int로 치환되었고, 두 번째 경우에는 String으로 치환되었습니다.


    한 번에 여러개의 type parameter를 선언할 수 있습니다. 각각의 type parameter를 꺾쇠(<>)로 감싸고 콤마(,)로 구분하면 됩니다.





    Naming Type Parameters

    type parameter를 선언할 때에는 주로 해당 type parameter를 나타낼 수 있는 값을 적습니다. Dictionary<Key, Value>and Element in Array<Element>에서처럼

     코드를 보면 해당 함수에서 type parameter가 어떤 의미를 갖는지 알 수 있도록 하는 것이 좋습니다. type parameter가 별다른 의미가 없는 경우에는 일반적으로 T,U,V처럼 알파벳 하나로 사용하는 경우가 많습니다.

    Note : type parameter는 항상 Camel Cases 표기(T, MyTypeParameter)를 해주도록 합시다. value가 아닌 placeholder라는 걸 알려주기 위함입니다.





    Generic Types

    Swift는 제네릭 함수 외에 제네릭 타입도 제공하고 있습니다. 제네릭 타입은 Array와 Dictionary처럼 어떤 타입과도 함께 사용할 수 있는 커스텁 클래스,구조체,Enum입니다.


    이번 섹션에서는 Stack이라는 제네릭 컬렉션 타입을 만들어 보도록 하겠습니다. stack은 array처럼 데이터가 순서대로 저장되어있는 컬렉션입니다. 하지만 데이터의 삽입(push)과 삭제(pop)가 항상 컬렉션의 끝부분에서만 이뤄진다는 차이점을 갖고있습니다.

    stack의 push/pop을 그림으로 나타내면 아래와 같습니다.


    1. stack에 3개의 값이 저장되어 있습니다.
    2. push를 통해 네 번째 값이 stack에 저장됩니다.
    3. 이제 stack에는 4개의 값이 저장되어 있습니다. 방금 전에 push된 값은 stack의 최상단에 있습니다.
    4. pop을 통해 최상단의 값을 제거합니다.
    5. pop이 끝난 뒤 stack은 다시 3개의 값을 갖게 됩니다.
    아래는 Int값만 사용할 수 있도록 구현한 non-generic버전의 stack입니다.

    이 구조체에서는 items라는 Array프로퍼티를 하나 사용합니다. 또한 Stack은 push와 pop이라는 두 개의 메소드를 제공하고 있습니다. 이 두 함수는 내부 프로퍼티인 items를 변경시키는 메소드이므로 mutating이라는 키워드가 붙었네요.


    위에서 구현한 IntStack은 오직 Int타입으로만 사용할 수 있습니다. 그래서 어떤 타입으로도 사용할 수 있는 제네릭 Stack을 만든다면 훨씬 더 유용하게 사용할 수 있을것 같습니다.

    제네릭 버전의 코드는 아래와 같습니다.


    제네릭 버전의 Stack과 non-generic버전의 구현은 Int대신 type parameter인 Element를 사용했다는 점외에는 완전히 똑같습니다. type parameter를 구조체 이름의 바로 오른쪽에 꺾쇠(<Element>)로 둘러쌓여 있네요. 


    Element는 나중에 이 구조체를 사용할 때 사용할 타입의 placeholder name 역할을 합니다. 구조체에 실제로 사용된 타입은 구조체의 내부에 있는 Element로 유추되어 사용됩니다. 이 예제에서는 총 세 곳에서 placeholder로 사용되고 있네요.

    • Element타입의 빈 배열인 items프로퍼티를 만들기 위해 사용되었습니다.
    • push(_:)메소드가 Element형 item이라는 파라미터 1개를 받는 메소드라는 점을 나타내기 위해 사용되었습니다.
    • pop()메소드의 리턴 값이 Element타입이라는 나타내기 위해 사용되었습니다.
    Stack은 제네릭 타입으로 구현되었기 때문에 Array Dictionary처럼 Swift의 모든 타입과 함게 사용할 수 있습니다.
    꺾쇠내에 특정 타입을 표시하면서 Stack을 선언하면 새로운 Stack인스턴스가 생성됩니다. 예를 들어, String타입의 Stack을 생성하기 위해서는 Stack<String>()라고 선언해주면 됩니다. 

    아래 그림은 stackOfStrings에 값이 pushing되는 과정을 보여주고 있습니다.


    pop을 호출하면 stackOfStrings의 최상단 값인 "cuatro"가 삭제되고 리턴됩니다.


    아래 그림은 stackOfStrings에서 pop이 일어나는 과정을 보여주고 있습니다.







    Type Constraints

    위의 swapTwoValues(_:_:)와 Stack은 어떤 타입과도 함께 사용할 수 있습니다. 하지만 가끔은 제네릭 함수나 제네릭 타입에 특정 타입을 사용하도록 강제로 지정할 필요가 있을수도 있습니다. 제네릭에서는 Type Constraints를 통해 특정 클래스를 상속받는 클래스만을 사용하도록 하거나, 특정 프로토콜을 따르는 클래스만 사용할 수 있도록 지정할 수 있습니다.


    예를 들어, Dictionary는 key값으로 사용될 수 있는 클래스에는 제한사항이 존재합니다. Dictionaries에 설명되어 있는데요, Dictionary의 key값으로 사용할 수 있는 타입은 반드시 hashable해야 합니다. 즉 하나의 key값은 다른 key값과 반드시 구별되는 성질을 갖고 있어야 합니다. 이런 제약이 없다면 Dictionary에 값을 넣고자 할 때 새로운 key-value쌍을 만들어서 넣어야할 지, 기존에 존재하는 key의 value값을 바꾸기만 하면 되는지를 판단할 수가 없겠죠. 물론 특정 key값에 대한 value가 존재하는지도 100% 확신할 수 없게 되구요.


    Dictionary에서는 이런 제약을 만들기 위해 key값으로 사용되는 타입은 반드시 Hashable프로토콜을 따르도록 하고 있습니다. Swift의 모든 기본 타입(StringIntDouble, and Bool 등)은 기본적으로 Hashable프로토콜을 따르고 있습니다.


    제네릭 타입을 만들 때, Type Constraints를 사용하면 제네릭이 훨씬 강력한 힘을 발휘하게 됩니다. Hashable과 같은 추상적 제약을 통해 해당 제네릭 타입의 특징을 암시할 수 있습니다.


    Type Constraint Syntax

    제네릭 파라미터의 이름 오른쪽에 콜론(:)을 통해서 type constraints를 사용할 수 있습니다. 제네릭 함수에서 type constraints를 사용하는 기본 형태는 아래와 같습니다.(물론 제네릭 타입에서도 동일합니다.) 

    위 함수는 두 가지 타입의 파라미터를 받고 있습니다. 첫 번째 파라미터 T는 반드시 SomeClass를 상속받는 클래스여야 합니다. 두 번째 파라미터 U는 반드시 SomeProtocol을 따르는 타입이어야 합니다.



    Type Constraints in Action

    여기 findStringIndex라는 non-generic 함수가 있습니다. 두 번째 파라미터인 String배열에서 첫 번째 파라미터 String값을 찾는 함수입니다. findStringIndex(_:_:)는 옵셔널 Int타입을 리턴합니다. 만약 값을 찾으면 해당 값의 index를, 찾지 못하면 nil을 리턴합니다.


    findStringIndex(_:_:)는 아래처럼 배열에서 특정 String값을 찾기 위해 사용할 수 있습니다.


    이런 함수를 String에만 사용할 수 있다는 건 좀 아쉽네요. 그래서 findIndex라는 제네릭 함수를 만들도록 할겁니다. findStringIndex(_:_:)에서 String으로 되어있는 모든 부분을 T로 바꿔주면 됩니다.


    아래는 findStringIndex를 제네릭 버전으로 구현한 findIndex입니다. findIndex의 리턴 값은 동일하게 배열의 index이므로 리턴타입 또한 동일한 Int?입니다. 

    !!경고 : 이 함수는 위에서 설명한 몇 가지 이유때문에 컴파일 되지 않습니다. 



    보시다시피 컴파일에러가 발생합니다. Equality를 체크하는  “if value == valueToFind에서 에러가 발생하는군요. Swift에는 ==연산자를 사용할 수 없는 경우가 있습니다. 예를 들어 여러분이 Data Model 클래스나 스트럭쳐를 만든 뒤, 두 객체의 동등함(equal to)을 비교할 때 Swift는 어떤 경우에 동등하다고 해야할 지 알 수가 없습니다. 이 때문에 이 코드가 모든 T타입에 대해 정상적인 작동을 한다는 보장이 없습니다.


    하지만 방법이 있습니다. Swift에는 Equatable라는 프로토콜이 있습니다. 이 프로토콜은 객체에 Equal연산자(==)와 Not Equal연산자(!=)를 사용할 수 있도록 합니다. 모든 Swift의 기본 타입들은 Equatable프로토콜를 따르고 있습니다.


    Equatable프로토콜을 따르는 타입들은 findIndex(_:_:)에 사용될 수 있습니다. ==연산자를 사용할 수 있기 때문이죠. 이를 코드로 표현해보겠습니다. 


    findIndex는 T: Equatable타입 하나만을 사용하는 제네릭 함수입니다. 이는 Equatable프로토콜을 따르는 타입만 사용할 수 있다는 뜻이겠네요.

    이제 findIndex는 성공적으로 컴파일이 되고 아래 처럼 사용할 수 있겠습니다. Double과 String는 Equatable프로토콜을 따르기 때문에 두 타입 모두 사용할 수 있습니다.




    Associated Types

    프로토콜을 정의할 때, 프로토콜 내부에 associated type을 사용하면 유용할 때가 있습니다. associated type은 프로토콜 내부에서 사용될 타입에 placeholder name을 부여합니다. 실제로 이 타입이 어떤 타입이 될지는 프로토콜을 구현하기 전까지 정해지지 않습니다. Associated Type은 associatedtype키워드로 사용할 수 있습니다.



    Associated Types in Action

    (설명이 좀 어려운데요...) Container라는 프로토콜의 예제 소스입니다. 이 프로토콜은 ItemType이라는 associated type을 선언하고 있습니다.


    Container프로토콜은 세 가지 요구사항을 정의하고 있습니다.

    • append(_:) 메소드를 통해 container에 새로운 item을 추가할 수 있어야 합니다.
    • Int타입을 반환하는 count프로퍼티를 통해 container에 담겨있는 item의 개수를 알 수 있습니다. 
    • Int를 통해 특정 item을 반환하는 subscript를 구현해야 합니다.
    이 프로토콜은 item이 어떻게 저장되고 어떤 타입을 저장하는 지에 대해서는 관심이 없습니다. 단지 Container라면 갖추어야 할 세 가지 기능에 대해서만 명시하고 있습니다. 물론 새로운 타입을 구현할 때 이 세가지만 만족한다면 얼마든지 다른 기능을 확장할 수 있습니다.

    Container프로토콜을 따르는 타입을 구현할 때는 어떤 값을 저장할 것인지 반드시 명시해야 합니다. 특히 container에 저장되는 타입이 올바른 타입이라는 것을 보장해야하고 subscript로 반환되는 타입 또한 반드시 명시되어야 합니다.(이 부분은 본문 첨부)
    Any type that conforms to the Container protocol must be able to specify the type of values it stores. Specifically, it must ensure that only items of the right type are added to the container, and it must be clear about the type of the items returned by its subscript.

    이런 조건을 만족시키기 위해서 Container프로토콜에는 container가 담을 타입을 추론할 방법이 필요합니다. Container프로토콜에서 append(_:)메소드에 넘겨지는 값의 타입은 container가 담는 타입, subscript가 반환하는 타입과 일치해야 합니다. 


    이를 위해 Container프로토콜은 ItemType이라는 associated type을 선언했습니다. 코드상으로는 associatedtype ItemType이라고 되어있네요. 프로토콜에서는 ItemType이 어떤 타입인지는 선언하지 않은 채 남겨두고 있습니다.(ItemType의 타입은 프로토콜을 구현하는 시점에 결정될 사항입니다.) ItemType라는 alias(가칭)을 사용함으로써 이 container에서 저장하는 타입과, append(_:)에 사용되는 타입, subscript가 반환하는 타입이 반드시 일치해야함을 나타낼 수 있습니다.


    아래는 non-generic버전의 Container을 따르는 IntStack타입의 예제 소스입니다.


    IntStack은 Container프로토콜의 세 가지 요구사항을 모두 구현하고 있네요. 


    뿐만 아니라 IntStack은 Container프로토콜을 구현하면서 ItemType타입을 Int로 사용하겠다고 선언했습니다. typealias ItemType = Int는 추상적 타입인 ItemType을 구체적 타입인 Int로 사용하겠다는 의미입니다. 


    사실 Swift의 타입추론기능 덕분에 굳이 이 문장을 쓰지 않아도 IntStack의 구현에는 전혀 문제가 없습니다. Swift는 IntStack이 Container프로토콜을 구현할 때 append(_:)메소드의 파라미터 item과 subscript의 리턴 타입을 통해 ItemType의 타입을 유추합니다. 실제로 typealias ItemType = Int코드를 삭제해도 코드는 동일하게 작동을 합니다.


    Container프로토콜을 따르면서 제네릭을 사용하는 Stack을 구현할 수도 있습니다.

    이번에는 append(_:)의 파라미터인 item과 subscript의 리턴타입에 Element라는 type parameter가 사용되고 있습니다. 이를 통해 Swift는 마찬가지로 ItemType에 어떤 타입을 사용해야할 지를 유추할 수 있게 됩니다.



    Extending an Existing Type to Specify an Associated Type

    Adding Protocol Conformance with an Extension에서 확인할 수 있듯, 기존에 존재하는 타입이 특정 프로토콜을 따르도록 확장구문을 작성할 수 있습니다. 이는 associated type을 사용하는 프로토콜에도 마찬가지입니다.


    Swift의 Array에는 이미 append(_:)라는 메소드와 count프로퍼티와 Int를 받는 subscript를 제공하고 있습니다. 사실상 Array는 이미 Container 프로토콜을 따르고 있습니다. 따라서 아래처럼 Array가 Container프로토콜을 따른다는 표시만 해주고 빈 괄호로 확장구문을 끝낼 수 있습니다. Declaring Protocol Adoption with an Extension에 Empty extension에 대한 설명이 있습니다.


    Swift는 위의  Stack의 경우와 마찬가지로 Array의 append(_:)메소드와 subscript에서 ItemType이 어떤 타입이 될지 유추할 수 있습니다.





    Where Clauses

    Type Constraints에서 설명하였듯, Type constraints를 통해 여러분은 제네릭의 타입에 제한을 둘 수 있습니다. 


    associated type에도 제한을 두는 것이 유용할 때가 있는데요, where라는 구문을 통해 제한을 둘 수 있습니다. where구문은 associated type이 특정 프로토콜을 따르거나 다른 특정 type parameter와 같은 타입이어야 한다는 제약등을 둘 수 있습니다. 제네릭 타입을 선언하는 부분 바로 뒤에 where을 표기하여 where구문을 사용할 수 있습니다. 


    아래의 예제는 allItemsMatch라는 제네릭 함수의 코드입니다. 이 함수는 두 개의 Container인스턴스가 같은 item들을 같은 순서로 담고 있는지를 확인하는 기능을 합니다. 


    두 Container인스턴스는 같은 타입일 필요는 없지만, 담고 있는 item의 타입은 일치해야 합니다. 아래의 where구문에서 이를 체크하고 있네요.


    이 함수는 두개의 파라미터 someContainer와 anotherContainer를 받고 있습니다. someContainerC1타입에 해당하고, anotherContainerC2타입에 해당하며 두 타입 모두 함수가 호출 될 때 결정됩니다.


    정리하자면 이 함수의 두 파라미터와 관련된 제약 사항은 아래와 같습니다.

    • C1은 Container프로토콜을 따라야 합니다.(C1: Container)
    • C2도 마찬가지로 Container프로토콜을 따라야 합니다.(C2: Container
    • C1의 ItmeType은 C2의 ItemType과 같은 타입이어야 합니다. (C1.ItemType == C2.ItempType)
    • C1의 ItmeType은 Equatable프로토콜을 따라야 합니다.(C1.ItemType: Equatable)
    이 내용은 다시 이렇게 해석할 수 있습니다.
    • someContainer는 C1타입의 container인스턴스입니다.
    • anotherContainer는 C2타입의 container인스턴스입니다.
    • someContainer와 anotherContainer는 같은 타입의 item을 저장하는 container입니다.
    • someContainer의 item들은 not equal 연산자(!=)를 사용하여 비교할 수 있어야 합니다.
    세 번째, 네 번째 내용을 미루어 볼 때 anotherContainer의 item들 또한 not equal 연산자(!=)를 사용하여 비교할 수 있어야 한다는 의미를 갖습니다. 
    이 요구사항들만 따르면 allItemsMatch(_:_:)함수는 두개의 container인스턴스가 다른 타입일지라도 내부에 저장된 item을 비교할 수 있게 됩니다.
    allItemsMatch(_:_:)함수는 먼저 두 개의 container가 같은 수의 item을 갖고 있는지 확인합니다. item수가 다르다면 false를 리턴하고 함수가 종료되겠죠. 
    item의 개수 확인을 통과했다면, for-in루프를 통해 someContainer의 모든 item을 순회합니다. 각 item에 대해 anotherContainer의 item과 비교를 합니다. 두 아이템이 다르다면 false를 리턴하고 함수가 종료됩니다.
    다른 item을 찾지 못하고 루프가 종료되면 true를 리턴하게 됩니다.

    아래는 함수가 사용되는 예제 코드입니다.


    String을 담는 Stack인스턴스를 만들고, 세 개의 item을 넣어줍니다. Array인스턴스도 하나 만들어 세 개의 item을 넣어줍니다. Stack과 Array는 다른 타입이지만 둘 모두 Container프로토콜을 따르고 있고 item의 타입(String) 또한 동일합니다. 그렇다면 allItemsMatch(_:_:)의 요구사항을 모두 만족하므로 함수를 호출하여 사용할 수 있습니다. 예제에서는 allItemsMatch(_:_:)가 두 인스턴스의 item들이 모두 일치한다는 결과를 보여주고 있네요.




    출처 : https://developer.apple.com/library/ios/documentation/Swift/Conceptual/Swift_Programming_Language/Generics.html










    댓글 0

Designed by Tistory.