[Swift] 클로저에 대해 알아보자!

zooneon·2020년 11월 10일
0

Swift 기본 문법

목록 보기
6/14

본 내용은 '스위프트 프로그래밍' 책을 학습한 후 이를 바탕으로 작성한 글입니다.

클로저

클로저는 일정 기능을 하는 코드를 하나의 블록으로 모아놓은 것을 말한다.

기본 클로저

  • 기본 클로저 표현
{ (매개변수들) -> 반환 타입 in 
	실행 코드
}
  • 기본 클로저 예시
let names: [String] = ["zooneon", "mike", "minsoo", "bear"]

let reversed: [String] = names.sorted(by: { (first: String, second: String) -> Bool in
    return first > second
})

print(reversed)    //["zooneon", "minsoo", "mike", "bear"]

후행 클로저

  • 클로저가 조금 길어지거나 가독성이 떨어진다 싶을 때 후행 클로저 기능을 사용하면 좋다.
  • 후행 클로저는 맨 마지막 전달인자로 전달되는 클로저에만 해당된다.
  • 단 하나의 클로저만 전달인자로 전달하는 경우에는 소괄호를 생략 해줄 수 도 있다.
  • 매개변수에 클로저가 여러 개 있는 경우, 다중 후행 클로저 문법을 사용할 수 있다.
let names: [String] = ["zooneon", "mike", "minsoo", "bear"]

//후행 클로저 사용
let reversed: [String] = names.sorted() { (first: String, second: String) -> Bool in
    return first > second
}

//소괄호까지 생략 가능
let reserved: [String] = names.sorted { (first: String, second: String) -> Bool in
    return first > second
}

func doSomething(do: (String) -> Void,
                 onSuccess: (Any) -> Void,
                 onFailure: (Error) -> Void) {
    // do something...
}

//다중 후행 클로저 사용
doSomething { (something: String) in
    // do closure
} onSuccess: { (result: Any) in
    // success closure
} onFailure: { (error: Error) in
    // failure closure
}

클로저 표현 간소화

타입 유추

  • 메서드의 전달인자로 전달하는 클로저는 메서드에서 요구하는 형태로 전달해야 한다.
  • 전달인자로 전달할 클로저는 이미 적합한 타입을 준수하고 있다고 유추할 수 있다.
  • 즉, 매개변수의 타입이나 반환 값의 타입을 생략하더라도 문제가 없다.
let names: [String] = ["zooneon", "mike", "minsoo", "bear"]

//클로저의 타입 유추
let reversed: [String] = names.sorted { (first, second) in
    return first > second
}

단축 인자 이름

  • 단축 인자 이름은 첫 번째 전달인자부터 $0, $1, $2... 순서로 $와 숫자의 조합으로 표현한다.
  • 단축 인자 표현을 사용하게 되면 키워드 in을 사용할 필요도 없다.
let names: [String] = ["zooneon", "mike", "minsoo", "bear"]

//단축 인자 이름 사용
let reversed: [String] = names.sorted {
    return $0 > $1
}

암시적 반환 표현

  • 클로저에서는 return 키워드도 생략할 수 있다.
  • 만약 클로저가 반환 값을 갖는 클로저이고 클로저 내부의 실행문이 단 한 줄이라면, 암시적으로 그 실행문을 반환 값으로 사용할 수 있다.
let names: [String] = ["zooneon", "mike", "minsoo", "bear"]

//암시적 반환 표현
let reversed: [String] = names.sorted { $0 > $1 }

연산자 함수

  • 클로저는 매개변수의 타입과 반환 타입이 연산자를 구현한 함수의 모양과 동일하다면 연산자만 표기하더라도 알아서 연산하고 반환한다.

    → 연산자가 일종의 함수이기 때문

let names: [String] = ["zooneon", "mike", "minsoo", "bear"]

//연산자 함수를 클로저의 역할로 사용
let reversed: [String] = names.sorted(by: > )

값 획득

  • 클로저는 자신이 정의된 위치의 주변 문맥을 통해 상수나 변수를 획득할 수 있다.
  • 값 획득을 통해 클로저는 주변에 정의한 상수나 변수가 더 이상 존재하지 않더라도 해당 상수나 변수의 값을 자신 내부에서 참조하거나 수정할 수 있다.
  • 중첩 함수 주변의 변수나 상수를 획득해 놓을 수도 있다.
  • 클로저를 상수나 변수에 할당한다는 것은 클로저의 값을 할당하는 것이 아니라 해당 클로저의 참조를 할당하는 것이다.
//중첩 함수에서의 값 획득 예시
func makeIncrementer(forIncrement amount: Int) -> (() -> Int) {
    var runningTotal = 0
    func incrementer() -> Int {
        runningTotal += amount
        return runningTotal
    }
    return incrementer
}

incrementer() 함수는 runningTotalamount 변수의 참조를 획득할 수 있다.

참조를 획득하면 runningTotalamountmakeIncrementer 함수의 실행이 끝나도 사라지지 않는다.

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

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 ten: Int = incrementByTen()    //10
let twenty: Int = incrementByTen()    //20
let thirty: Int = incrementByTen()    //30

→ 각각의 incrementer 함수는 언제 호출이 되더라도 자신만의 runningTotal 변수를 갖고 카운트하게 된다.

자신만의 runningTotal 변수의 참조를 미리 획득했기 때문에 다른 함수의 영향도 받지 않는다.

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

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

→ 두 상수는 같은 클로저를 참조하기 때문에 동일한 클로저가 동작한다.

탈출 클로저

  • 함수의 전달인자로 전달한 클로저가 함수 종료 후에 호출될 때 클로저가 함수를 탈출한다고 표현한다.
  • @escaping키워드를 사용하여 클로저가 탈출하는 것을 허용한다고 명시해줄 수 있다.
  • @escaping키워드를 따로 명시하지 않는다면 매개변수로 사용되는 클로저는 기본으로 비탈출 클로저이다.
  • 클로저가 함수 외부에 정의된 변수나 상수에 저장되어 함수가 종료된 후에 사용될 경우 함수를 탈출해 있어야 한다. (eg: 비동기 작업)
  • 함수의 전달인자로 전달받은 클로저를 다시 반환할 때도 클로저가 함수를 탈출해 있어야 한다.
typealias VoidVoidClosure = () -> Void
let AClosure: VoidVoidClosure = {
    print("Closure A")
}
let BClosure: VoidVoidClosure = {
    print("Closure B")
}

func returnOneClosure(A: @escaping VoidVoidClosure, B: @escaping VoidVoidClosure, shouldReturnAClosure: Bool) -> VoidVoidClosure {
    return shouldReturnAClosure ? A : B
}

let returnedClosure: VoidVoidClosure = returnOneClosure(A: AClosure, B: BClosure, shouldReturnAClosure: true)

returnedClosure()    //Closure A

var closures: [VoidVoidClosure] = []

func appendClosure(closure: @escaping VoidVoidClosure) {
    closures.append(closure)
}

→ 탈출할 수 있는 조건이 명확한 경우 @escaping키워드를 사용하지 않으면 컴파일 오류가 발생한다.

typealias VoidVoidClosure = () -> Void

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

class SomeClass {
    var x = 10
    
    func runEscapingClosure() -> VoidVoidClosure {
        return functionWithEscapingClosure { self.x = 100 }
    }
}

let instance: SomeClass = SomeClass()
let returnedClosure: VoidVoidClosure = instance.runEscapingClosure()
returnedClosure()
print(instance.x)    //100

→ 탈출 클로저임을 명시한 경우, 클로저 내부에서 해당 타입의 프로퍼티나 메서드 등에 접근하려면 self 키워드를 명시적으로 사용해야 한다.

withoutActuallyEscaping

  • 비탈출 클로저로 전달한 클로저가 탈출 클로저인 척 해야 하는 경우에 사용한다.
let numbers: [Int] = [2, 4, 6, 8]

let evenNumberPredicate = { (number: Int) -> Bool in
    return number % 2 == 0
}

let oddNumberPredicate = { (number: Int) -> Bool in
    return number % 2 == 1
}

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)    //true
print(hasOddNumber)    //false

lazy컬렉션에 있는 filter메서드의 매개변수로 비탈출 클로저를 전달하는데, lazy컬렉션은 비동기 작업을 할 때 사용하기 때문에 filter메서드가 요구하는 클로저는 탈출 클로저이다.

이럴 경우에 withoutActuallyEscaping 함수를 사용한다.

자동 클로저

  • 함수의 전달인자로 전달하는 표현을 자동으로 변환해주는 클로저를 자동 클로저라고 한다.
  • 자동 클로저는 전달인자를 갖지 않고, 호출되었을 때 자신이 감싸고 있는 코드의 결괏값을 반환한다.
  • 자동 클로저는 클로저가 호출되기 전까지 클로저 내부의 코드가 동작하지 않는다.
var customerInLine: [String] = ["zooneon", "mike", "minsoo", "jhon"]

func serveCustomer(_ customerProvider: @autoclosure () -> String) {
    print("Now serving \(customerProvider())!")
}

serveCustomer(customerInLine.removeFirst())    //Now serving zooneon!
  • 기본적으로 @autoclosure 속성은 @noescape 속성을 포함한다.
  • 만약 자동 클로저를 탈출하는 클로저로 사용하고 싶다면 @autoclosure 속성 뒤에 @escaping 속성을 붙여서 사용하면 된다.
var customerInLine: [String] = ["zooneon", "mike", "minsoo", "jhon"]

func returnProvider(_ customerProvider: @autoclosure @escaping () -> String) -> (() -> String) {
    return customerProvider
}

let customerProvider: () -> String = returnProvider(customerInLine.removeFirst())
print("Now serving \(customerProvider())!")    //Now serving zooneon!

주의 ! 자동 클로저의 과도한 사용은 코드를 이해하기 어렵게 만들 수 있다.

profile
블로그 이전했습니다. https://blog.zooneon.dev

0개의 댓글