클로저(Clousure) : 일회용 함수를 작성할 수 있는 구문, 익명(Anonymous) 함수
- 스위프트에서 제공하는 클로저는 소프트웨어 아키텍처에서의 클로저의 개념을 포함!
→ 자신이 정의된 context로부터, 모든 상수와 변수의 값을 capture 하거나 reference를 저장하는 익명함수
⇒ 이름이 생략된 함수!
클로저 표현식
{ (매개변수) -> 반환 타입 in
실행할 구문
}
- 일반적인 함수와 달리 시작 부분에서 이미 중괄호가 선언됨 → in 키워드를 사용해 실행 블록의 시작을 표현
func greet(name: String) -> String {
return "Hello, \(name)!"
}
let greetClosure: (String) -> String = { (name: String) in
return "Hello, \(name)!"
}
// 타입 추론을 통해 타입 어노테이션을 생략
let greetClosure = { (name: String) in
return "Hello, \(name)!"
}
// $0: 첫 번째 인자
let greetClosure = { "Hello, \($0)!" }
// 사용 예시
let result = greetClosure("Bob")
print(result) // "Hello, Bob!"
예시) sort 함수
정렬 함수 sort(by:)
- 정렬: 두 값의 비교를 반복하는 알고리즘
→ 두 값을 비교하여 작은 값을 앞으로, 큰 값을 뒤로 배치하는 과정을 무수히 반복하여 순서를 바꿀 값이 더이상 나타나지 않을 때 정렬이 완료
→ 두 개의 인자값을 입력받고 비교하여 Bool 타입을 반환
- 첫 번째 인자값이 두 번째 인자값보다 앞쪽에 와야 한다고 판단하면 true, 이외에는 false 반환
- 내부적으로 임의의 비교 기준을 정하여 이 기준에 따라 큰 값과 작은 값을 구분해도 됨!(결과도 일관된 기준에 따라 Bool값 반환)
// 정렬 함수 sort(by:)를 이용하여 순서대로 정렬하기
var value = [1, 9, 5, 7, 3, 2]
// 내림차순으로 정렬하기
func order(s1: Int, s2: Int) -> Bool {
if s1 > s2 {
return true
} else {
return false
}
}
// 클로저를 사용하지 않고 sort(by:)를 사용하는 경우
value.sort(by: order)
print(value) // [9, 7, 5, 3, 2, 1]
value.sort(by: { (s1: Int, s2: Int) -> Bool in
if s1 > s2 {
return true
} else {
return false
}
})
value.sort(by: { (s1: Int, s2: Int) -> Bool in return s1 > s2 })
// 타입 추론을 통해 반환값 표현 생략
value.sort(by: { (s1: Int, s2: Int) in
return s1 > s2
})
// 타입 추론을 통해 매개변수 타입 생략
value.sort(by: { s1, s2 in return s1 > s2 })
// 내부 상수 $0, $1, $2 ... 등을 사용하여 매개변수 생략하기
// 해당 값은 입력받은 인자값의 순서대로 매칭됨 -> 첫 번째 인자값이 $0에 매칭
// 실행 구문과 클로저 구문 부분을 분리할 필요가 없으므로 in 키워드도 생략가능
value.sort(by: { return $0 > $1 })
// 반환 타입을 추론할 수 있으므로 return 구문까지 생략가능
value.sort(by: { $0 > $1 })
// 연산자 함수(Operator Functions)
value.sort(by: >)
트레일링 클로저(Trailling Closure)
- 함수의 마지막 인자값이 클로저일 때, 이를 인자값 형식으로 작성하는 대신 함수의 뒤에 꼬리처럼 붙일 수 있는 문법. 이때 인자 레이블은 생략됨
- 함수의 마지막 인자값에만 적용됨
→ 클로저를 인자값으로 받더라도 마지막 인자값이 아니라면 적용할 수 없음
→ 인자값이 하나라면 첫번째 인자값이지만 동시에 인자값이므로 트레일링 클로저 문법 사용 가능
// 인자값이던 클로저가 바깥으로 빼어지고, sort()의 뒤쪽에 붙음
// -> 코딩 과정에서 sort()를 여닫는 범위가 줄어듦!
value.sort() { (s1, s2) in
return s1 > s2
}
// 인자값이 하나일 때는 괄호도 생략할 수 있음
value.sort { (s1, s2) in
return s1 > s2
}
- 인자값이 여러 개일 경우, 무작정 괄호를 생략할 수는 없음
func divideTraillingClosure(base: Int, success s: () -> Void) -> Int {
defer {
s()
}
return 100 / base
}
// 인자값이 하나 이상이면 괄호를 생략할 수 없음
divideTraillingClosure(base: 100) { () in
print("연산 성공!")
}
// input이 없으므로 아래와 같은 구문도 가능
divideTraillingClosure(base: 100) {
print("연산 성공!")
}
@: attribute
@escaping and @autoclosure
@escaping: 코드 작성 시 클로저를 변수나 상수에 대입하거나 중첩 함수 내부에서 사용해야 할 경우 사용되는 것
- 인자값으로 전달된 클로저를 저장해두었다가, 다른 곳에서도 실행할 수 있도록 허용해주는 속성
func callback(fn: () -> Void) {
let f = fn // Non-escaping parameter 'fn' may only be called
// Non-escaping parameter 'fn'은 직접 호출만 가능함
f()
}
- 함수의 인자값으로 전달된 클로저는 기본적으로 탈출불가(non-escape)의 성격을 가짐
→ 해당 클로저를 1. 함수 내에서 2. 직접 실행을 위해서만 사용
→ 함수 내부라 할지라도 변수나 상수에 클로저를 대입할 수 없음
→ 변수나 상수에 대입하는 것을 허용한다면 내부 함수를 통한 capture 기능을 이용하여 클로저가 함수 바깥으로 탈출할 수 있기 때문
(탈출: 함수 내부 범위를 벗어나서 실행하는 것)
// 실행 안됨
func callback(fn: () -> Void) {
func innerCallback() {
fn()
}
}
- 인자값으로 전달된 클로저는 중첩된 내부 함수에서 사용할 수도 없음
→ 내부 함수에서 사용할 수 있도록 허용할 경우, 역시 context의 capture를 통해 탈출될 수 있기 때문
⇒ @escaping으로 탈출이 가능한 인자값으로 설정하여 제약조건을 없앨 수 있음
// @escaping은 인자값에 설정되는 값이므로, 함수 타입 앞에 넣어줘야 함
func callback(fn: @escaping () -> Void) {
let f = fn // 클로저를 상수 f에 대입
f() // 대입된 클로저를 실행
}
callback {
print("closure 실행!")
}
[참조] 인자값으로 전달되는 클로저의 기본 속성이 탈출불가하도록 설정된 이유?
- 컴파일러가 코드를 최적화하는 과정에서의 성능향상
→ 해당 클로저가 탈출할 수 없다 = 컴파일러가 더 이상 메모리 관리상의 지저분한 일들에 관여할 필요가 없다
- 탈출 불가 클로저 내에서는 self 키워드를 사용할 수 있음
→ 해당 함수가 끝나서 리턴되기 전에 호출될 것이 명확하기 때문 (클로저 내에서 self에 대한 weak reference를 사용할 필요가 없음)
[참조] self 키워드를 쓸 수 있다는 게 무슨 의미이지? (feat. GPT)
Swift에서 self 키워드는 클래스나 구조체의 인스턴스 자신을 참조할 때 사용됩니다. 클로저 내에서 self를 사용하는 방식은 클로저가 탈출(escaping)하는지, 아니면 비탈출(non-escaping) 클로저인지에 따라 달라집니다.
탈출 불가 클로저 (Non-escaping Closure)
- 탈출 불가 클로저는 함수의 인자로 전달된 클로저가 함수의 실행이 끝날 때까지 실행되며, 함수가 종료된 후에는 클로저가 더 이상 사용되지 않는 경우를 의미합니다. 즉, 함수 내에서만 실행되는 클로저입니다.
- self 키워드를 명시적으로 사용할 필요가 없습니다.
이유:
탈출 불가 클로저는 함수의 범위 내에서만 실행되기 때문에, 해당 함수가 끝나기 전에 클로저가 호출될 것이 보장됩니다. 따라서 클로저가 self를 강하게 참조해도 메모리 누수가 발생할 위험이 없습니다. 즉, 클로저 내에서 self에 접근할 때 weak 또는 unowned 같은 약한 참조를 사용할 필요가 없다는 뜻입니다.
탈출 가능한 클로저 (Escaping Closure)
- 반대로, 탈출 가능한 클로저는 함수가 끝난 후에도 호출될 수 있는 클로저입니다. 예를 들어, 비동기 작업에서 주로 사용됩니다. 탈출 가능한 클로저는 함수가 종료된 후에도 self에 접근할 수 있기 때문에, self가 메모리에서 해제되지 않도록 주의해야 합니다.
여기서 문제가 되는 이유:
- 탈출 가능한 클로저에서 self를 직접 참조하면, 강한 참조 순환(Strong Reference Cycle)이 발생할 수 있습니다. 즉, 클로저가 self를 강하게 참조하고, self가 클로저를 강하게 참조하면 둘 다 메모리에서 해제되지 않는 상태가 발생합니다.
- 이를 방지하기 위해 weak self 또는 unowned self를 사용하여 약한 참조로 만들어야 합니다.
예시)
class MyClass {
var name = "Swift"
func doSomething() {
printName { print(self.name) } // 탈출 불가 클로저
}
func doSomethingAsync() {
DispatchQueue.main.async {
print(self.name) // 탈출 가능한 클로저에서는 strong reference로 위험
}
}
}
func printName(closure: () -> Void) {
closure() // 함수 내부에서 바로 호출 (탈출 불가)
}
정리
- 탈출 불가 클로저에서는 함수가 끝나기 전에 클로저가 호출된다는 것이 보장되므로, self를 참조할 때 메모리 관리에 대한 걱정을 덜어도 됩니다. 따라서 클로저 내에서 `self`를 바로 사용할 수 있습니다.
- 반면에, 탈출 가능한 클로저는 함수가 종료된 후에도 실행될 수 있기 때문에, self가 메모리에서 해제되지 않도록 weak self나 unowned self를 사용해 참조 순환을 방지해야 합니다.
@autoclosure: 인자값으로 전달된 일반 구문이나 함수 등을 클로저로 wrapping 할 수 있음
- 일반 구문을 인자값으로 넣더라도 컴파일러가 자동으로 클로저 형태로 감싸 처리
- 인자값을 {} 형태가 아닌 () 형태로 사용할 수 있는 장점
func condition(stmt: () -> Bool) {
if stmt() == true {
print("true")
} else {
print("false")
}
}
// 실행구문 1: 일반 구문
condition(stmt: {
4 > 2
})
// 실행구문 2: 클로저 구문
condition {
4 > 2
}
/* -------------------------------------------------------- */
// @autoclosure 속성 적용
func condition(stmt: @autoclosure () -> Bool) {
if stmt() == true {
print("true")
} else {
print("false")
}
}
// 실행 구문 -> 안에 들어간 내용만 인자값으로 넣어줌
condition(stmt: ( 4 > 2))
지연된 실행(Delayed Execution)
- 어떤 값이나 작업이 필요할 때까지 실행을 미루는 것
- 메모리의 효율성을 높이고, 필요하지 않는 코드를 즉시 실행하지 않도록 할 때 유용함
// (): 배열 초기화, 빈 배열을 생성한다
var arrs = [String]()
func addVars(fn: @escaping () -> Void) {
// 배열 요소를 3개까지 추가하여 초기화
arrs = Array(repeating: "", count: 3)
// 인자값으로 전달된 클로저 실행
fn()
}
// 오류 발생 : addVars(fn:)이 실행되기 전까지 arrs의 인덱스는 0까지 밖에 없음
// 두 번째 위치에 "KR"을 입력할 수 없음
// arrs.insert("KR", at: 1)
// 배열 초기화 이후 값을 삽입
addVars(fn: arrs.insert("KR", at: 1))
- 함수 내에 작성된 구문은 함수가 실행하기 전까지 실행되지 않음
→ @autoclosure 가 부여된 인자값은 클로저로 감싸지기 때문에 addVars(fn:) 실행 전까지는 실행되지 않음
Structure and Class in Swift (3) | 2024.09.18 |
---|---|
함수의 생명주기와 참조 카운트의 연관성(feat. chat GPT) (0) | 2024.09.08 |
Nested Function (feat. Closure in Software Architecture) (0) | 2024.09.08 |
First-Class Object - 일급 객체로서의 함수 (0) | 2024.09.08 |
[Ellen] FAQ: 조건문 (0) | 2024.08.27 |
댓글 영역