Swift 문법 심화 목차
1. 프로퍼티 옵저버
2. 타입 캐스팅
3. 접근제한자
4. 클로저
5. 고차함수
6. 예외처리
7. ARC와 메모리 누수
8. 프로토콜
9. 확장
10. 제네릭
11. 비동기와 네트워킹
12. Combine 맛보기
13. RxSwift 맛보기
Swift 문법 심화 정리 (1) 은 여기로
오늘은 클로저, 고차함수, 제네릭을 다뤄 볼 예정
클로저는 일종의 익명 함수로, 코드에서 다른 함수나 메서드에 인수로 전달하거나 변수에 저장할 수 있다. 함수와 달리 클로저는 파라미터와 반환 타입을 명시하지 않을 수 있고, 문맥에 따라 타입을 추론한다. (컴파일러가 할 수 있는 선에서)
사실 이전에 다뤘던 내용이라 자세한건 링크로
클로저(Closure) - 1
클로저(Closure) - 2
사실 이거 쓸때만 해도 공식문서 번역이라 이게 뭔지... 어떻게 쓰라는건지 잘 몰랐는데 직접 써보니까 좀 알거같은 느낌이다
고차함수는 다른 함수를 인수로 받거나 함수를 반환하는 함수이다.
대표적인 예제로는 map
, filter
, reduce
같은 배열 메서드가 있다.
map
map
은 배열의 각 요소에 동일한 변환을 적용해서 새로운 배열을 반환한다.자세한건 [Swift] map, compactMap 에서 다뤘음
filter
filter
는 배열의 각 요소를 조건에 따라 걸러서 새로운 배열을 반환한다.reduce
reduce
는 배열의 모든 요소를 하나로 합쳐서 단일 값을 만든다. 초기값을 지정하고, 클로저를 통해 두 값을 합치는 방식으로 동작한다.이거 역시 자세한건 [Swift][프로그래머스] 배열의 평균값 - reduce() 여기로...
고차함수를 직접 만들어볼 수도 있다.
func applyTwice(_ function: (Int) -> Int, to value: Int) -> Int {
return function(function(value))
}
let result = applyTwice({ $0 * 2 }, to: 3)
print(result) // 12
여기서 applyTwice
함수는 함수와 값을 인수로 받아서, 그 함수를 두 번 적용한 결과를 반환한다. 첫 번째 호출에서 3이 6이 되고, 두 번째 호출에서 6이 12가 된다.
이렇게 고차 함수를 사용하면 코드의 재사용성이 높아지고, 더 유연하고 간결한 코드를 작성할 수 있다.
스위프트에서 제네릭은 타입에 의존하지 않는 코드를 작성할 수 있게 하여 함수의 재사용성을 높인다. 함수, 구조체, 클래스, 열거형 등에 제네릭을 적용할 수 있다.
스위프트의 표준 라이브러리인Array
, Dictionary
등은 모두 제네릭 타입이다. 생각해보니 그렇네.
func swapTwoValues<T>(_ a: inout T, _ b: inout T) {
let temporaryA = a
a = b
b = temporaryA
}
여기서 T
는 제네릭 타입 인자로, 함수 이름 뒤에 <T>
를 붙여서 이 함수가 제네릭 함수임을 나타낸다. 이 T
는Int
, String
, Double
등 어떤 타입이라도 될 수 있고, 어떤 값이라도 서로 교환할 수 있다.
var firstInt = 10
var secondInt = 20
swapTwoValues(&firstInt, &secondInt)
print("\(firstInt), \(secondInt)") // 20, 10
var firstString = "Hello"
var secondString = "World"
swapTwoValues(&firstString, &secondString)
print("\(firstString), \(secondString)") // World, Hello
제네릭을 사용한 swapTwoValues 함수가 string도 받고 int도 받을 수 있는걸 보여준다.
함수뿐 아니라 클래스나 구조체에서도 제네릭을 사용할 수 있다.
struct Stack<Element> {
var items = [Element]()
mutating func push(_ item: Element) {
items.append(item)
}
mutating func pop() -> Element? {
return items.popLast()
}
}
여기서 Stack
구조체는 제네릭 타입 Element
를 사용하여Int
, String
, 또는 어떤 타입이든 저장할 수 있다.
var intStack = Stack<Int>()
intStack.push(1)
intStack.push(2)
print(intStack.pop()) // Optional(2)
var stringStack = Stack<String>()
stringStack.push("Hello")
stringStack.push("World")
print(stringStack.pop()) // Optional("World")
Stack 구조체인 intStack, stringStack이 Int뿐 아니라 String도 저장할 수 있음을 보여준다.
제네릭을 사용할 때는 타입 제약도 추가할 수 있다. 예를 들어, 특정 프로토콜을 준수하는 타입만 허용할 수도 있다.
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 numbers = [1, 2, 3, 4, 5]
if let index = findIndex(of: 3, in: numbers) {
print("Index of 3 is \(index)") // Index of 3 is 2
}
여기서 T: Equatable
은 T
타입이 Equatable
프로토콜을 준수해야 한다는 제약을 뜻하며, 덕분에 ==
연산자를 사용할 수 있게 된다.
Equatable
프로토콜을 준수하지 않는 타입은 ==
이나 !=
를 사용할 수 없다. 따라서 위처럼 제약을 걸지 않았다면,
func findIndex<T>(of valueToFind: T, in array: [T]) -> Int? {
for (index, value) in array.enumerated() {
if value == valueToFind { // 오류 발생
return index
}
}
return nil
}
저 주석에서 오류가 나게 된다.
처음에 단순히 아니 Int나 String으로 선언해서 ==
나 !=
를 쓸 때는 오류가 안나는데? 저 만능처럼 보이는 제네릭으로 선언한다고 갑자기 오류가 난다고? 싶었는데...
스위프트에서는 기본 타입(Int, String 등)에서 ==
연산자가 이미 정의되어 있어서, 이 타입들을 제네릭 함수에서 사용할 때는 Equatable
을 명시적으로 사용하지 않아도 되는 경우가 많고 그래서 오류가 발생하지 않는 것처럼 보일 수 있다고 한다.
하지만 제네릭 함수에서 Equatable
제약을 명시적으로 사용하지 않으면, 커스텀 타입이나 Equatable
을 준수하지 않는 타입을 사용하려고 할 때 오류가 발생한다. 쉽게 생각하면 Class나 Struct끼리 땡으로 비교를 시도한다고 보면 된다.
Equatable
없이 ==
연산자를 사용해보자:
func findIndex<T>(of valueToFind: T, in array: [T]) -> Int? {
for (index, value) in array.enumerated() {
if value == valueToFind { // 컴파일 오류 발생 가능
return index
}
}
return nil
}
여기서 T
타입이 String
, Int
같은 기본 타입이라면 ==
연산자가 이미 정의되어 있어서 오류가 발생하지 않을 수 있다.
let numbers = [1, 2, 3, 4, 5]
if let index = findIndex(of: 3, in: numbers) {
print("Index of 3 is \(index)") // Index of 3 is 2
}
위 코드에서는 Int
타입을 사용하기 때문에 문제가 없다. 하지만 커스텀 타입을 사용한다면 다음과 같은 문제가 발생할 수 있다.
다음과 같은 커스텀 타입을 만들어보자:
struct Person {
var name: String
var age: Int
}
let people = [Person(name: "Alice", age: 30), Person(name: "Bob", age: 25)]
if let index = findIndex(of: Person(name: "Alice", age: 30), in: people) {
print("Index of Alice is \(index)")
}
이 코드는 컴파일 오류가 발생한다. 왜냐하면 구조체인 Person
타입은 ==
연산자를 지원하지 않기 때문. 이 문제를 해결하려면 Person
타입이 Equatable
을 준수하도록 만들어야 한다.
struct Person: Equatable {
var name: String
var age: Int
static func == (lhs: Person, rhs: Person) -> Bool {
return lhs.name == rhs.name && lhs.age == rhs.age
}
}
이제 Person
타입은 Equatable
을 준수하기 때문에 ==
연산자를 사용할 수 있다.
let people = [Person(name: "Alice", age: 30), Person(name: "Bob", age: 25)]
if let index = findIndex(of: Person(name: "Alice", age: 30), in: people) {
print("Index of Alice is \(index)") // Index of Alice is 0
}
기본 타입에서는 Equatable
을 명시적으로 사용하지 않아도 ==
연산자가 이미 정의되어 있어서 오류가 발생하지 않지만, 커스텀 타입이나 Equatable
을 준수하지 않는 타입을 사용하려면 Equatable
을 명시적으로 사용해야 한다.