Swift

[Swift] 클로저 : Closure

yevdev 2022. 3. 20. 13:42
함수형 프로그래밍 패러다임과 클로저의 관계
- 스위프트에서 함수형 프로그래밍 패러다임을 접할 때 첫걸음으로 꼭 알아야할 클로저!
- 클로저와 제네릭, 프로토콜, 모나드 등의 결합으로 스위프트는 훨씬 강력한 언어가 되었다.

함수와 클로저?
- 함수는 클로저의 한 형태!

 

클로저?

변수나 상수가 선언된 위치에서 참조를 획득하고 저장할 수 있다.

이를 변수나 상수의 클로징(잠금)이라고 하며, 여기서 착안된 이름 '클로저'

 

클로저의 형태

1. 이름이 있으면서 어떤 값도 획득하지 않는 전역함수의 형태

2. 이름이 있으면서 다른 함수 내부의 값을 획득할 수 있는 중첩된 함수의 형태

3. 이름이 없고 주변 문맥에 따라 값을 획득할 수 있는 축약 문법으로 작성한 형태

 

 

 

1️⃣ 클로저의 표현방법 (위치를 기준으로)

1. 기본 클로저

클로저 표현

{ (매개변수들) -> 반환 타입 in

    실행 코드

}

 

정렬을 위한 함수 전달 (sorted(by:) 매서드 이용)

func backwards(first: String, second: String) -> Bool {
    return first > second
}

let names: [String] = ["yejin", "zejin", "hejin"]

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

print(reversed)

// ["zejin", "yejin", "hejin"]

sorted(by:)메서드에 클로저 전달

let names: [String] = ["yejin", "zejin", "hejin"]

// backwards(first:second:) 함수 대신에 sorted(by:) 매서드의 전달인자로 클로저를 직접 전달
let reversed: [String] = names.sorted(by: {
    (first: String, second: String) -> Bool in
     return first > second
})

print(reversed)

 

➰클로저도 함수와 마찬가지로 입출력 매개변수를 사용할 수 있다. 매개변수 이름을 지정한다면 가변 매개변수 또한 사용 가능! 다만, 클로저는 매개변수 기본값을 사용할 수 없다.

 

➰전달인자를 함수로 보낸다?

- 함수를 매서드의 전달인자로 보내는 일은 함수형 프로그래밍 패러다임에서는 아주 당연한 일!

 

2. 후행 클로저

후행클로저의 사용

- 클로저가 조금 길어지거나 가독성이 조금 떨어진다 싶을 때 사용!

 

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

또한, sorted(by:) 매서드처럼 하나의 클로저만 전달인자로 전달하는 경우에는 소괄호를 생략해줄 수도 있다.

 

후행 클로저를 사용한 sorted(by:)메서드

let names: [String] = ["yejin", "zejin", "hejin"]

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

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

 

 

2️⃣ 클로저 표현 간소화

1. 문맥을 이용한 타입 유추 : 타입 생략 가능

2. 단축 인자 이름 

- 위의 코드에서 first와 second가 매우 의미 없음!! 우리는 이를 생략해주고 $0, $1 과 같이 표현할 수 있고, 매개변수 및 반환 타입과 실행코드를 구분하기 위해 사용했던 in 키워드를 생략해줄 수 있다.

3. 암시적 반환 표현 : return 키워드 또한 생략 가능

4. 연산자 함수 (>)

클로저로서의 연산자 함수 사용

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

 

➰ 덧셈 클로저 간소화하기 전

func calculate(a: Int, b: Int, method: (Int, Int) -> Int) -> Int {
	return method(a, b)
}

result = calculate(a: 10, b: 10, method: {(left: Int, right: Int) -> Int in
	return left + right
}

➰ 덧셈 클로저 간소화 후

result = calculate(a: 10, b: 10) { $0 + $1 }

 

 

 

3️⃣ 값 획득

incrementer라는 함수를 중첩함수로 표현하는 makeIncrementer

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

() -> Int ?

makeIncrementer의 반환 타입으로, 함수 객체를 반환한다는 의미가 된다!

 

상수에 함수 할당

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

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

 

 

4️⃣ 클로저는 참조 타입!

값 획득 부분의 예제는 상수 타입이다. 

함수와 클로저는 상수타입!

함수나 클로저를 상수나 변수에 할당할 때마다 사실은 상수나 변수에 함수나 클로저의 참조를 설정하는 것이다.

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

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

-> 두 상수는 같은 클로저를 참조하기 때문에 동일한 클로저가 동작하는 것이 확인된다.

 

 

5️⃣ 탈출 클로저

@escaping 키워드

typealias VoidVoidClosure = () -> Void
let firstClosure: VoidVoidClosure = {
    print("Closure A")
}
let secondClosure: VoidVoidClosure = {
    print("Closure B")
}

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

// 함수에서 반환한 클로저가 함수 외부의 상수에 저장되었다.
let returnedClosure: VoidVoidClosure = returnOneClosure(first: firstClosure, second: secondClosure, shouldReturnFirstClosure: true)

returnedClosure()	// Closure A

var closures: [VoidVoidClosure] = []

// closure 매개변수 클로저는 함수 외부의 변수에 저장될 수 있으므로 탈출 클로저
func appendClosure(closure: @escaping VoidVoidClosure) {
    // 전달인자로 전달받은 클로저가 함수 외부의 변수 내부에 저장되므로 함수를 탈출
    closures.append(closure)
}

 

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

self 키워드

- 탈출하는 클로저에서 값 획득을 하기 위해서는 self 키워드를 이용하여 프로퍼티에 접근해야한다!

typealias VoidVoidClosure = () -> Void

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

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

class SomeClass{
    var x = 10
    
    func runNoescapeClosure(){
        // 비탈출 클로저에서 self 키워드 사용은 선택 사항!
        functionWithNoneescapeClosure {
            x = 200
        }
    }
    
    func runEscapeClosure() -> VoidVoidClosure {
        // 탈출 클로저에서는 명시적으로 self를 사용해야한다.
        return functionWithEscapeClosure {
            self.x = 100
        }
    }
}

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

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

 

withoutActuallyEscaping

비탈출 클로저가 탈출 클로저인 척 해야할 경우 사용함

 

오류가 발생하는 hasElements 함수

배열의 lazy 컬랙션에 있는 filter 메서드의 매개변수로 비탈출 클로저를 전달한다. 그런데, lazy 컬랙션은 비동기 작업을 할 때 사용하기 때문에 filter메서드가 요구하는 클로저는 탈출 클로저이다. 따라서, 탈출 클로저 자리에 비탈출 클로저를 전달할 수 없다는 오류와 마주함!!

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

withoutActuallyEscaping(_:do:) 함수의 활용

import Foundation
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

withoutActuallyEscaping(_:do:)

- 첫번째 전달인자로 탈출 클로저인 척 해야하는 클로저가 전달

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

- 이 함수를 활용하여 비탈출 클로저를 탈출 클로저처럼 사용할 수 있다.

 

 

6️⃣ 자동 클로저

- 전달인자를 갖지 않음

- 클로저가 호출되기 전까지 클로저 내부의 코드가 동작하지 않는다 => 연산을 지연시킬 수 있다!

 

클로저를 이용한 연산 지연

// 대기 중인 손님들
var customerInLine: [String] = ["yejin", "hejin", "qejin"]
print(customerInLine.count) // 3

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

// 실제로 실행한다.
print("Now serving \(customerProvider())!") // "Now serving yejin!"
print(customerInLine.count) // 2

removeFirst() : 자신의 첫요소를 제거하면서 그 요소를 반환해주는 메서드

 


Reference

Swift 스위프트 프로그래밍 - 야곰