SW

클로저 탈출 본문

프로그래밍/Swift

클로저 탈출

LCD 2018. 10. 17. 16:44

탈출 클로저(Escaping Closure)


탈출(Escape) 클로저 ? : 함수의 전달인자로 전달한 클로저가 함수 종료 후에 호출될 경우 클로저가 함수를 탈출한다.


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


예를 들어 비동기 작업을 실행하는 함수들은 클로저를 Completion handler 전달인자로 받아온다. 비동기 작업으로 함수가 종료되고 난 후 작업이 끝나고 호출할 필요가 있는 클로저를 사용해야 할 경우에 탈출 클로저(Escaping Closure)가 필요하다.

 

! @escaping 키워드를 따로 명시하지 않는다면 매개변수로 사용되는 클로저는 기본적으로 비탈출 클로저이다, 비탈출 클로저는 함수의 동작이 끝난 후 전달된 클로저가 필요 없을 때 사용한다.



typealias VoidVoidClosure = () -> Void

let firstClosure: VoidVoidClosure = {

    print("Closure A")

}

let secondClosure: VoidVoidClosure = {

    print("Closure B")

}


func returnOneClousure(first: @escaping VoidVoidClosure, second: @escaping VoidVoidClosure, shouldReturnFirstClosure: Bool) -> VoidVoidClosure{

    return shouldReturnFirstClosure ? first : second

}


let returnedClosure: VoidVoidClosure = returnOneClousure(first: firstClosure, second: secondClosure, shouldReturnFirstClosure: true)


returnedClosure()


var closures: [VoidVoidClosure] = []


func appendClosure(closure: @escaping VoidVoidClosure) {

    closures.append(closure)

}


위 예제에서 두 함수의 전달인자로 전달하는 클로저 앞에 @escaping 키워드를 사용하여 탈출 클로저임을 명시해주었다. 이 코드는 클로저 모두가 탈출할 수 있는 조건이 명확하기 때문에 @escaping 키워드를 사용하여 탈출 클로저임을 명시하지 않으면 컴파일 오류가 발생한다.
 
이 코드는 함수 외부로 다시 전달되어 외부에서 사용이 가능하다든가, 외부 변수에 저장되는 등 클로저의 탈출 조건을 모두 갖추고 있다.

타입 내부 메서드의 매개변수 클로저에 @escaping 키워드를 사용하여 탈출 클로저임을 명시한 경우에는 클로저 내부에서 해당 타입의 프로퍼티나 메서드, 서브스크립트 등에 접근하려면 self 키워드를 명시적으로 사용해야 한다. 

비탈출 클로저는 클로저 내부에서 타입 내부의 프로퍼티나 메서드, 서브스크립트 등에 접근할 때 self 키워드를 꼭 써주지 않아도 된다.

즉 비탈출 클로저 내부에서 self 키워드는 선택 사항이다.


- 클래스 인스턴스 메서드에 사용되는 탈출, 비탈출 클로저 예제

typealias VoidVoidClosure = () -> Void


func functionWithNoescapeCloosure(closure: VoidVoidClosure) {

    closure()

}


func functionWithEscapingClosure(completionHandler: @escaping VoidVoidClosure) -> VoidVoidClosure {

    return completionHandler

}


class SomeClass {

    var x = 10;

    

    func runNoescapeClosure() {

        functionWithNoescapeCloosure {

            x = 200

        }

    }

    

    func runEscapingClosure() -> VoidVoidClosure {

        return functionWithEscapingClosure {

            self.x = 100

        }

    }

}


let instance: SomeClass = SomeClass()

instance.runNoescapeClosure()

print(instance.x//200


let returnedClosure: VoidVoidClosure = instance.runEscapingClosure()

returnedClosure()

print(instance.x//100


위 예제의 비탈출 클로저에서는 인스턴스의 프로퍼티인 x를 사용하기 위해 self 키워드를 생략해도 무관했지만 탈출클로저에서는 값 획득을 하기 위해 self 키워드를 사용하여 프로퍼티에 접근해야만 한다.

+ withoutActuallyEscaping

비탈출 클로저나 탈출 클로저의 여러 가지의 상황 중 한 가지 애매한 경우가 있다.


비탈출 클로저로 전달한 클로저가 탈출 클로저인 척을 해야 하는 경우이다. 실제로는 탈출하지 않는데 다른 함수에서 탈출 클로저를 요구하는 상황에 해당 된다.


다음 에제에서 구현한 함수 hasElements(in:match: ) 는 in 매개변수로 검사할 배열을 전달받으며, match라는 매개변수로 검사를 실행할 클로저를 받아들인다.


hasElements(in:match:) 함수는 @escaping 키워드가 없으므로 비탈출 클로저를 전달받게 된다. 그리고 내부에서 배열 lazy 컬렉션에 있는 filter 메서드의 매개변수로 비탈출 클로저를 전달한다. 그런데 lazy 컬렉션은 비동기 작업을 할 때 사용하기 때문에 filter 메서드가 요구하는 클로저는 탈출 클로저 이다.

그래서 탈출 클로저 자리에 비탈출 클로저를 전달할 수 없다는 오류와 마주한다.


그런데 함수 전체를 보면 match 클로저가 탈출할 필요가 없다. 이때 해당 클로저를 탈출 클로저인 것 처럼 사용할 수 있게 돕는 withoutActuallyEscaping(_:do:)함수가 있다.


let numbers: [Int] = [2468]


let evenNumberPredicate = {(number : Int) -> Bool in

    return number % 2 == 0

}


let oddNumberPredicate = {(number : Int) -> Bool in

    return number % 2 != 0

}


func hasElements(in array: [Int], match predicate: (Int) -> Bool) -> Bool {

    return withoutActuallyEscaping(predicate, do: {escapablePredicate in

        return (array.lazy.filter {escapablePredicate($0)}.isEmpty == false)

    })

}


let hasEvenNumber = hasElements(in: numbers, match: evenNumberPredicate)

let hasOddNumber = hasElements(in: numbers, match: oddNumberPredicate)


print(hasEvenNumber)

print(hasOddNumber)



withoutActuallyEscaping(_do:) 함수의 첫 번째 전달인자로 탈출 클로저인 척해야 하는 클로저가 전달되었다. do 전달인자는 이 비탈출 클로저를 또 매개변수로 전달받아 실제로 작업을 실행할 클로저를 전달합니다. 이렇게 withoutActuallyEscaping(_:do:) 함수를 활용하여 비탈출 클로저를 탈출 클로저 처럼 사용할 수 있다.




'프로그래밍 > Swift' 카테고리의 다른 글

옵셔널 이해하기 (복습)  (0) 2018.10.21
자동 클로저 (학습)  (0) 2018.10.17
클로저 값 획득 (복습)  (0) 2018.10.17