티스토리 뷰
이제는 문법은 지겹도록 봤기에 앱만들기에 집중하고 있어 제네릭문법은 간단하게 다루으나 핵심은 담겨있다..(아님말구☺️)
필요하면 그때 더 심화된 내용으로 포스팅~!
INDEX
1. 제네릭
2. 제네릭 타입
3. 연관 타입
4. 마무리
1. 제네릭 문법
1-1. 제네릭 문법은 왜(why) 필요한가?
// 정수 2개
var num1 = 10
var num2 = 20
// 두 숫자를 스왑(서로 교환)하는 함수의 정의
func swapTwoInts(_ a: inout Int, _ b: inout Int) {
let tempA = a
a = b
b = tempA
}
// 위에서 정의한 함수의 실행
swapTwoInts(&num1, &num2)
print(num1)
print(num2)
// 만약, Double을 교환하고 싶다면?, String을 교환하고 싶다면?
// Double 스왑 함수 정의
func swapTwoDoubles(_ a: inout Double, _ b: inout Double) {
let tempA = a
a = b
b = tempA
}
// String 스왑 함수의 정의
func swapTwoStrings(_ a: inout String, _ b: inout String) {
let tempA = a
a = b
b = tempA
}
제네릭이라는 문법이 없다면,
함수(클래스, 구조체, 열거형 등)타입마다
모든 경우를 다 정의해야 하기 때문에 번거롭다. (유지보수/재사용성 관점에서 어려움)
1-2. 제네릭이란?
Generic
- 형식에 관계없이, 한번의 구현으로 모든 타입을 처리하여, 타입에 유연한 함수 작성가능 (유지보수/재사용성 증가)
- (함수 뿐만아니라) 구조체 / 클래스 / 열거형도 제네릭으로 일반화 가능
- 타입 파라미터는 함수 내부에서 파라미터 형식이나 리턴형으로 사용됨 (함수 바디에서 사용하는 것도 가능)
- 관습적으로 T를 사용, 다른 이름을 사용해도 문제가 없음, 타입 이름이기에 UpperCamelcase(대문자)로 선언
- 2개이상을 선언하는 것도 가능 ex) 튜플로 반환 (T, T)
중요한건❗️
타입 파라미터는 실제 자료형으로 대체되는 place holder(어떤 기호같은것)로 새로운 (T라는) 형식이 생성되는 것이 아님
코드가 실행될때 문맥에 따라서 실제 형식으로 대체되는 "플레이스 홀더"임(코드 부분에서 설명🐶)
1-3 제네릭 함수 정의하기
- 타입 파라미터<T>는 함수 내부에서 파라미터의 타입이나 리턴형으로 사용됨 (함수 body에서 사용O)
1. 위에서 언급했듯이, 관습적으로 Type(타입)의 의미인 대문자 T를 사용하지만,
다른 문자를 사용해도 됨 <U> <A> <B> <Anything> (Upper camel case사용해야함)
2. <T, U> <A, B> 이렇게 타입 파라미터를 2개이상도 선언 가능
제네릭 함수의 사용
// placeholder의 역할(그저 표시 역할일뿐) (But 둘다 같은 타입이어야함)
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) // 🐶여기서 타입 T가 String으로 명확히 결정. 단 같은 타입으로만 전달가능
print(string1)
print(string2)
제네릭 함수로 배열 출력하기
let numbers = [2, 3, 4, 5]
let scores = [3.0, 3.3, 2.4, 4.0, 3.5]
let people = ["Jobs", "Cook", "Musk"]
func printArray<T>(array: [T]) {
for element in array {
print(element)
}
}
printArray(array: numbers) // placeholder는 [Int]
printArray(array: scores) // placeholder는 [Double]
printArray(array: people) // placeholder는 [String]
윗 배열에서 보이듯이
실제로 Swift에서 컬렉션은 모두 구조체의 제네릭 타입 <T>으로 구현되어 있었다...(이제는 <자료형>을 이해할 수 있음)
// 배열 타입
let array1: [String] = ["Steve", "Allen"]
let array2: Array<String> = ["Cook", "Musk"] // 실제 컴파일시 내부에서 일어나는 일
// 딕셔너리 타입
let dictType1: [String: Int] = ["Steve": 20, "Paul": 24]
let dictType2: Dictionary<String, Int> = ["Alex": 25, "Michel": 18]
// 옵셔널 타입
var optionalType1: String?
var optionalType2: Optional<String>
2. 제네릭 타입
2-1. 제네릭 타입 정의
- 구조체, 클래스, 열거형의 타입 이름 뒤에 타입 파라미터<T>를 추가하면, 제네릭 타입으로 선언됨
ex) struct Member<T>, class Point<T>
- 타입 파라미터는 타입 이름뒤에 선언하며 아래에서 나올 타입 제약 문법도 동일함 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])
클래스를 제네릭 타입으로 정의하기
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)
열거형에서 연관값을 가질때 제네릭으로 정의하기
(어차피 case는 선택항목 중에 하나일뿐, 이를 타입으로 정의할 일은 없다.. )
enum Pet<T> {
case dog
case cat
case etc(T)
}
let animal = Pet.etc("고슴도치") //etc("고슴도치")
2-2. 제네릭 타입의 확장
제네릭 구조체의 확장
//제네릭 구조체 정의
struct Coordinates<T> {
var x: T
var y: T
}
// 제네릭에 확장 적용하기
extension Coordinates { // Coordinates<T> (X)
// 튜플로 리턴하는 메서드
func getPlace() -> (T, T) {
return (x, y)
}
}
let place = Coordinates(x: 5, y: 5)
print(place.getPlace())
let place2 = Coordinates(x: 5.5, y: 10.5)
print(place2.getPlace())
where절을 추가해 확장 적용을 제한하기
// where절 추가도 가능
// Int타입에만 적용되는 확장과 getIntArray() 메서드
extension Coordinates where T == Int { // Coordinates<T> (X)
// 튜플로 리턴하는 메서드
func getIntArray() -> [T] {
return [x, y] //Int형 배열로 리턴
}
}
let place3 = Coordinates(x: 3, y: 5) //Int타입만 받을 수 있게됨
place3.getIntArray() // [3,5]
2-3. 제네릭 타입의 제약
- 제네릭에서 타입을 제약할 수도 있음
- 타입 매개 변수 이름 뒤에 콜론으로 "프로토콜" 제약 조건 또는 "단일 클래스"를 배치 가능
-(1) 프로토콜 제약<T: Equatable>
-(2) 클래스 제약 < T: SomeClass>
(1) 프로토콜 제약
// Equatable 프로토콜을 채택한 타입만 findIndex 함수를 사용가능하다는 제약
func findIndex<T: Equatable>(item: T, array:[T]) -> Int? { // <T: Equatable>
for (index, value) in array.enumerated() {
if item == value {
return index
}
}
return nil
}
let aNumber = 5
let someArray = [3, 4, 5, 6, 7]
if let index = findIndex(item: aNumber, array: someArray) {
print("밸류값과 같은 배열의 인덱스: \(index)")
}
//밸류값과 같은 배열의 인덱스: 2
(2) 클래스 제약 - 상속관계로 제약
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()])
//class Ailen {}
//var ailen = Ailen()
// personClassOnly(array: [ailen, ailen]) //Person클래스 상속X로 메서드 실행 불가함
반대로 구체/특정화(specialization) 함수도 가능하다
항상 제네릭을 적용시킨 함수를 실행하게만 하면, 또다른 불편함이 생기지 않을까?
(제네릭 함수가 존재하더라도) 동일한 함수이름에 구체적인 타입을 명시해 정의하면
해당 구체적인 타입의 함수가 실행됨(우선순위가 있다...)
아래예제의 경우 똑같은 findIndex함수지만 String타입으로 명확히 정의한 2번째 메서드가 실행됨
import UIKit
func findIndex<T: Equatable>(item: T, array:[T]) -> Int? {
for (index, value) in array.enumerated() {
if item == value {
return index
}
}
return nil
}
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)
}
3. 연관 타입
3-1 연관타입의 정의
프로토콜을 제네릭 방식으로 선언하려면 어떻게 해야할까?
⭐️연관타입(Assiciated Types)으로 선언⭐️
프로토콜은 타입들이 채택할 수 있는 한차원 높은 단계에서
요구사항만을 선언하는 개념이기 때문에
제네릭 타입과 조금 다른 개념인 연관타입을 추가적으로 도입
차이점은
<T>로 지금까지 했지만 프로토콜에서는 associatedtype T로 정의
protocol RemoteControl { // <T>의 방식이 아님
associatedtype T // 연관형식은 대문자로 시작해야함(UpperCamelcase)
func changeChannel(to: T) // 관습적으로 Element를 많이 사용
func alert() -> T?
}
struct TV: RemoteControl {
typealias T = Int // 생략 가능. why? 컴파일러가 자동 타입 추론 실행
func changeChannel(to: Int) {
print("TV 채널바꿈: \(to)")
}
func alert() -> Int? {
return 1
}
}
class Aircon: RemoteControl {
// 연관타입이 추론됨
func changeChannel(to: String) {
print("Aircon 온도바꿈: \(to)")
}
func alert() -> String? {
return "1"
}
}
3-2 연관타입에 제약 조건 추가
protocol RemoteControl2 {
associatedtype Element: Equatable // <T: Equatable> 제약조건 추가
func changeChannel(to: Element)
func alert() -> Element?
}
Equatable??
간단히 설명하면
Equatable 프로토콜을 채택해야만 == 연산이 가능한데,
Int,String, Double...기본적인 자료형들은 Equatable이 기본적으로 채택되어있기에
컴파일러가 자동구현 함
위 Element는 Equatable을 채택하는 타입이어야한다를 의미
4. 마무리
이번에는 제네릭 문법을 왜사용하고, 제네릭 함수, 제네릭 타입을 어떻게 정의하고 사용하며,
프로토콜에서도 제네릭 문법을 적용시킬 수 있는 방법(연관타입)에 대해서만 간단히 정리했다.
제네릭에 대한 기본적인 부분만 올리고 딱히 꼼꼼하게 정의 하지 않았다.
물론 제네릭에도 where절이 있고,
공식 문서를 따라 스택 자료구조를 Stack 제네릭 구조체 타입으로 정의해볼까 했으나,
앱을 만들다보니 이정도만 알고 있어도 충분하다고 생각..
어떤 문법이든 그게 필요해질때..더 자세히 하고 싶어지니 그때 심화된 내용으로 포스팅!
'Swift' 카테고리의 다른 글
Swift [Error Handling(throw, try, do-catch, rethrows)] (0) | 2023.03.07 |
---|---|
Swift [Closure 3부] (0) | 2023.03.04 |
Swift [Closure 2부] (0) | 2023.03.04 |
Swift [ARC- strong, memory leak, weak, unowned] (0) | 2023.03.02 |
Swift [Closure 1부] (0) | 2023.03.01 |