아래의 예시를 보면서 비교를 해보자! 공식 문서 참조
// 두 숫자를 스왑(서로 교환)하는 함수의 정의
func swapTwoInts(_ a: inout Int, _ b: inout Int) {
let tempA = a
a = b
b = tempA
}
// Double을 스왑하는 함수의 정의
func swapTwoDoubles(_ a: inout Double, _ b: inout Double) {
let tempA = a
a = b
b = tempA
}
// (정수가 아닌) 문자열을 스왑하는 함수의 정의
func swapTwoStrings(_ a: inout String, _ b: inout String) {
let tempA = a
a = b
b = tempA
}
위의 함수를 비교해보면 함수 명은 다르지만 각자가 하는 역할은 동일하다!
앞에 데이터와 뒤의 데이터를 변경하는 기능이다.
함수를 호출 시 매개변수에 대한 참조를 전달하는 방법.
함수 내에서 매개변수의 값을 직접 변경을 할 수 있는 기능을 제공한다.
var num1 = 123 // 전역변수
var num2 = 456 // 전역변수
func swap(a: Int, b: Int) {
var c = a
a = b
b = c
}
swap(a: num1, b: num2)
// 실행시 에러
error: cannot assign to value: 'a' is a 'let' constant
a = b
^
error: cannot assign to value: 'b' is a 'let' constant
b = c
^
위의 코드를 실행했을 때, 함수 내의 파라미터는 기본적으로 값타입이며, 임시상수이기 때문에 이를 변경할 수 없다!
그리하여 다음과 같이 변경을 해야한다.
num1 = 123
num2 = 456
func swapNumbers(a: inout Int, b: inout Int) {
var temp = a
a = b
b = temp
}
// 함수 실행시에는 앰퍼샌드를 꼭 붙여야함
swapNumbers(a: &num1, b: &num2)
print(num1) // 출력: 456
print(num2) // 출력: 123
파라미터의 복사본이 전달되는 것이 아니라 원본이 전달이 된다!
이 원본이 전달이되는 의미를 가지는 것이 &(앰퍼샌드)를 붙여야한다.
그리하여 결과적으로 함수 외부에 선언한 전역변수 num1, num2는 값이 변경이 되어 출력이 된다.
위의 코드와 함께 비교하였을 때, 동일한 기능이지만 각 타입마다 모든 경우를 정의해야하므로, 개발자의 할 일이 늘어나고, 유지보수와 재사용 관점에서 보았을 때도 너무 힘들게된다!
형식에 관계없이, 한번의 구현으로 모든 타입을 처리하여, 타입에 유연한 함수 작성가능 (유지보수/재사용성 증가)
(함수 뿐만아니라) 구조체 / 클래스 / 열거형도 제네릭으로 일반화 가능
보통은 T를 사용하지만 다른 이름을 사용하는 것도 문제가 없음, 형식이름이기 때문에 UpperCamelcase로 선언
(관습적으로 Type(타입)의 의미인 대문자 T를 사용하지만, 다른 문자를 사용해도 됨.
ex) <U> <A> <B> <Anything> (Upper camel case 사용해야함 - 맨 앞글자를 대문자로 표기)
2개이상을 선언하는 것도 가능
ex) <T, U> <A, B>
제네릭은 타입에 관계없이, 하나의 정의(구현)로 모든 타입(자료형)을 처리할 수 있는 문법
제네릭 함수, 제네릭 구조체/클래스
타입 파라미터는 실제 자료형으로 대체되는 플레이스 홀더(어떤 기호같은것) ===> 새로운 형식이 생성되는 것이 아님.
코드가 실행될때 문맥에 따라서 실제 형식으로 대체되는 "플레이스 홀더"일뿐
그렇다면 위에 선언하였던 함수를 제네릭 문법을 통해 변경해보려고한다.
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) // 출력 : world
print(string2) // 출력 : hello
var number1 = 123
var number2 = 456
swapTwoValues(&number1, &number2)
print(number1) // 출력 : 456
print(number2) // 출력 : 123
함수 뒤에 <T> 를 붙여 제네릭 문법을 적용하였다.

위의 그림과 같이 내가 선언했던 T로 보이는 것을 확인할 수 있다.
하지만, 내가 아래와 같이 코드를 변경한다면,
func swapTwoValues<Hello>(_ a: inout Hello, _ b: inout Hello) {
let tempA = a
a = b
b = tempA
}

표시될 때도 이와같이 Hello로 변경되는 것처럼 상관은 없지만 대부분이 T를 사용한다.
// 구조체로 제네릭의 정의하기
struct GenericMember<T> {
var members: [T] = []
}
var member1 = GenericMember(members: ["Jobs", "Cook", "Musk"]) // String
var member2 = GenericMember(members: [1, 2, 3]) // Int
// 클래스로 제네릭의 정의하기
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) // Int
let bPoint = GridPoint(x: 10.4, y: 20.5) // Double
// 열거형에서 연관값을 가질때 제네릭으로 정의가능
// (어차피 케이스는 자체가 선택항목 중에 하나일뿐(특별타입)이고, 그것을 타입으로 정의할 일은 없음)
enum Pet<T> {
case dog
case cat
case etc(T)
}
let animal = Pet.etc("고슴도치")
제네릭 타입을 확장도 할 수 있다.
이때, 타입 파라미터 명시 없이 확장을 할 수 있다. 아래의 예시를 보자!
struct Coordinates<T> {
var x: T
var y: T
}
// 제네릭을 Extension(확장)에도 적용할 수 있다.
extension Coordinates { // Coordinates<T> (X)
// 튜플로 리턴하는 메서드
func getPlace() -> (T, T) {
return (x, y)
}
}
let place = Coordinates(x: 5, y: 5)
print(place.getPlace()) // 출력: (5, 5)
// where절 추가도 가능
// Int타입에만 적용되는 확장과 getIntArray() 메서드
extension Coordinates where T == Int { // Coordinates<T> (X)
// 배열로 리턴하는 메서드
func getIntArray() -> [T] {
return [x, y]
}
}
let place2 = Coordinates(x: 3, y: 5)
print(place2.getIntArray()) // 출력: [3, 5]
코드상에서 확장 시에는 타입 파라미터를 작성하지 않아도된다! 그리고 확장 시 사용되는 타입은 위에서 적용되는 타입과 동일하게 된다.
제네릭에 타입을 제약할 수 있다.
타입 매개 변수 이름 뒤에 콜론으로 "프로토콜" 제약 조건 또는 "단일 클래스"를 배치할 수 있다.
// 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
}
let aNumber = 5
let someArray = [1, 2, 3, 4, 5, 6, 7]
if let index = findIndex(item: aNumber, array: someArray) {
print("밸류값과 같은 배열의 인덱스: \(index)")
}
<T: SomeClass>class Person {
let name: String
init(name: String) {
self.name = name
}
}
class Student: Person {
let studentID: Int
init(name: String, studentID: Int) {
self.studentID = studentID
super.init(name: name)
}
}
// 특정 클래스 및 해당 클래스를 상속받은 클래스만을 대상으로 하는 제네릭 함수
func printAllowedPeople<T: Person>(array: [T]) {
print("Allowed people:")
for person in array {
print("- \(person.name)")
}
}
let person1 = Person(name: "John")
let person2 = Person(name: "Alice")
let student1 = Student(name: "Bob", studentID: 123)
let student2 = Student(name: "Eve", studentID: 456)
printAllowedPeople(array: [person1, person2]) // Person 클래스만을 포함한 배열
// 실행 결과
// Allowed people:
// - John
// - Alice
printAllowedPeople(array: [student1, student2]) // Student 클래스를 포함한 배열
// 실행 결과
// Allowed people:
// - Bob
// - Eve
위에서 선언했던 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)
}
// 실행 결과: 문자열의 비교: 0
만약에 구체적인 함수를 설정하지 않는다면, if let 구문에서 실행을 했을 때 nil의 결과가 출력되어 print가 실행되지 않았을 것이다.
하지만 String라는 구체적인 타입을 설정하여 문자열인 경우 해당 함수를 실행할 수 있도록 구체적으로 선언을 하였을 경우 해당 함수가 실행이 되며 결과값도 출력이 된다.
⭐️ 프로토콜은 타입들이 채택할 수 있는 한차원 높은 단계에서 요구사항만을 선언하는 개념
-> 제네릭 타입과는 다른 <연관 타입>이라는 약속을 사용.
<T> -> associatedtype T
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
}
}
TV().alert()
TV().changeChannel(to: 10)
class Aircon: RemoteControl {
// 연관형식이 추론됨. 생략됨
// typealias T = String
func changeChannel(to: String) {
print("Aircon 온도바꿈: \(to)")
}
func alert() -> String? {
return "1"
}
}
Aircon().changeChannel(to: "10도")
Aircon().alert()
protocol RemoteControl3 {
associatedtype T: Equatable // <T: Equatable> 제약조건 추가
func changeChannel(to: T)
func alert() -> T?
}
제네릭은 서로 다른 타입에 대해 동일한 기능을 하는 함수나 타입을 작성할 때 사용된다!
이를 통해 각 타입에 맞게 모든 함수를 구현하지 않아도 되어 코드 유지보수와 재사용성을 높일 수 있다!!!