제네릭(Generics)

Ios_Roy·2023년 3월 5일
0

swift 문법

목록 보기
21/29
post-thumbnail

제네릭(Generics) 문법이 왜 필요한가?

  • 제네릭이 없다면, 함수(클래스, 구조체, 열거형 등)타입마다 모든 경우를 다 정의해야 하기 때문에
  • 개발자의 할일이 늘어난다. (유지보수/재사용성 관점에서 어려움)

제네릭(Generics)의 개념이 없다면, 함수를 모든 경우마다 다시 정의해야 한다.

  • 제네릭 문법
  • 형식에 관계없이, 한번의 구현으로 모든 타입을 처리하여, 타입에 유연한 함수 작성가능 (유지보수/재사용성 증가)
    • (함수 뿐만아니라) 구조체 / 클래스 / 열거형도 제네릭으로 일반화 가능
  • 타입 파라미터는 함수 내부에서 파라미터 형식이나 리턴형으로 사용됨 (함수 바디에서 사용하는 것도 가능)
  • 보통은 T를 사용하지만 다른 이름을 사용하는 것도 문제가 없음, 형식이름이기 때문에 UpperCamelcase로 선언
  • 2개이상을 선언하는 것도 가능
  • 제네릭은 타입에 관계없이, 하나의 정의(구현)로 모든 타입(자료형)을 처리할 수 있는 문법
  • 제네릭 함수, 제네릭 구조체/클래스
  • 일반 함수와 비교해보면, 작성해야하는 코드의 양이 비약적으로 감소
  • 타입 파라미터는 실제 자료형으로 대체되는 플레이스 홀더(어떤 기호같은것) ===> 새로운 형식이 생성되는 것이 아님
  • 코드가 실행될때 문맥에 따라서 실제 형식으로 대체되는 "플레이스 홀더"일뿐

제네릭 함수의 정의

제네릭(Generics) 함수를 정의하는 방법

func swapTwoValues<T>(_ a: inout T, _ b: inout T) {      // 플레이스홀더의 역할(표시 역할일뿐) (같은 타입이어야함)
    let tempA = a
    a = b
    b = tempA
}

var string1 = "hello"
var string2 = "world"

// 제네릭으로 정의한 함수 사용해보기

swapTwoValues(&string1, &string2)     // 같은 타입이라면, 어떠한 값도 전달 가능 해짐
print(string1)
print(string2)

// 배열을 출력하는 예제

func printArray<T>(array: [T]) {
    for element in array {
        print(element)
    }
}

printArray(array: numbers)     // 플레이스홀더 ====> [Int]
printArray(array: scores)      // 플레이스홀더 ====> [Double]
printArray(array: people)      // 플레이스홀더 ====> [String]

제네릭 타입의 정의

제네릭(Generics) 구조체 / 클래스 / (열거형)

  • 클래스, 구조체, 열거형의 타입이름 뒤에 타입 파라미터를 추가하면, 제네릭 타입으로 선언됨
  • 타입 파라미터는 타입(형식) 이름뒤에 선언, 타입 제약 문법도 동일함 GridPoint<T: Equatable>
  • 속성의 자료형, 메서드의 파라미터형식, 리턴형을 타입 파라미터로 대체 가능
struct Member {
    var members: [String] = []
}

struct GenericMember<T> {
    var members: [T] = []
}

var member1 = GenericMember(members: ["Jobs", "Cook", "Musk"])
var member2 = GenericMember(members: [1, 2, 3])
print( member1.members)
print(member2.members)

// 클래스로 제네릭의 정의하기

class GridPoint<A> {
    var x: A
    var y: A
    
    init(x: A, y: A){
        self.x = x
        self.y = y
    }
}

let aPoint = GridPoint(x: 10, y: 20)
let bPoint = GridPoint(x: 10.4, y: 20.5)

// 열거형에서 연관값을 가질때 제네릭으로 정의가능
// (어짜피 케이스는 자체가 선택항목 중에 하나일뿐(특별타입)이고, 그것을 타입으로 정의할 일은 없음)

enum Pet<T> {
    case dog
    case cat
    case etc(T)
}

let animal = Pet.etc("고슴도치")

제네릭을 Extension(확장)에도 적용할 수 있다

  • (확장 대상을 제한하는 것도 가능은 함)
extension Coordinates {     // Coordinates<T> (X)
    
    // 튜플로 리턴하는 메서드
    func getPlace() -> (T, T) {
        return (x, y)
    }
}

let place = Coordinates(x: 5, y: 5)
print(place.getPlace())

// where절 추가도 가능
// Int타입에만 적용되는 확장과 getIntArray() 메서드

extension Coordinates where T == Int {     // Coordinates<T> (X)
    
    // 튜플로 리턴하는 메서드
    func getIntArray() -> [T] {
        return [x, y]
    }
}

let place2 = Coordinates(x: 3, y: 5)
place2.getIntArray()

//let place3 = Coordinates(x: 3.5, y: 2.5)
//place3.getIntArray()

타입 제약(Type Constraint)

  • 제네릭에서 타입을 제약할수 있음
  • 타입 매개 변수 이름 뒤에 콜론으로 "프로토콜" 제약 조건 또는 "단일 클래스"를 배치 가능
  • (1) 프로토콜 제약 <T: Equatable>
  • (2) 클래스 제약 <T: SomeClass>
  • 특정 클래스와 상속관계에 내에 있는 클래스만 타입으로 사용할 수 있다는 제약 (구조체, 열거형은 사용 못
    • (해당 타입을 상속한 클래스는 가능)
  • Equatable 프로토콜을 채택한 타입만 해당 함수에서 사용 가능 하다는 제약
func findIndex<T: Equatable>(item: T, array:[T]) -> Int? {     // <T: Equatable>
    for (index, value) in array.enumerated() {
        if item == value {
            return index
        }
    }
    return nil
}

findIndex(item: 7 , array: [3, 4, 5, 6, 7])

let aNumber = 5
let someArray = [3, 4, 5, 6, 7]

if let index = findIndex(item: aNumber, array: someArray) {
    print("밸류값과 같은 배열의 인덱스: \(index)")
}

// 클래스 제약의 예시

class Person {}
class Student: Person {}

let person = Person()
let student = Student()

// 특정 클래스와 상속관계에 내에 있는 클래스만 타입으로 사용할 수 있다는 제약  (구조체, 열거형은 사용 못함)
// (해당 타입을 상속한 클래스는 가능)

func personClassOnly<T: Person>(array: [T]) {
    // 함수의 내용 정의
}

personClassOnly(array: [person, person])
personClassOnly(array: [student, student])

//personClassOnly(array: [Person(), Student()])

반대로 구체/특정화(specialization) 함수구현도 가능

  • 항상 제네릭을 적용시킨 함수를 실행하게만 하면, 또다른 불편함이 생기지 않을까?
  • (제네릭 함수가 존재하더라도) 동일한 함수이름에 구체적인 타입을 명시하면, 해당 구체적인 타입의 함수가 실행됨
  • 문자열의 경우, 대소문자를 무시하고 비교하고 싶어서 아래와 같이 구현 가능 ⭐️
  • 위의 findIndex<T: Equatable>(item: T, array:[T]) -> Int? 와 완전 동일
func findIndex(item: String, array:[String]) -> Int? {
    for (index, value) in array.enumerated() {
        if item.caseInsensitiveCompare(value) == .orderedSame {
            return index
        }
    }
    return nil
}

let aString = "jobs"
let someStringArray = ["Jobs", "Musk"]

if let index2 = findIndex(item: aString, array: someStringArray) {
    print("문자열의 비교:", index2)
}

프로토콜에서의 제네릭 문법의 사용

프로토콜에서 제네릭의 사용 - Associated Types(연관 타입)

  • 연관타입(Assiciated Types)으로 선언해야함 ⭐️
  • 프로토콜은 타입들이 채택할 수 있는 한차원 높은 단계에서
  • 요구사항만을 선언(자격증)하는 개념이기 때문에
  • 제네릭 타입과 조금 다른 개념(연관타입)을 추가적으로 도입한 것 뿐
protocol RemoteControl {           // <T>의 방식이 아님
    associatedtype T               // 연관형식은 대문자로 시작해야함(UpperCamelcase)
    func changeChannel(to: T)      // 관습적으로 Element를 많이 사용
    func alert() -> T?

    
}

// 연관형식이 선언된 프로토콜을 채용한 타입은, typealias로 실제 형식을 표시해야함

struct TV: RemoteControl {
    
    typealias T = Int       // 생략 가능
    
    func changeChannel(to: Int) {
        print("TV 채널바꿈: \(to)")
    }
    
    func alert() -> Int? {
        return 1
    }

}

let tv = TV()
tv.changeChannel(to: 2)
tv.alert()

class Aircon: RemoteControl {

    // 연관형식이 추론됨
    
    func changeChannel(to: String) {
        print("Aircon 온도바꿈: \(to)")
    }

    func alert() -> String? {
        return "1"
    }

}

let air = Aircon()
air.changeChannel(to: "20")
air.alert()
profile
iOS 개발자 공부하는 Roy

0개의 댓글