Swift study - Closure

rbw·2022년 2월 10일
0

swift-study

목록 보기
2/17
post-thumbnail

클로저(Closure)

클로저의 정의

클로저는 코드에서 주변으로의 전달과 사용을 할 수 있는 기능 블럭이다 클로저는 정의된 컨텍스트에서 모든 상수와 변수에 대한 참조를 캡처하고 저장할 수 있다. 이러한 상수와 변수를 폐쇄(closing over)라고 한다 Swift는 캡처의 모든 메모리 관리를 처리함

클로저의 형태

  • 전역 함수는 이름을 가지고 어떠한 값도 캡처하지 않는 클로저
  • 중첩 함수는 이름을 가지고 둘러싼 함수로 부터 값을 캡처할 수 있는 클로저
  • 클로저 표현식은 주변 컨텍스트에서 값을 캡처할 수 있는 경량 구문으로 작성된 이름 없는 클로저

정렬 메서드(The Sorted Method)

let names = ["Chris", "Alex", "Ewa", "Barry", "Daniella"]

// sorted(by:) 메서드
func backward(_ s1: String, _ s2: String) -> Bool {
    return s1 > s2
}
var reversedNames = names.sorted(by: backward)
// reversedNames is equal to ["Ewa", "Daniella", "Chris", "Barry", "Alex"]

정렬 클로저를 제공하는 방법 중 하나는 일반 함수를 작성하고 sorted(by:) 메서드에 인자로 전달하는 것

위 방식은 단일 표현식 함수(a > b)를 작성하는 방법 보다 다소 길다, 따라서 클로저 표현식 구문을 사용하여 정렬 클로저를 인라인으로 작성하는 것이 좋다

클로저 표현구 (Closure Expression Syntax)

// 이런 형태를 가짐
{ (parameters) -> return type in 
  statements
}

// 위 함수의 클로저 표현 방식
reversedNames = names.sorted(by: { (s1: String, s2: String) -> Bool in
    return s1 > s2
})

TS의 화살표 함수랑 비슷한 방식이라고 생각이 들었다

클로저 표현구의 파라미터는 in-out 파라미터 일 수 있지만 기본값을 가지지 못함

인라인 클로저 표현식을 위한 파라미터와 반환 타입은 중괄호 안에 작성한다

클로저 바디의 시작은 in키워드로 시작한다 클로저의 파라미터와 리턴 타입 정의가 끝남을 나타내며 클로저의 바디가 시작함을 나타낸다(위 코드는 바디가 짧기 때문에 한 줄로 작성 가능)

in-out 파라미터

함수가 호출이 종료된 후에도 변경 사항을 유지해야할 경우 사용하는 파라미터

알아야 할 부분은 Swift에서 함수의 파라미터는 기본적으로 상수이다. 따라서 전달 받은 파라미터를 함수 내부에서 변경을 시도하면 컴파일 에러가 발생한다

func addOne(value: Int){
  value += 1 // error, value is let
  print(value)
}
// 해결하려면 inout 키워드 사용

var number = 10
func addOne(value: inout Int){
  value += 1 
}
addOne(value: &number) // & 기호 사용해야함 !
// c언어의 포인터와 유사한듯

in-out 파라미터는 다음과 같이 작동한다

  1. 함수가 호출될 때 인수의 값은 복사가 된다
  2. 함수의 바디 내에서 복사본은 수정이 된다
  3. 함수가 반환될 때 복사본의 값은 기존 인수에 할당이 된다

궁금한 점은 위 코드에서 let number로 선언을 하면 error가 뜬다는 점이다 내 생각으로 함수의 인수는 let이기 때문에 let으로 선언한 변수를 인자로 넘겨주어도 작동을 똑같이 할거라고 생각을 하였는데 그렇지 않았다. 왜 그런지 검색을 해보아도 잘 나오지 않았는데, 추측으로는 인수를 복사 할 때 전달받은 변수를 복사를 하기 때문에 수정을 가능하게끔 만들어 주지 않나 라고 생각한다 그래서 let으로 선언한 변수는 복사를 하여도 수정이 불가하므로 작동이 안되는 건가 라고 생각하는데 아직 잘 모르겠어서 나중에 더 알아 봐야 겠다

02/12) 동작과정을 다시 살펴보았더니 그 때는 못본 부분이 눈에 들어왔다.. 인수의 값이 복사가되고 수정이 되어 기존 인수에 할당이 된다라는 동작과정이라 let으로 선언을하면 에러가 뜨는건 당연한 문제였다.. 다음엔 좀 자세히 읽어봐야겠다

컨텍스트로 타입 유추

sorted(by:) 메서드는 문자열 배열에서 호출되므로 인자는 유추가 가능하다 따라서 클로저 표현식 정의에 타입을 작성할 필요가 없음을 의미한다

reversedNames = names.sorted(by: { s1, s2 in return s1 > s2 } )

클로저를 인라인 클로저 표현식으로 전달할 때 항상 파라미터 타입과 반환 타입을 유추가 가능하나, 타입을 명시적으로 표현을 하여 가독성을 늘리고, 모호성을 피할 수 있다면 그러는 편이 낫다

단일 표현 클로저의 암시적 반환 (Implicit Returns from Single-Expression Closures)

위 코드에서 return 키워드를 생략하여 단일 표현식으로 암시적으로 값을 반환하는 것이 가능하다

reversedNames = names.sorted(by: { s1, s2 in s1 > s2 } )

Bool 값을 반환하는 단일 표현식이 포함되므로 모호하지 않고 생략이 가능하다

짧은 인자 이름 (Shorthand Argument Names)

인라인 클로저에 $0, $1, $2 등 클로저의 인자 값으로 참조하는데 사용하는 짧은 인자 이름을 제공한다

reversedNames = names.sorted(by: { $0 > $1 } )

클로저 표현식이 전체 바디로 구성되기 때문에 in 키워드를 생략이 가능

연산자 메서드 (Operator Methods)

더 축약하는 방법으로 Swift의 String 타입은 보다 큰 연산자의 문자열 별 구현을 String 타입의 파라미터 2개 있는 메서드로 정의하고 Bool 타입을 반환한다

reversedNames = names.sorted(by: >)

후행 클로저 (Trailing Closure)

함수의 마지막 인자로 클로저 표현식을 전달해야 하고 그 표현식이 긴 경우 후행 클로저 로 작성하는 것이 유용할 수 있다, 후행 클로저는 함수의 인자이지만, 호출부인 소괄호 다음에 작성을 한다. 함수 호출은 여러개의 후행 클로저도 포함 가능하다

func someFunctionThatTakesAClosure(closure: () -> Void) {
    // function body goes here
}
// 후행 클로저가 아닌 방식
someFunctionThatTakesAClosure(closure: {
    // closure's body goes here
})

// 후행 클로저 방식 
someFunctionThatTakesAClosure() {
    // trailing closure's body goes here
}

// 위 정렬 메서드로 후행클로저 사용 예
reversedNames = names.sorted() { $0 > $1 }

// 만약 후행 클로저가 유일한 인자인 경우 소괄호 생략 가능 
reversedNames = names.sorted { $0 > $1 }

함수가 여러개의 클로저를 가지고 있다면 첫 후행 클로저의 인자 레벨은 생략하고 남은 후행 클로저의 라벨은 표기함

func loadPicture(from server: Server, completion: (Picture) -> Void, onFailure: () -> Void) {
    if let picture = download("photo.jpg", from: server) {
        completion(picture)
    } else {
        onFailure()
    }
}

loadPicture(from: someServer) { picture in
    someView.currentPicture = picture
} onFailure: {
    print("Couldn't download the next picture.")
}
// 중괄호를 기준으로 클로저를 표현함

loadPicture(from:completion:onFailure:) 함수는 네트워크 작업을 백그라운드로 전달하고 완료 시 두 완료 처리기 중 하나를 호출한다. 두 상황을 모두 처리하는 하나의 클로저를 사용하는 대신 성공시 코드와 오류 처리 코드를 명확하게 분리가 가능하다


캡처값 (Capturing Values)

클로저는 정의된 둘러싸인 컨텍스트에서 상수와 변수를 캡처 할 수 있다. 그러면 상수와 변수를 정의한 원래 범위가 더이상 존재하지 않더라고 바디 내에서 해당 상수와 변수 값을 참조, 수정이 가능하다

값을 캡처하는 가장 간단한 클로저 형태는 다른 함수 바디 내에 작성하는 중첩 함수이다

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

incrementer()runningTotal, amount를 캡처한다 캡처 후 호출될 때마다 amountrunningTotal을 증가시키는 클로저로 makeIncrementer에 의해 반환된다.

makeIncrementer 의 반환 타입은 () -> Int 이고, 이것은 단순한 값이 아닌 함수를 반환한다는 의미이다.

incrementer() 함수는 둘러싸인 함수에 runningTotalamount 에 대한 참조(reference)를 캡처하고 함수 내에서 사용한다

let incrementByTen = makeIncrementer(forIncrement: 10)
incrementByTen() // 10
incrementByTen() // 20
incrementByTen() // 30

let incrementByseven = makeIncrementer(forIncrement: 7)
incrementBySeven() // 7
incrementByTen() // 40 , 위 함수의 영향을 받지 않음 

incrementByTen이 값을 캡처 하였기 때문에 계속 증가한 값을 리턴한다


클로저는 참조 타입 (Closures Are Reference Types)

함수 또는 클로저를 상수 또는 변수에 할당할 때마다 실제로 해당 상수 또는 변수를 함수 또는 클로저에 대한 참조로 설정한다 위의 예에서 IncrementByTen은 클로저 자체의 내용이 아니라 상수를 가리키는 클로저 선택이다

let alsoIncrementByTen = incrementByTen
alsoIncrementByTen() // 50, 참조하고 있기 때문이다

이스케이프 클로저 (Escaping Closures)

함수의 인자로 전달된 클로저가 함수가 반환된 후 실행 되는 클로저

전달인자로 받은 클로저가 함수 종료 후 호출될 경우 클로저가 함수를 탈출한다 로 표현한다.(클로저를 외부로 보낸다는 의미도 된다) 클로저를 파라미터로 갖는 함수를 선언할 때 이 클로저는 탈출을 허락한다는 의미로 @escaping 을 작성 가능

클로저가 탈출할 수 있는 한가지 방법은 함수 바깥에 정의된 변수에 저장되는 것이다

var completionHandlers: [() -> Void] = []
func someFunctionWithEscapingClosure(completionHandler: @escaping () -> Void) {
    completionHandlers.append(completionHandler)
}

someFunctionWithEscapingClosure(_:) 함수는 인자로 클로저를 가지고 있고 함수 바깥에 선언된 배열에 추가합니다. 함수의 파라미터에 @escaping 을 표시하지 않으면 컴파일 시 에러가 발생합니다.

일반적으로 클로저는 내부에서 변수를 사용하여 암시적으로 변수를 캡처하지만 self를 참조하는 경우 명시적이어야 한다. 명시적으로 self를 작성하거나 클로저 캡처 목록에 self 를 포함한다. 명시적 작성으로 의도를 표현하고 참조 사이클이 없음을 상기시켜 준다.

다음은 클로저의 캡처 리스트에 포함하여 self를 캡처하고 암시적으로 self를 참조 한다

class SomeOtherClass {
    var x = 10
    func doSomething() {
        someFunctionWithEscapingClosure { [self] in x = 100 }
        someFunctionWithNonescapingClosure { x = 200 }
    }
}

self가 구조체 또는 열거형 인스턴스이면 항상 암시적으로 self를 참조 가능하다. 그러나 이스케이프 클로저라면 self에 대한 변경 가능한 참조를 캡처 불가하다. 구조체와 열거형은 공유 변경을 허용하지 않음

이스케이프 클로저는 구조체인 self를 변경 가능한 참조로 캡처할 수 없다

struct SomeStruct {
    var x = 10
    mutating func doSomething() {
        someFunctionWithNonescapingClosure { x = 200 }  // Ok
        someFunctionWithEscapingClosure { x = 100 }     // Error
    }
}

자동 클로저 (Autoclosures)

자동클로저는 함수에 인자로 전달되는 표현식을 래핑하기 위해 자동으로 생성되는 클로저이다. 인자를 가지지 않으며 호출될 때 내부에 래핑된 표현식 값을 반환한다. 명시적 클로저 대신 일반 표현식을 작성하여 함수 파라미터 주위의 중괄호 생략 가능

클로저가 호출될 때까지 코드 내부 실행이 되지 않기 때문에 자동 클로저는 판단을 지연시킬 수 있다. 판단 지연은 코드 판단 시기를 제어 할 수 있기 때문에 사이드 이펙트가 있거나 계산이 오래 걸리는 코드에 유용하다

var customersInLine = ["Chris", "Alex", "Ewa", "Barry", "Daniella"]
print(customersInLine.count)
// Prints "5"

// 타입은 파라미터가 없고, 문자열을 반환하는 () -> String 이다
let customerProvider = { customersInLine.remove(at: 0) }
print(customersInLine.count)
// Prints "5"

print("Now serving \(customerProvider())!")
// Prints "Now serving Chris!"
// 클로저가 실제 호출이 되었기 때문에 내부의 표현식이 실행된다
print(customersInLine.count)
// Prints "4"
// 함수의 소비자 이름을 반환하는 명시적 클로저를 가진 serve
func serve(customer customerProvider: () -> String) {
    print("Now serving \(customerProvider())!")
}
serve(customer: { customersInLine.remove(at: 0) } )

// 같은 동작을 수행하지만 @autoclosure 속성을 표기하여 자동 클로저를 가짐
// 인자는 자동으로 클로저로 변환이 된다
func serve(customer customerProvider: @autoclosure () -> String) {
    print("Now serving \(customerProvider())!")
}
serve(customer: customersInLine.remove(at: 0))

자동 클로저의 남용은 코드의 가독성이 떨어질 수 있다

profile
hi there 👋

0개의 댓글