1.
Generic
1. Generic이란?
타입에 의존하지 않는 범용 코드를 작성할 때 사용
제네릭을 사용하면 중복을 피하고, 코드를 유연하게 작성할 수 O.
애플말에 따르면 swift에서 가장 강력한 기능중 하나로 swift 표준 라이브러리의 대다수는 제네릭으로 선언되어 있다고 함
흔히 사용하는 Array와 Dictionary 또한 제네릭 타입이다
1-1. 제네릭 함수(Generic Function)
만약 인자로 오는 두 Int 타입의 값을 swap하는 함수를 구현하려고 하면
func swapTwoInts(_ a: inout Int, _ b: inout Int) {
let tempA = a
a = b
b = tempA
}
Shell
복사
이렇게 구현할 수 있다
위의 경우엔 파라미터 모두 Int형일 때는 문제없이 잘 돌아가지만 파라미터 타입이 Double, String일 경우엔 사용할 수 X.
만약 Double, String에 대해서 swap함수를 사용하고 싶으면
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
}
Shell
복사
귀찮지만 형식에 맞게끔 함수를 오버로딩할 수 O.
이럴 때 사용하는 것이 제네릭이다.
타입에 제한을 두지 않는 코드를 사용하고 싶을 떄 쓴다.
func swapTwoValues<T>(_ a: inout T, _ b: inout T) {
let tempA = a
a = b
b = tempA
}
Shell
복사
<>를 이용해서 안에 타입처럼 사용할 이름(T)를 선언해주면 그 뒤로 해당 이름(T)를 타입처럼 사용할 수 O.
여기서 T를 Type Parameter라고 부르는데 T라는 새로운 형식이 생성되는 것이 아니라, 실제 함수가 호출될 떄 해당 매개변수의 타입으로 대체되는 Placeholder다.
왜 함수 이름 뒤에 <>로 T를 감싸냐면 T는 새로운 형식이 아니라 Placeholder기 떄문에 swift한테 T는 새로운 타입아니야, 그러니까 이 타입이 존재하는지 찾지마 자리타입이야라고 말해주기 위한 것이다.
따라서 swapTwoValues라는 함수를 제네릭으로 선언해주면
var someInt = 1
var aotherInt = 2
swapTwoValues(&someInt, &aotherInt) // 함수 호출 시 T는 Int 타입으로 결정됨
var someString = "Hi"
var aotherString = "Bye"
swapTwoValues(&someString, &aotherString) // 함수 호출 시 T는 String 타입으로 결정됨
Shell
복사
이렇게 실제 함수를 호출할 때, Type Parameter인 T의 타입이 결정되는 것이다.
여기선 파라미터 a,b 모두 같은 타입 파라미터인 T로 선언되었기 때문에
만약 서로 다른 타입을 파라미터로 전달하면
swapTwoValues(&someInt, &aotherString) // Cannot convert value of type 'String' to expected argument type 'Int'
Shell
복사
오류가 난다.
타입 파라미터는 굳이 T가 아닌 원하는 이름 마음대로 해도 되고, 한개 말고 여려개를 .를 이용해서 선언할 수 O.
func swapTwoValues<One, Two> { ... }
Shell
복사
또 타입 파라미터 이름을 선언할 떄는 가독성을 위해 T,V같은 단일 문자 or Upper Camel Case를 사용한다.
제네릭은 함수에만 가능한 것이아니라, 구조체, 클래스, 열거형 타입에서도 선언할 수 있는데 이것을 제네릭 타입(Generic Type)이라고 한다.
만약 stack을 제네릭으로 만들고 싶다면
struct Stack<T> {
let items: [T] = []
mutating func push(_ item: T) { ... }
mutating func pop() -> T { ... }
}
Shell
복사
이렇게 제네릭 타입으로 Stack을 선언할 수 O.
제네릭 타입의 인스턴스를 생성할 땐 선언과 마찬가지로 <>를 통해 어떤 타입으로 사용할 것인지 명시 해줘야 한다.
let stack1: Stack<Int> = .init()
let stack2 = Stack<Int>.init()
Shell
복사
자세히 보면 배열 생성할 때랑 똑같다
let array1: Array<Int> = .init()
let array2 = Array<Int>.init()
Shell
복사
사실 제네릭 타입을 배열을 공부할때 자연스럽게 제네릭을 사용헀다.
2. 타입 제약(Type Constraints)
제네릭 함수와 타입을 사용할 때 특정 클래스의 하위 클래스나, 특정 프로토콜을 준수하는 타입만 받을 수 있게 제약을 둘 수 O.
프로토콜 제약
파라미터로 두 개의 값을 받아서 두 값이 같으면 true, 다르면 false를 반환하는 함수를 제네릭으로 선언할 때
func isSameValues<T>(_ a: T, _ b: T) -> Bool {
return a == b // Binary operator '==' cannot be applied to two 'T' operands
}
Shell
복사
이렇게 쓰면 될것 같지만 에러가난다. 왜냐하면 ==연산자는 a,b의 타입이 Equatable이란 프로토콜을 준수할 때만 사용할 수 O.
따라서 이때는
func isSameValues<T: Equatable>(_ a: T, _ b: T) -> Bool {
return a == b
}
Shell
복사
이렇게 타입 파라미터에 T: Equatable를 사용해 제약을 줄수 있다.
클래스 제약
클래스 제약같은 경우 프로토콜 재약과 똑같지만 해당 자리에 프로토콜이 아닌 클래스 이름이 온다.
class Bird { }
class Human { }
class Teacher: Human { }
func printName<T: Human>(_ a: T) { }
Shell
복사
이렇게 T: Human 이런식으로 클래스 이름을 써주면
let bird = Bird.init()
let human = Human.init()
let teacher = Teacher.init()
printName(bird) // Global function 'printName' requires that 'Bird' inherit from 'Human'
printName(human)
printName(teacher)
Shell
복사
에러가 난다.
Human클래스 인스턴스인 human과, Human 클래스를 상속 받은(서브클래스) teacher는 printName이란 제네릭 함수를 실행 시킬수 있지만
Human클래스의 서브 클래스가 아닌 bird 인스턴스는 실행할 수 X
제네릭 확장하기
제네릭 타입인 Array를 확장하고 싶다면
extension Array {
mutating func pop() -> Element {
return self.removeLast()
}
}
Shell
복사
만약 제네릭 타입을 확장하면서 타입 파라미터를 사용할 경우
실제 Array 구현부에서 타입 파라미터가 Element기 떄문에 Element로 사용해야 함
만약 extension에서 새로운 제네릭을 선언하거나 다른 타입 파라미터를 사용하면 에러가 난다.
제네릭 함수와 오버로딩
제네릭은 보통 타입에 관계없이 동일하게 실행되지만 만약 특정 타입일 경우, 제네릭 말고 다른 함수로 구현하고 싶다면 제네릭 함수 오버로딩을 하면 된다.
func swapValues<T>(_ a: inout T, _ b: inout T) {
print("generic func")
let tempA = a
a = b
b = tempA
}
func swapValues(_ a: inout Int, _ b: inout Int) {
print("specialized func")
let tempA = a
a = b
b = tempA
}
Shell
복사
이렇게 할 경우 타입이 지정된 함수가 제네릭 함수보다 우선순위가 높아서
var a = 1
var b = 2
swapValues(&a, &b) //"specialized func"
var c = "Hi"
var d = "Sodeul!"
swapValues(&c, &d)
Shell
복사
Int 타입으로 swapValue를 실행할 경우 타입이 지정된 함수가 실행되고 String 타입으로 swapValue를 실행할 경우, 제네릭 함수가 실행된다
2.
protocol
protocol이란?
protocol은 타입으로 Swift의 class나 struct의 행동(func)을 정의하는 역할을 한다.
보통 프로그래밍에서의 protocol은 프로그램간에 데이터를 교환 방식을 정의하는 규칙을 말한다.
즉, 프로토콜은 코드 구현이 없는 규칙일 뿐이다. 따라서 프로토콜을 사용하는 프로그램에서 프로토콜을 보고 구현을 한다.
Swift에서도 마찬가지로 protocol은 행동을 정의하기만 할 뿐 규현하지 않는다.
protocol의 특징
•
protocol은 행동을 정의한다.
•
protocol은 타입이다.
protocol은 행동을 정의한다.
예를 들어 shop protocol이 있다면 shop protocol은 sell이라는 행동을 가진 protocol이다.
protocol Shop {
func sell()
}
Shell
복사
위 프로토콜을 보면 func sell()이 정의만 되어있을 뿐 구현을 하지 X. sell()의 행동을 구현하기 위해선 class나 struct에서 상속받아 구현해야 한다.
예를 들어 다음 class Coffeshop과 같이 Shop인터페이스에서 정의한 sell을 구현해 func를 완성시킬수 있다.
class CoffeShop: Shop {
func sell() {
print("Sell Coffe")
}
}
Shell
복사
protocol은 타입이다.
protocol을 이용해 타입을 정의하면 protocol을 구현하는 모든 class나 struct가 해당 protocol 타입을 수신하는 자리에 올 수 O.
예를 들어 doSellAction은 Shop protocol을 파라미터로 받는다.
func doSellAction(shop: Shop) {
shop.sell()
}
Shell
복사
위 코드에서 class CoffeeShop 객체를 생성해 이 자리에 오도록 해보면 CoffeShop은 Shop protocol을 상속 받으므로 Shop이 들어가야 할 자리에 올 수 O. 즉, Shop 타입을 받는 자리에 Shop 타입을 구현하는 CoffeShop이 올 수 O.
protocol이 중요한 이유
객체의 유연한 설계를 위해서는 정의와 구현이 분리되어야한다. 즉 객체가 OCP 원칙을 지키기 위해서는 정의와 구현을 분리 해야 한다.
(OCP: Open Close Principle, 확장에는 열려 있고 변경에는 닫혀있어야 한다는 원칙)
객체의 행동을 정의하는 타입을 만드는 것이 시작이다. 만약 func이 구체타입인 class나 struct를 인자로 받는다면 해당 인자에는 해당 class나 func만이 들어갈 수 O. 하지만, protocol을 인자로 받게 되면 protocol을 구현하는 어떤 인자든지 들어갈 수 있게 된다.
func doSellAction(shop: Shop){
shop.sell()
}
Shell
복사
따라서 이 자리에는 Shop을 구현하는 어떤 객체라도 들어올 수 O. 예를 들어 카페, 빵집이 모두 Shop을 상속받는다고 하면 이곳에는 어느 값이든 들어올 수 있게 된다.
let coffeeShop = CoffeeShop()
doSellAction(shop: coffeeShop()
let bakeryShop = BakeryShop()
doSellAction(shop: bakeryShop()
Shell
복사