Swift - 클로저(Closure)

Jay SJ Baek·2021년 3월 19일
0
post-thumbnail

클로저 (Closures)


  • 클로저는 여러 코드들을 모아둔 괄호 "{}"로 구분된 코드 블럭입니다. 이렇게 구현된 클로저는 일급 객체의 역할을 할 수 있습니다.

    일급객체: 다른 객체들에 일반적으로 적용 가능한 연산을 모두 지원하는 객체로서, 일반적으로 매개변수로 넘기기, 변수 대입하기 같은 연산을 지원할 때 일급 객체라고 합니다.

    클로저는 참조타입으로, 함수는 클로저의 한 형태로 네이밍 되어있는 클로저입니다. 클로저는 Python의 lambda와 비슷하다고 볼 수 있습니다.


표현법


기본 표현법

{ (parameter) -> 반환타입 in
  구현
}

Inline Closure

  • 함수를 정의하여 사용하는 것이 아니라 인자로 들어가 있는 형태의 클로저는 Inline Closure라고 합니다.

    let midtermExamScore = scores.sorted(by: { (score1: Int, score2: Int) -> Bool in return score1 > score2 })

    Inline Closure가 아닌 함수를 정의하기 위해서는 외부에 선언한 후 다시 사용해주어야합니다.

    func sortCriteria(_ score1: Int, _ score2: Int) -> Bool {
      return score1 > score2
    }
    
    let midtermExamScore = scores.sorted(by: sortCriteria(score1, score2))

  • 보통 함수들이 타입 명시를 생략해주어도 되는 것처럼 클로저도 알고있는 타입에 대해서는 타입을 생략해주어도 괜찮습니다.(정확히는 함수가 클로저의 일종이기 때문에 타입 생략이 가능합니다.)

    let midtermExamScore = scores.sorted(by: { (score1, score2) in return score1 > score2 })

    여기에 더해 return(반환 키워드) 역시 생략 가능합니다.

    let midtermExamScore = scores.sorted(by: { (score1, score2) in score1 > score2 })

  • 위의 클로저의 경우 인자에 네이밍을 해서 사용했지만 네이밍 없이 축약을 통해 사용 가능합니다. parameter는 $0부터 시작해서 $1, $2로 1씩 증가하는 순서로 붙여줍니다.

    let midtermExamScore = scores.sorted(by: { $0 > $1 })

    위의 예시의 경우, 클로저의 return이 Bool이고 연산자를 사용할 수 있는 타입의 경우 parameter까지 생략해서 연산자만 남길 수 있습니다.

    let midtermExamScore = scores.sorted(by: > )

    이 경우는 중괄호 {}를 표시해주지 않습니다.


후위 클로저(Trailing Closures)

  • parameter로서 클로저의 길이가 너무 길면 코드를 읽는데 불편할 수 있으므로 함수 뒤에 인자를 붙일 수 있습니다.

    let midtermExamScore = scores.sorted() { $0 > $1 }

    또한 위처럼 함수의 마지막 인자가 클로저인 경우 후위 클로저를 사용할 때 함수의 소괄호 "()"를 생략할 수 있습니다.



값 캡쳐(Capturing Values)

  • 클로저는 클립보드에 저장하듯 상수나 변수의 값을 캡쳐할 수 있습니다. 원본 값이 사라져도 클로저의 body 안에서 그 값을 활용 가능합니다. Swift에서 값을 캡쳐하는 가장 단순한 형태는 중첩함수(nested function)입니다.

    중첩함수(nested function): 함수의 body에서 다른 함수를 다시 호출하는 함수

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

    위의 반환 타입은 일반적인 것과 달라보이지만 결국 입력 parameter는 Int이며, 반환 타입은 (() -> Int)입니다. 즉, 여기서는 다른 예시와 다르게 입력이 클로저가 아니라 반환값이 클로저입니다.


    함수 incrementer()를 보면 runningTotal이나 amount가 선언되지 않았음에도 작동합니다.

    func incrementer() -> Int {
            runningTotal += amount
            return runningTotal
        }

    변수 runningTotal은 makeIncrementer의 지역변수로 함수가 종료되면 사라지는 변수이지만, incrementer 함수를 정의할 때 makeIncrementer 함수 스코프(Scope)에서 존재하고, 이는 캡쳐되어 incrementer 함수에서 사용되므로 문제가 없습니다.


    Note:

    As an optimization, Swift may instead capture and store a copy of a value if that value isn’t mutated by a closure, and if the value isn’t mutated after the closure is created.

    Swift also handles all memory management involved in disposing of variables when they’re no longer needed.

    최적화를 위해 Swift는 어떤 값이 클로저에 의해 더이상 변경되지 않거나 클로저가 생성된 후 그 값이 변경되지 않는 경우 값을 캡쳐하거나 복사본을 저장할 수 있습니다. 또 변수들이 더이상 사용되지 않는 경우 메모리관리를 자동으로 합니다.


    다음은 위에서 선언한 makeIncrementer 함수의 사용 예시입니다.

    let incrementByTen = makeIncrementer(forIncrement: 10)
    
    incrementByTen()
    // returns a value of 10
    incrementByTen()
    // returns a value of 20
    incrementByTen()
    // returns a value of 30
    
    let incrementBySeven = makeIncrementer(forIncrement: 7)
    incrementBySeven()
    // returns a value of 7
    
    incrementByTen()
    // returns a value of 40
    • incrementByTen은 지역변수 runningTotal을 가지며 메소드 incrementer을 반환하고, incrementer은 호출될 때마다 runningTotal을 10씩 증가하며 반환됩니다. 함수는 여러번 실행되지만 변수 runningTotal과 amount가 캡쳐되며 변수를 공유하여 계산이 누적값을 가집니다(변경상태 저장).

    • 하지만 아래에 새로 incrementBySeven이라는 상수를 선언하여 함수를 정의하면, 이때에는 새로운 runningTotal을 가지고, 다른 클로저이기 때문에 연산에 영향이 없습니다.

클로저는 참조 타입입니다(Closures are Reference Types).

  • 위의 예제에서 incrementByTen과 incrementBySeven은 상수 let으로 선언되었습니다. 상수라면 여러번 반복해도 같은 값을 가지고 있어야하지만 명령을 반복할 때마다 누적값을 출력합니다. 하지만 위의 예제에서 runningTotal 변수는 계속 증가할 수 있고, 이는 함수(클로저)는 참조 타입이기 때문입니다. 함수나 클로저를 상수나 변수에 할당할 때, 실제로는 함수가 아닌 함수의 참조(reference)가 할당됩니다. 즉, 한 클로저를 서로 다른 상수나 변수에 할당하면 그 두 상수나 변수는 같은 클로저를 참조하게됩니다.


    let alsoIncrementByTen = incrementByTen
    alsoIncrementByTen()
    // returns a value of 50
    
    incrementByTen()
    // returns a value of 60

    위의 예시의 경우 다른 상수에 클로저를 담았지만 같은 runningTotal을 공유합니다.

reference:

profile
iOS Developer

0개의 댓글