iOS/Swift Grammar

[Swift] 클로저(Closure) 간소화 표현, 값 획득, 탈출 클로저

kyxxn 2024. 2. 6. 13:06
728x90

클로저 표현 간소화

문맥을 이용한 타입 유추

let reversed: [String] = names.sorted { (s1, s2) in
    return s1 > s2
}

클로저는 메소드가 요구하는 형태로 전달해야 함

매개변수 타입과 개수, 반환 타입 등이 같아야 전달인자로 전달되므로

클로저는 이미 적합한 타입을 준수한다고 유추할 수 있기에 생략해도 됨

단축 인자 이름

let reversed: [String] = names.sorted {
		return $0 > $1
}

간결한 표현을 통해 매개변수 이름 생략가능

in 키워드도 안 적어도 됨

암시적 반환 표현

let reversed: [String] = names.sorted { $0 > $1 }

클로저가 반환 값을 갖는 클로저이고, 클로저 내부의 실행문이 단 한 줄일 경우 암시적으로 그 실행문을 반환 값으로 사용가능

return 키워드까지 생략 가능

 

UIKit에서 TableView를 사용할 때 쓰는 Delegate와 DataSource 프로토콜의 필수 구현문인 numberOfRowsInSection 메소드에서 경험해봤음

연산자 함수

비교 연산자 (>, <)은 두 개의 피연산자를 통해 Bool 타입을 반환

sorted(by:) 메소드에 전달한 클로저와 동일한 조건

‘>’ 연산자의 정의

public func ><T: Comparable>(lhs: T, rhs: T) -> Bool
// 함수 이름이 '>' 인 거임

sorted 함수와 사용

let reversed: [String] = names.sorted(by: >)

값 획득 (Value capture)

클로저는 자신이 정의된 위치의 주변 문맥을 통해 상수나 변수 획득이 가능

값 획득을 통해 주변에 정의한 변수, 상수가 더 이상 존재하지 않더라도

해당 값을 자신 내부에서 참조하거나 수정이 가능함

클로저를 통해 비동기 콜백을 작성하는 경우

현재 상태를 미리 확보해두지 않으면, 실제로 클로저의 기능을 실행하는 순간 주변 상수나 변수가 이미 메모리에 존재하지 않을 수 있음

실제로 클로저가 비동기 작업에 많이 사용된다 함

중첩 함수의 변수나 상수를 획득해둘 수 있음

func makeIncrementer(forIncrement amount: Int) -> () -> Int {
    var runningTotal = 0
    func incrementer() -> Int {
        runningTotal += amount
        return runningTotal
    }
    return incrementer
}

makeIncrementer 함수의 반환 타입은 () → Int, 함수 객체를 반환한다는 의미

반환되는 함수는 매개변수가 없고 Int를 반환함

incrementer가 반환하게 될 값을 저장하는 용도로 runningTotal을 정의했고, 0으로 초기화 함

이 함수가 호출될 때마다 amount의 값만큼 runningTotal 변수 값 증가

값 증가를 하는 incrementer 함수는 중첩 함수임

독립적으로 보면 아무것도 못 하지만, 중첩 함수이기에 runningTotal과 amount에 대한 참조를 획득할 수 있음

참조를 획득하면 makeIncrementer 함수가 끝나도 사라지지 않음

let incrementByTwo: (() -> Int) = makeIncrementer(forIncrement: 2)

let first: Int = incrementByTwo() // 2
let second: Int = incrementByTwo() // 4
let third: Int = incrementByTwo() // 6

각각의 incrementer 동작

let incrementByTwo: (() -> Int) = makeIncrementer(forIncrement: 2)
let incrementByTwo2: (() -> Int) = makeIncrementer(forIncrement: 2)
let incrementByFive: (() -> Int) = makeIncrementer(forIncrement: 5)

let first: Int = incrementByTwo() // 2
let second: Int = incrementByTwo() // 4
let third: Int = incrementByTwo() // 6

let first2: Int = incrementByTwo2() // 2
let second2: Int = incrementByTwo2() // 4
let third2: Int = incrementByTwo2() // 6

let first3: Int = incrementByFive() // 5
let second3: Int = incrementByFive() // 10
let third3: Int = incrementByFive() // 15

각각의 incrementer 함수는 언제 호출이 되더라도 자신만의 runningTotal 변수를 갖음

다른 함수의 영향도 안 받는 게, 자신만의 runningTotal의 참조를 미리 획득했기 때문

클래스의 인스턴스의 프로퍼티로 클로저를 할당하면,

클로저는 해당 인스턴스 || 인스턴스의 멤버의 참조를 획득할 수 있으나 강한참조 문제 발생 염두

클로저는 참조 타입 (@escaping)

함수와 클로저는 참조 타입

함수나 클로저를 상수나 변수에 할당할 때마다,

사실은 상수나 변수에 함수나 클로저의 참조를 설정하는 것

즉, incrementByTwo 라는 상수에 클로저를 할당한다는 것은

클로저의 내용물, 즉 값을 할당하는 것이 아니라 해당 클로저의 참조를 할당하는 것

클로저의 참조를 다른 상수에 할당한다면 이는 두 상수가 모두 같은 클로저를 가리킴

let incrementByTwo: (() -> Int) = makeIncrementer(forIncrement: 2)
let sameByTwo: (() -> Int) = incrementByTwo

let first: Int = incrementByTwo()  // 2
let second: Int = sameByTwo() // 4

incrementByTwo와 sameByTwo 두 상수 모두, 같은 클로저를 참조하기에

동일한 클로저가 동작하여 runningTotal 변수가 같은 것을 알 수 있음

탈출 클로저

함수의 전달인자로 전달한 클로저가 함수 종료 후에 호출될 때, 클로저가 함수를 탈출한다고 함

클로저를 매개변수로 갖는 함수를 선언할 때, 매개변수 이름의 콜론(:) 뒤에 @escaping 키워드를 통해 클로저가 탈출하는 것을 허용한다고 명시할 수 있음

예를 들어, 비동기 작업을 실행하는 함수들은 클로저를 컴플리션 핸들러(Completion handler) 전달인자로 받아옴

비동기 작업으로 함수가 종료되고 난 후, 호출할 필요가 있는 클로저를 사용해야 할 때 탈출 클로저가 필요하게 됨

sorted(by:) 메소드를 비롯해, 계속 살펴본 함수에는 @escaping 키워드가 없음

정렬할 요소를 연산하기 위해 전달인자로 전달한 클로저는 비탈출 클로저(Nonescape)이기에.

@escaping을 명시하지 않는다면, 매개변수로 사용되는 클로저는 기본적으로 비탈출 클로저

함수로 전달된 클로저가 함수의 동작이 끝난 후, 사용할 필요가 없을 때 비탈출 클로저 사용

클로저가 함수를 탈출할 수 있는 경우 중 하나는 함수 외부 정의된 변수나 상수에 저장되어 함수가 종료된 후에 사용할 경우

ex) 비동기로 작업을 하기 위해 컴플리션 핸들러를 전달인자를 이용해 클로저 형태로 받는 함수가 많음

함수가 작업을 종료하고 난 이후(즉, 함수의 return 후)에 컴플리션 핸들러, 즉 클로저를 호출하기에 클로저는 함수를 탈출해 있어야만 함

함수의 전달인자로 전달받은 클로저를 다시 반환할 때도 마찬가지

탈출 클로저를 매개변수로 갖는 함수

var completionHandlers: [() -> Void] = []

func someFunctionWithEscapingClosure(completionHandler: @escaping () -> Void) {
    completionHandlers.append(completionHandler)
}
let firstClosure: () -> Void = { print("First Closure") }
let secClosure: () -> Void = { print("Second Closure") }

// first와 second 매개변수 클로저는 함수의 반환 값으로 사용될 수 있으므로 탈출 클로저
func returnOneClosure(first: @escaping () -> Void,
                      second: @escaping () -> Void,
                      TorF: Bool) -> () -> Void {
    // 전달인자로 전달받은 클로저를 함수 외부로 다시 반환하기에 함수를 탈출하는 클로저
    return TorF ? first : second
}

let returnedClosure: () -> Void = returnOneClosure(first: firstClosure,
                                                  second: secClosure,
                                                  TorF: true)

returnedClosure() // First Closure

var closures: [() -> Void] = []

// 함수에서 반환한 클로저가 함수 외부의 상수에 저장됨
func appendClosure(closure: @escaping () -> Void) {
    // 전달인자로 전달받은 클로저가 함수 외부의 변수 내부에 저장되므로 함수를 탈출함
    closures.append(closure)
}

위 코드에서 두 함수의 전달인자에 @escaping을 적어 탈출 클로저임을 명시함

클로저 모두가 탈출할 수 있는 조건이 명확하기에 @escaping 키워드를 사용하여 탈출 클로저임을 명시하지 않으면, 컴파일 오류가 발생하게 됨

이 코드는 함수 외부로 다시 전달되어 외부에서 사용이 가능하다든가, 외부 변수에 저장되는 등 클로저의 탈출 조건을 모두 갖춤

타입 내부 메소드의 매개변수 클로저에 @escaping 키워드로 명시한 경우,

클로저 내부에서 해당 타입의 프로퍼티나 메소드, 서브스크립트 등에 접근하려면 self 키워드를 명시적으로 사용해야 함

비탈출 클로저는 클로저 내부에서 타입 내부의 프로퍼티 | 메소드 | 서브스크립트 등에 self 키워드 안 써줘도 됨

클래스 인스턴스 메소드에 사용되는 탈출, 비탈출 클로저

import Foundation

func someFunctionWithNonescapingClosure(closure: () -> Void) {
    closure()
}

func someFunctionWithEscapingClosure(completionHandler: @escaping () -> Void) -> () -> Void {
    return completionHandler // return 생략가능
}

class SomeClass {
    var x = 10
    
    func runNoescapeClosure() {
        // 비탈출 클로저에서 self 키워드 사용은 선택
        someFunctionWithNonescapingClosure { x = 200 }
    }
    
    func runEscapingClosure() -> () -> Void {
        // 탈출 클로저에서는 명시적으로 self 사용해야 함
        return someFunctionWithEscapingClosure { self.x = 100 }
    }
}

let instance: SomeClass = SomeClass()
instance.runNoescapeClosure()
print(instance.x) // 200

let returnedClosure: () -> Void = instance.runEscapingClosure()
returnedClosure()
print(instance.x) // 100

 


참고

https://docs.swift.org/swift-book/documentation/the-swift-programming-language/closures/#Capturing-Values

 

Documentation

 

docs.swift.org

https://babbab2.tistory.com/81

 

Swift) 클로저(Closure) 정복하기(1/3) - 클로저, 누구냐 넌

안녕하세요 :) 소들입니다 으휴 저번 주도 쓸데없이 바빴어서 포스팅을 못했네용 나태한 저번주의 나를 반성하며.. 하암..🥱 음 전에 제가 Swift의 꽃이 Optional이라고 말한 적 있는데, Optional 만큼

babbab2.tistory.com

+ 야곰 스위프트 문법 교재