개발하는 뚝딱이

[swift] 클로저 본문

swift

[swift] 클로저

개발자뚝딱이 2020. 5. 7. 12:06

 

책 <스위프트 프로그래밍 3판 ;야곰 지음> 을 정리한 글입니다.

 


 

 

0. 클로저

- 클로저는 변수나 상수가 선언된 위치에서 참조(Reference)를 획득(Capture)하고 저장

- 이를 변수나 상수의 클로징이라고 하며 클로저는 여기서 착안된 이름

- 함수도 클로저의 형태 중 하나

- 클로저의 세 가지 형태

  • 이름이 있으면서 어떤 값도 획득하지 않는 전역함수의 형태
  • 이름이 있으면서 다른 함수 내부의 값을 획득할 수 있는 중첩된 함수의 형태
  • 이름이 없고 주변 문맥에 따라 값을 획득할 수 있는 축약 문법으로 작성한 형태

 

 

1. 기본 클로저

- 클로저의 기본 형태

{ (매개변수들) -> 반환타입 in
   실행 코드
}
// sorted(by:) 메서드에 클로저 전달

let name: [String] = ["james", "sam", "david", "su"]

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

 

 

2. 후행 클로저

- 함수나 메서드의 마지막 전달인자로 위치하는 클로저는 함수느 메서드의 소괄호를 닫은 후 작성해도 됨

- 클로저가 조금 길어지거나 가독성이 떨어질 때 후행 클로저 기능을 사용하면 좋음

- 단, 후행 클로저는 맨 마지막 전달인자로 전달되는 클로저에만 해당되므로 클로저 여러 개를 전달할 때는 맨 마지막 클로저만 후행 클로저를 사용할 수 있음

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

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

 

 

3. 클로저 표현 간소화

3.1 문맥을 이용한 타입 유추

- 메서드의 전달인자로 전달하는 클로저는 메서드에서 요구하는 형태로 전달해야 함

- 매개변수의 타입이나 개수, 반환 타입 등이 같아야 전달인자로서 전달 가능

- 매개변수의 타입이나 반환 타입을 굳이 표현해주지 않고 생략 가능

let reversed: [String] = name.sorted { (first, second) in
    return first > second
}

 

3.2 단축 인자 이름

- 첫 번째 전달인자부터 $0, $1, $2, $3, ... 순서로 $와 숫자의 조합으로 표현

- 매개변수와 반환 타입을 실행코드와 구분하기 위해 in 사용했으나 필요 없어짐

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

 

3.3 암시적 반환 표현

- return 키워드도 생략 가능

- 클로저가 반환 값을 갖고 클로저 내부의 실행문이 한 줄이면, 실행문을 반환값으로 사용 가능

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

 

3.4 연산자 함수

- '>'가 자체가 함수의 이름

// > 연산자 정의
public func ><T:Comparable>(lhs: T, rhs: T) -> Bool

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

 

 

4. 값 획득

- 클로저는 자신이 정의된 위치의 주변 문맥을 통해 상수나 변수를 획득 (Capture)

- 값 획득을 통해 클로저는 주변에 정의한 상수나 변수가 더 이상 존재하지 않더라도 해당 상수나 변수의 값을 자신 내부에서 참조하거나 수정할 수 있음

- 따라서, 비동기 작업에 많이 사용됨

- 클로저를 통해 비동기 콜백을 작성하는 경우, 현재 상태를 미리 획득해두지 않으면 실제로 클로저의 기능을 실행하는 순간에는 주변의 상수나 변수가 이미 메모리에서 존재하지 않는 경우가 발생

- 즉, 자신을 포함하는 함수의 지역변수나 지역상수를 획득할 수 있음

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

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 twnety: Int = incrementByTen() // 20
let rhid: Int = incrementByTen() // 30

- makeIncrementer 함수의 실행이 끝나도 runningTotal과 amount는 사라지지 않음

- 각각 자신만의 runningTotal의 참조를 미리 획득하여, 다른 함수의 영향을 전혀 받지 않음

- 그러나 클래스 인스턴스 프로퍼티로서의 클로저를 할당하면, 클러스와 인스턴스 사이에 강한참조 순환문제가 발생 (이후에 다룸)

 

 

 

5. 클로저는 참조 타입

- 함수나 클로저를 상수나 변수에 할당하는 것은 함수나 클로저의 참조를 설정하는 것

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

- 그래서 클로저의 참조를 다른 상수에게 할당해준다면 이 두 상수가 모두 같은 클로저를 가리킨다는 뜻

 

 

 

6.  탈출 클로저

- 함수의 파라메터로 전달한 클로저가 함수 종료 후에 호출될 때 클로저가 함수를 탈출한다고 표현

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

- @escaping 키워드를 명시하지 않으면 매개변수로 사용되는 클로저는 기본으로 비탈출 클로저

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

// 탈출 클로저를 파라메터로 갖는 함수

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

func someFunctionwithEscapingClosure(completionHandler: @escaping () -> Void) {
    completionHandlers.append(completionHandler)
}

 

- 탈출 클로저의 경우, 클로저 내부에서 해당 타입의 프로퍼티나 메서드, 서브스크립트 등에 접근하려면 self 키워드를 사용해야 함

typealias VoidVoidClosure = () -> Void

func functionWithNoescapeClosure(closure: VoidVoidClosure) {
    closure()
}

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


class SomeClass {
    var x = 10
    
    func runNoescapeClosure() {
        // 비탈출 클로저에서 self 키워드 사용은 선택사항
        functionWithNoescapeClosure { x = 200 }
    }
    
    func runEscapingClosure() -> VoidVoidClosure {
        // 탈출 클로저에서는 명시적으로 self를 사용해야 함
        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

 

 

6.1 withoutAcutallyEscaping

- 실제로는 탈출하지 않는데 다른 함수에서 탈출 클로저인 척 해야 하는 경우

func hasElements(in array: [Int], match predicate: (Int) -> Bool) -> Bool {
    return (array.lazy.filter {predicate($0)}.isEmpty == false) // 오류 발생
}

- hashElements(in:match:) 함수는 match라는 매개변수로 검사를 실행할 클로저를 받아들임

- hashElements(in:match:) 함수는 @escaping 키워드가 없으므로 비탈출 클로저를 전달받음

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

 

- withoutActuallyEscaping(_:do:)함수의 첫번째 전달인자로 탈출 클로저인 척해야 하는 클로저가 전달됨

- do 전달인자는 이 비탈출 클로저를 또 매개변수로 전달받아 실제로 작업을 실행할 탈출 클로저를 전달

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

 

 

7. 자동 클로저

- 함수의 전달인자로 전달하는 표현을 자동으로 변환해주는 클로저를 자동 클로저라고 함

- 자동 클로저는 전달인자를 갖지 않음

- 호출되었을 때 자신이 감싸고 있는 코드의 결괏값을 반환

- 함수로 전달하는 클로저를 (소괄호와 중괄호를 겹쳐서 써야 하는) 어려운 클로저 문법을 사용하지 않고도 클로저로 사용할 수 있도록 문법적 편의를 제공

- 자동클로저는 클로저가 호출되기 전까지 클로저 내부의 코드가 동작하지 않음, 따라서 연산을 지연시킬 수 있음

// 클로저를 이용한 연산 지연

var customersInLine: [String] = ["Eric", "Jin", "Taylor", "Sophie"]
print(customersInLine.count) // 4

// 클로저를 만들어두면 클로저 내부의 코드를 미리 실행하지 않고 가지고만 있음
let customerProvider: () -> String = {
    return customersInLine.removeFirst()
}
print(customersInLine.count) // 4

// 실제로 실행
print("Now serving \(customerProvider())!") // "Now serving Eric!"
print(customersInLine.count) // 3
// 함수의 전달인자로 전달하는 클로저

var customersInline: [String] = ["Eric", "Jin", "Taylor", "Sophie"]

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

serveCustomer({ customersInline.removeFirst() }) // "Now serving Eric"
// 자동 클로저의 사용

var customersInLine: [String] = ["Eric", "Jin", "Taylor", "Sophie"]

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

serveCustomer(customersInLine.removeFirst()) // "Now serving Eric!"
// 자동 클로저의 탈출

var customersInLine: [String] = ["Eric", "Jin", "Taylor", "Sophie"]

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

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

 

'swift' 카테고리의 다른 글

[swift] 맵, 필터, 리듀스  (0) 2020.05.13
[swift] 옵셔널 체이닝과 빠른 종료  (0) 2020.05.08
[swift] 접근제어  (0) 2020.05.02
[swift] 인스턴스 생성 및 소멸  (0) 2020.05.01
[swift] 옵셔널  (0) 2020.04.28