상세 컨텐츠

본문 제목

Closure in Swift and @escaping, @autoclosure

Swift&SwiftUI

by (방울)도마토 2024. 9. 8. 20:14

본문

클로저(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:) 실행 전까지는 실행되지 않음 

관련글 더보기

댓글 영역