클로저 (Closures)
명명된 함수 생성없이 실행되는 코드 그룹입니다.
클로저 (Closures) 는 코드에서 주변에 전달과 사용할 수 있는 자체 포함된 기능 블럭입니다. Swift의 클로저는 C와 Objective-C에서 블럭과 다른 프로그래밍 언어에서 람다와 유사합니다.
클로저는 정의된 컨텍스트에서 모든 상수와 변수에 대한 참조를 캡처하고 저장할 수 있습니다. 이러한 상수와 변수를 폐쇄 (closing over) 라고 합니다. Swift는 캡처의 모든 메모리 관리를 처리합니다.
- 전역 함수는 이름을 가지고 어떠한 값도 캡처하지 않는 클로저입니다.
- 중첩 함수는 이름을 가지고 둘러싼 함수로 부터 값을 캡처할 수 있는 클로저입니다.
- 클로저 표현식은 주변 컨텍스트에서 값을 캡처할 수 있는 경량 구문으로 작성된 이름이 없는 클로저입니다.
Swift의 클로저 표현식은 일반 시나리오에서 간단하고 깔끔한 구문을 장려하는 최적화를 통해 깔끔하고 명확한 스타일을 가지고 있습니다. 이러한 최적화에는 다음이 포함됩니다:
- 컨텍스트에서 파라미터와 반환값 타입 유추
- 단일 표현식 클로저의 암시적 반환
- 약식 인수 이름
- 후행 클로저 구문
중첩 함수 (Nested Functions) 에서 소개된 중첩 함수는 더 큰 함수에 부분으로 자체 포함된 코드 블럭의 이름을 지정하고 정의하기 편리한 수단입니다. 그러나 완전한 선언과 이름없이 함수와 유사한 구조의 짧은 버전을 작성하는 것이 때때로 유용합니다. 함수를 하나 이상의 인수로 사용하는 함수 또는 메서드로 작업할 때 특히 그렇습니다.
클로저 표현식 (Closure expressions) 은 간단하고 집중적인 구문으로 인라인 클로저로 작성하는 방법입니다. 클로저 표현식은 명확성이나 의도를 잃지 않고 짧은 형태로 클로저를 작성 하기위한 몇가지 구문 최적화를 제공합니다. 아래의 클로저 표현식 예제는 여러 반복에 걸쳐
sorted(by:)
메서드의 단일 예제를 구체화하는 최적화를 나타냅니다. 각 예제는 동일한 기능을 보다 간결한 방식으로 표현합니다.Swift의 표준 라이브러리는 사용자가 제공하는 정렬 클로저의 출력을 기반으로 알려진 타입의 값 배열을 정렬하는
sorted(by:)
라는 메서드를 제공합니다. 정렬 프로세스가 완료되면 sorted(by:)
메서드는 원본 배열과 같은 타입과 같은 크기의 올바르게 정렬된 요소의 새로운 배열로 반환합니다. 기존 배열은 sorted(by:)
메서드로 수정되지 않습니다.아래 예제의 클로저 표현식은 알파벳 역순으로
String
값의 배열을 정렬하기 위해 sorted(by:)
메서드를 사용합니다. 다음은 정렬하기 위한 초기화 배열입니다:let names = ["Chris", "Alex", "Ewa", "Barry", "Daniella"]
sorted(by:)
메서드는 배열 내용과 동일한 타입의 두 인수를 사용하는 클로저를 허용하고 값이 정렬된 후 첫번째 값이 두번째 값의 앞 또는 뒤에 표시되어야 하는지 여부를 나타내는 Bool
값을 반환합니다. 정렬 클로저는 첫번째 값이 두번째 값 앞에 나타나야 하는 경우 true
를 반환하고 그렇지 않으면 false
를 반환해야 합니다.이 예제는
String
값의 배열을 정렬하고 정렬 클로저는 (String, String) -> Bool
타입의 함수를 필요로 합니다.정렬 클로저를 제공하는 한가지 방법은 올바른 타입의 일반 함수를 작성하고
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"]
첫번째 문자열 (
s1
)이 두번째 문자열 (s2
)보다 크다면 backward(_:_:)
함수는 true
를 반환하고 이것은 정렬된 배열에 s1
은 s2
전에 나타납니다. 문자열의 문자가 "더 크다"는 "알파벳 순으로 더 뒤에 나타난다"는 의미입니다. 이것은 문자 B
는 문자 A
보다 "더 크다"이고 문자열 "Tom"
은 문자열 "Tim"
보다 더 큽니다. 알파벳 역순으로 정렬될 때 "Barry"
는 "Alex"
보다 앞에 위치합니다.그러나 이것은 본질적으로 단일 표현식 함수 (
a > b
)를 작성하는 다소 긴 방식입니다. 이 예제에서는 클로저 표현식 구문을 사용하여 정렬 클로저를 인라인으로 작성하는 것이 좋습니다.클로저 표현구는 아래와 같이 일반적인 형태를 가지고 있습니다:
{ (<#parameters#>) -> <#return type#> in
<#statements#>
}
클로저 표현구의 파라미터 는 in-out 파라미터 일 수 있지만 기본값을 가질 수 없습니다. 가변 파라미터의 이름을 지정하면 가변 파라미터를 사용할 수 있습니다. 튜플은 파라미터 타입과 반환 타입으로 사용될 수도 있습니다.
아래 예제는 위에서
backward(_:_:)
함수의 클로저 표현 버전입니다:reversedNames = names.sorted(by: { (s1: String, s2: String) -> Bool in
return s1 > s2
})
이 인라인 클로저를 위한 파라미터와 반환 타입의 선언은
backward(_:_:)
함수에서 선언한 것과 동일합니다. 두 경우 모두 (s1: String, s2: String) -> Bool
로 작성합니다. 그러나 인라인 클로저 표현식을 위한 파라미터와 반환 타입은 중괄호 바깥이 아닌 안에 작성합니다.클로저의 본문의 시작은
in
키워드로 시작합니다. 이 키워드는 클로저의 파라미터와 리턴 타입 정의가 끝남을 나타내며 클로저의 본문이 시작함을 나타냅니다.클로저의 본문이 너무 짧기 때문에 한줄로 작성할 수 있습니다:
reversedNames = names.sorted(by: { (s1: String, s2: String) -> Bool in return s1 > s2 } )
이것은
sorted(by:)
메서드에 대한 전체 호출이 동일하게 유지되었음을 보여줍니다. 소괄호는 여전히 메서드의 전체 인수를 둘러싸고 있습니다. 그러나 인수는 이제 인라인 클로저입니다.정렬 클로저는 메서드에 인수로 전달되기 때문에 Swift는 파라미터 타입과 반환되는 값의 타입을 유추할 수 있습니다.
sorted(by:)
메서드는 문자열 배열에서 호출되므로 인수는 (String, String) -> Bool
타입의 함수이어야 합니다. 이는 (String, String)
과 Bool
타입을 클로저 표현식 정의에 일부러 작성할 필요가 없음을 의미합니다. 모든 타입은 유추할 수 있기 때문에 반환 화살표 (->
)와 파라미터의 이름을 둘러싼 소괄호를 생략할 수 있습니다:reversedNames = names.sorted(by: { s1, s2 in return s1 > s2 } )
함수나 메서드에 클로저를 인라인 클로저 표현식으로 전달할 때 항상 파라미터 타입과 반환 타입을 유추할 수 있습니다. 결과적으로 클로저가 함수 또는 메서드 인수로 사용될 때 완전한 형태로 인라인 클로저를 작성할 필요가 없습니다.
그럼에도 불구하고 원하는 경우 타입을 명시적으로 만들 수 있으며 코드를 읽는 자가 모호성을 피할 수 있다면 그렇게 하는 것이 좋습니다.
sorted(by:)
메서드의 경우 정렬이 발생한다는 사실에서 클로저의 목적이 명확하며 문자열 배열의 정렬을 지원하기 때문에 코드를 읽는 사람이 클로저가 String
값으로 작동할 가능성이 있다고 가정하는 것이 안전합니다.단일 표현 클로저 (Single-expression closures)는 이전 예제에서
return
키워드를 생략하여 단일 표현식으로 암시적으로 값을 반환할 수 있습니다:reversedNames = names.sorted(by: { s1, s2 in s1 > s2 } )
여기서
sorted(by:)
메서드의 인수의 함수 타입은 클로저에서 Bool
값이 반환되어야 하기 때문에 명확합니다. 클로저의 본문에 Bool
값을 반환하는 단일 표현식 (s1 > s2
)가 포함되므로 모호하지 않고 return
키워드를 생략할 수 있습니다.Swift는 인라인 클로저에
$0
, $1
, $2
등 클로저의 인수값으로 참조하는데 사용할 수 있는 자동적으로 짧은 인수 이름 (shorthand argument names)을 제공합니다.클로저 표현식에 이런 짧은 인수 이름을 사용한다면 선언에 클로저의 인수 리스트을 생략할 수 있고 짧은 인수 이름의 수와 타입은 함수 타입에서 유추됩니다. 클로저 표현식이 본문으로 전체가 구성되기 때문에
in
키워드를 생략할 수도 있습니다:reversedNames = names.sorted(by: { $0 > $1 } )
여기서
$0
와 $1
은 클로저의 첫번째와 두번째 String
인수를 참조합니다. $1
이 짧은 인수에서 가장 높은 숫자이므로 클로저는 2개의 인수가 있다고 이해합니다. 여기서 sorted(by:)
함수는 인수가 모두 문자열인 클로저로 기대하므로 짧은 인수 $0
과 $1
은 모두 타입 String
입니다.실제로 위의 클로저 표현식을 더 짧게 작성하는 방법이 있습니다. Swift의
String
타입은 보다 큰 연산자 (>
)의 문자열 별 구현을 String
타입의 파라미터 2개가 있는 메서드로 정의하고 Bool
타입의 값을 반환합니다. 이것은 sorted(by:)
메서드에 필요한 메서드 타입과 정확하게 일치합니다. 따라서 간단하게 보다 큰 연산자를 전달할 수 있고 Swift는 문자열 특정 구현을 사용하기 원한다고 유추합니다:reversedNames = names.sorted(by: >)
함수의 마지막 인수로 함수에 클로저 표현식을 전달해야하고 클로저 표현식이 긴 경우 후행 클로저 (trailing closure) 로 작성하는 것이 유용할 수 있습니다. 후행 클로저는 함수의 인수이지만 함수 호출의 소괄호 다음에 작성합니다. 후행 클로저 구문을 사용할 때 함수 호출의 일부로 첫번째 클로저 인수 라벨을 작성하지 않아도 됩니다. 함수 호출은 여러개의 후행 클로저를 포함할 수 있지만 아래 몇가지 예제에서는 단일 후행 클로저를 사용합니다.
func someFunctionThatTakesAClosure(closure: () -> Void) {
// function body goes here
}
// Here's how you call this function without using a trailing closure:
someFunctionThatTakesAClosure(closure: {
// closure's body goes here
})
// Here's how you call this function with a trailing closure instead:
someFunctionThatTakesAClosure() {
// trailing closure's body goes here
}
reversedNames = names.sorted() { $0 > $1 }
후행 클로저로 표현식이 함수와 메서드의 유일한 인수일 경우 함수를 호출할 때 함수 또는 메서드 이름 뒤에 소괄호
()
를 작성하지 않아도 됩니다:reversedNames = names.sorted { $0 > $1 }
후행 클로저는 클로저가 길어서 한줄로 인라인으로 작성이 불가능할 때 유용합니다. 예를 들어 Swift의
Array
타입은 단일 인수로 클로저 표현식을 가지는 map(_:)
메서드가 있습니다. 이 클로저는 배열의 각 아이템에 대해 한번 호출되고 아이템에 대해 매핑된 대체값 (다른 타입일 수 있음)이 반환됩니다. map(_:)
에 전달한 클로저에 작성된 코드에 따라 매핑 특성과 반환된 값의 타입을 지정합니다.제공된 클로저에 각 배열의 요소를 적용한 후에
map(_:)
메서드는 기존 배열에 해당값과 같은 순서로 새로 매핑된 값의 새로운 배열을 반환합니다.다음은
Int
값의 배열을 String
값의 배열로 변환하기 위해 후행 클로저와 map(_:)
메서드를 어떻게 사용하는지 나타냅니다. 배열 [16, 58, 510]
은 새로운 배열 ["OneSix", "FiveEight", "FiveOneZero"]
을 생성하는데 사용됩니다:let digitNames = [
0: "Zero", 1: "One", 2: "Two", 3: "Three", 4: "Four",
5: "Five", 6: "Six", 7: "Seven", 8: "Eight", 9: "Nine"
]
let numbers = [16, 58, 510]
위 코드는 정수와 그 정수에 맞는 영어 표기를 매핑하는 딕셔너리를 생성합니다. 문자열로 변환하기 위한 정수의 배열도 정의합니다.
numbers
배열을 사용하여 후행 클로저로 map(_:)
메서드로 클로저 표현식을 전달하여 String
값의 배열을 생성할 수 있습니다:let strings = numbers.map { (number) -> String in
var number = number
var output = ""
repeat {
output = digitNames[number % 10]! + output
number /= 10
} while number > 0
return output
}
// strings is inferred to be of type [String]
// its value is ["OneSix", "FiveEight", "FiveOneZero"]
map(_:)
메서드는 배열에 각 아이템을 위해 클로저 표현 식을 호출합니다. 매핑할 배열의 값에서 유추할 수 있으므로 클로저의 입력 파라미터 인 number
타입을 지정할 필요가 없습니다.이 예에서 변수
number
는 클로저의 number
파라미터의 값으로 초기화되기 때문에 값은 클로저 본문 내에서 수정될 수 있습니다 (함수와 클로저의 파 라미터는 항상 상수입니다). 클로저 표현식은 출력 매핑된 출력 배열에 저장될 타입을 나타내기 위해 String
타입도 반환 타입으로 지정합니다.클로저 표현식은 호출될 때마다
output
이라는 문자열을 만듭니다. 나머지 연산자 (number % 10
)를 이용하여 number
의 마지막 숫자를 계산하고 digitNames
딕셔너리에 적절한 숫자 문자열을 찾습니다. 클로저는 0보다 큰 정수에 대한 문자열 표현을 생성하는데 사용할 수 있습니다.Note 딕셔너리 서브 스크립트는 키가 존재하지 않는경우에 값을 찾는 것을 실패하기 위해 옵셔널 값을 반환합니다. 그래서digitNames
딕셔너리의 서브 스크립트를 호출할 때는 느낌표를 붙여 줍니다 (!
). 위의 예제에서digitNames
딕셔너리에number % 10
은 항상 유효한 서브 스크립트 키를 보장하므로 느낌표는 서브 스크립트의 옵셔널 반환 값에 저장된String
값을 강제로 언래핑 하는데 사용됩니다.
digitNames
딕셔너리에서 반환된 문자열이 output
앞에 추가되어 숫자의 문자열 버전을 역순으로 효과적으로 빌드합니다 (표현식 number % 10
은 16
의 경우 6
, 58
의 경우 8
, 510
의 경우 0
을 제공합니다).