01 정의
제네릭은 같은 동작을 하는 함수에서 타입만 변경하고 싶을 때 각각의 타입에 맞는 함수를 불필요하게 설계하는 일을 줄일 수 있다.
코드를 보면 쉽게 알 수 있다.
var someNum1 = [0, 1, 2, 3, 4]
var someDoubleNum1 = [0.1, 0.2, 0.3, 0.4]
var someString1 = ["Swift", "Developer", "Apple"]
func someGenerics(array: [Int]) {
for int in array {
print(int)
}
}
func someGenerics1(array: [Double]) {
for double in array {
print(double)
}
}
func someGenerics2(array: [String]) {
for string in array {
print(string)
}
}
someGenerics(array: someNum1) // 0, 1, 2, 3, 4
someGenerics1(array: someDoubleNum1) // 0.1, 0.2, 0.3, 0.4
someGenerics2(array: someString1) // "Swift", "Developer", "Apple"
위 코드는 같은 동작을 하지만 타입이 모두 다르다.
이럴 경우 번거로움을 덜기 위해 제네릭 함수를 설계하여 중복적인 함수를 하나로 통합하여 설계가 가능하다.
02 제네릭 함수 설계
1) 타입 파라미터를 지정하기
2) 타입 파라미터를 사용하여 함수 설계
func 함수명<T>(파라미터 T ) { code }
이렇게 간편하게 제네릭 함수의 설계가 가능하다.
즉 한번 구현으로 모든 타입을 커버 할 수 있는 함수로서 유지보수 및 재사용성의 효율이 좋게 설계가 가능하다.
func someGenericsFunc<T>(a: inout [T]) {
//배열이 아니라면 그냥 T
for element in a {
print(element)
}
}
someGenericsFunc(a: &someNum1)
someGenericsFunc(a: &someDoubleNum1)
someGenericsFunc(a: &someString1)
//타입 파라미터의 이름은 아무거나 해도 상관없다
func someGenericsFunc<TOT>(a: inout [TOT]) {
for element in a {
print(element)
}
}
someGenericsFunc(a: &someNum1) // 0, 1, 2, 3, 4
someGenericsFunc(a: &someDoubleNum1) // 0.1, 0.2, 0.3, 0.4
someGenericsFunc(a: &someString1) // "Swift", "Developer", "Apple"
03 제네릭 타입 정의
제네릭은 사용 방법에 맞게 타입으로 정의하여 코드의 유연성을 높일 수 있다.
정의할 수 있는 타입은 다음과 같다.
1. Struct
2. Class
3. enum
4. 함수의 리턴형으로 사용 가능
5. 파라미터로 사용 가능
각 타입 이름 뒤에 타입 파라미터<T>를 추가하여 사용 한다. (제네릭 타입으로 선언) 즉, 어떤 타입이 들어갈 수 있는 타입으로 선언
struct Member {
var members: [String] = []
}
//구조체로 정의
struct GenericsMember<T> {
var members: [T] = []
}
let a = GenericsMember(members: [1,2,3,])
let b = GenericsMember(members: [0.1, 0.2, 0.3])
let z = GenericsMember(members: ["멍멍이", "고양이", "수달"])
//z.members = [1, 2, 3] //컨파일러 에러 이미 string 타입으로 선언되었음으로, 정수 할당 불가
//메모리 구조가 결정되는 순간 변경 불가능
//클레스로 정의 가능
class GridPoint<T> {
var x: T
var y: T
init(x: T, y: T) {
self.x = x
self.y = y
}
}
let apoint = GridPoint(x: 4, y: 5)
let stringPoint = GridPoint(x: "-1", y: "3")
let doublePoint = GridPoint(x: 0.1, y: 0.5)
//연관값을 가질떄 제네릭으로 정의 가능
//case는 선택항목 중 하나일 뿐으로, 특정 타입으로 정의할일이 거의 없다.
enum Name<T> {
case personName
case job
case somePersonAge(T)
}
var name1 = Name.somePersonAge(30)
//함수의 파라미터, 리턴형으로 활용 가능
class Cclass<T> {
func somefunc(a: T) -> T {
let c = a
return c
}
}
04 제네릭 구조체의 확장
정의된 제네릭 형식의 구조체를 확장하여 사용도 가능하다.
또 where절을 사용하여 확장된 구조체에서 특정 타입에만 적용되도록 할 수 있다.
struct Generics4<T> {
var pointA: T
var pointB: T
}
//구조체의 확장
extension Generics4 { //플레이스홀더 <T> 사용 불필요
func c() -> (T, T) {
return(pointA, pointB)
}
}
let point = Generics4(pointA: 3, pointB: 4)
let point1 = Generics4(pointA: 0.1, pointB: 4.3)
where절을 통해 특정 타입에서만 적용되도록 정의
//where절로 특정 타입에만 구현이 되도록 설계 가능
extension Generics4 where T == Int {
//where절을 통해 인트타입으로 한정시켜 놓음
func point() -> [T] {
return [pointA, pointB] //튜플로 리턴
}
}
let point2 = Generics4(pointA: 3, pointB: 4)
point2.point()
let point3 = Generics4(pointA: "-1", pointB: "5")
//point3.point() //컴퍼일 에러
05 제네릭 형식의 프로토콜
제네릭 형식의 프로토콜을 사용하여 코드의 유연성과 재사용성을 증가시킬 수 있다. 즉 , 해당 프로토콜을 채택한다면 매서드 구현 시 같은 타입으로 구현해야한다.
제네릭 형식의 프로토콜을 정의하고 책할때는 주의가 필요하다.
1. 정의: associatedtype를 반듯이 사용하여 정의
2. 채택: typealias로 채택한 프로토콜의 타입을 설정 할 수 있지만 생략도 가능하다.
protocol Person {
associatedtype T
//제네릭 플레이스 홀더 설정(연관타입으로 선언) 관습적으로 Element라고 많이 씀
func ueser(number: T, age:T) -> [T]
func person() -> T?
}
class Member: Person {
//프로토콜 채택
typealias T = Int //생략가능
//제네릭 형식 프로토콜을 사용 시 필수적으로 제네릭의 타입을 선언해서 사용
func person() -> Int? {
return 1
}
func ueser(number name: Int, age: Int) -> [Int] {
ueser(number: 10, age: 15)
}
}
//다른 타입으로도 지정 가능
class Member1: Person {
typealias T = String//생략가능
func ueser(number: String, age: String) -> [String] {
ueser(number: "열번째", age: "15살")
}
func person() -> String? {
return "회원정보"
}
}
//연관 형식에 제약을 추가 가능
protocol Person1 {
associatedtype Element: Equatable
//제약 조건 추가
func ueser(number: Element, age:Element) -> [Element]
func person() -> Element?
}
06 제네릭 타입 제약
제네릭 타입의 제약은 두가지 경우가 있다.
1) Equatable프로토콜을 채택한 경우
Equatable프로토콜이란 swift에서 제공하는 프로토콜 중 하나로, 타입간 동등성 비교( == , !=) 을 사용할 수 있게 해주는 코드이다.
따라서 Equatable프로토콜을 통해 제네릭 타입의 제약을 둘 수 있다.
즉, 두 값이 동등한지 비교할 수 있는 타입에서만 작동하도록 제약을 둘 수 있다.
class MyClass<T: SomeClass> {
//SomeClass를 상속
var object: T
init(object: T) {
self.object = object
}
}
func findIndex<T: Equatable>(of valueToFind: T, in array: [T]) -> Int? {
for (index, value) in array.enumerated() {
if value == valueToFind {
return index
}
}
return nil
}
let arr = ["apple", "banana", "orange", "grape"]
let index = findIndex(of: "orange", in: arr)
print(index) // Output: 2
2) 클레스 제약
특정 클래스와 상속 관계에 있는 클래스만 사용 할 수 있도록 제약을 둘 수 있는데, 이를 통해 코드의 안정성을 높일 수 있다.
class Animal {
var name: String
init(name: String) {
self.name = name
}
}
class Dog: Animal {}
func printNameOfAnimal<T: Animal>(animal: T) {
print(animal.name)
}
let dog = Dog(name: "Puppy")
printNameOfAnimal(animal: dog) // "Puppy"
'🍎swift' 카테고리의 다른 글
[Swift] 연산자 커스텀 타입 (0) | 2023.04.05 |
---|---|
Swift - 문자열 다루기 01 (0) | 2023.03.29 |
메모리 관리 (0) | 2023.03.12 |
고차함수 (0) | 2023.03.07 |
swift 클로저 01 - Closure사용 이유와 형태 (0) | 2023.02.27 |