소피it블로그
[Swift 공식문서] Closures 정리 (1) 본문
https://docs.swift.org/swift-book/LanguageGuide/Closures.html
스위프트의 클로저는 다른 프로그래밍 언어의 lambda와 비슷하다.
함수에서 다룬 전역 함수와 중첩 함수는 클로저의 특별한 타입이다. 클로저는 다음 세 가지 중 하나의 형태를 띤다:
- 전역 함수는 이름이 있고 아무 값도 capture하지 않는 클로저이다.
- 중첩 함수는 이름이 있고 감싸는 함수에서 값을 capture할 수 있는 클로저이다.
- 클로저 표현은 주변 맥락으로부터 값을 capture할 수 있는 구문 안에 사용되는 이름 없는 클로저이다.
1. 클로저 표현
중첩 함수는 더 큰 함수의 일부로써 자립적인 코드 블럭을 정의하고 이름 짓는 편리한 수단이다. 그렇지만 때로는 함수와 같은 구조를 완전한 선언과 이름 없이 짧은 형태로 쓰는 게 더 편리할 때가 있다. 특히 함수를 인수로 받는 함수나 메서드를 가지고 작업할 때 더 그렇다.
클로저 표현은 클로저를 라인 안에 간단하게 적어줄 수 있는 방법이다.
(1) The Sorted Method
스위프트의 표준 라이브러리는 sorted(by:)라는 메서드를 제공한다. 이는 한 타입의 배열을 코더가 제공하는 소팅 클로저의 결과에 기반해 분류해주는 메서드이다. 소팅 작업이 끝나면 sorted(by:) 메서드는 원래의 배열과 타입과 크기가 같은, 그러나 순서가 정렬된 새로운 배열을 반환한다. 원본 배열은 sorted(by:) 메서드로 인해 변경되지 않는다.
아래의 예시에서는 문자열의 배열을 알파벳 역순으로 정렬하기 위해 sorted(by:) 메서드를 사용한다. sorted(by:) 메서드는 배열의 요소들과 같은 타입인 두 인수를 받는 클로저를 받고 첫 번째 값이 두 번째 값의 앞이나 뒤 중 어디에 와야 하는지에 관한 Bool 값을 리턴한다. 첫 번째 값이 두 번째 값의 앞에 나와야 하면 true를, 그렇지 않으면 false를 리턴한다.
문자열의 배열을 정렬하는 이 예시에서는 소팅 클로저가 (String, String) -> Bool 타입의 함수여야 한다. 소팅 클로저를 제공하는 한 방법은 올바른 타입의 일반적인 함수를 작성해서 sorted(by:) 메서드의 인수로 넘기는 것이다.
let names = ["Chris", "Alex", "Ewa", "Barry", "Daniella"]
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를 리턴한다. 문자열 내의 문자들에 대해서는 "더 크다"는 말이 "알파벳 순서상 더 뒤에 온다"는 말과 같다. 예를 들면 B는 A보다 크다.
그렇지만 위의 코드는 실제 본질적으로 필요한 게 단 한 줄짜리 표현인 것에 비해 길게 돌아가는 방식이다. 따라서 소팅 클로저를 클로저 표현 구문을 사용하여 라인 내에 써주는 것이 나을 것이다.
(2) 클로저 표현 구문
클로저 표현 구문은 주로 다음과 같은 일반적인 형태를 띈다.
{ (parameters) -> return type in
statements
}
클로저 표현 구문의 매개변수(parameters)는 인아웃 매개변수가 될 수는 있지만 디폴트 값은 가질 수 없다. variadic parameter 또한 사용할 수 있다. 튜플 역시 매개변수 타입과 리턴 타입으로 사용될 수 있다. 이하는 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:) 메서드를 부르는 것은 바뀌지 않았다. 메서드의 인수를 여전히 한 쌍의 괄호가 감싸고 있다. 그렇지만 이제는 인수가 인라인 클로저이다.
(3) 맥락에서 타입 추론하기
소팅 클로저가 메서드의 인수로 전달되기 때문에 스위프트는 매개변수와 리턴 값의 타입을 추론할 수 있다. sorted(by:) 메서드는 문자열의 배열에 대해 호출되기 때문에 반드시 (String, String) -> Bool 타입이 된다. 즉 (String, String)과 Bool 타입은 클로저 표현을 정의할 때 굳이 써줄 필요가 없다. 타입이 모두 추론될 수 있기 때문에 리턴 화살표(->)와 매개변수 명을 감싸는 괄호는 생략이 가능하다.
reversedNames = names.sorted(by: { s1, s2 in return s1 > s2 } )
클로저를 함수나 메서드에 인라인 클로저 표현으로써 넘겨주면 항상 매개변수 타입과 리턴 타입을 추론할 수 있다. 결국 클로저가 함수나 메서드의 인수로 사용되는 경우에는 인라인 클로저를 완전한 형태로 써줄 필요가 없다. 물론 원한다면 타입을 명시해줄 수 있고, 그렇게 한다면 코드를 읽는 이들이 모호함을 느끼지 않을 것이다.
(4) Implicit Returns from Single-Expression Closures
single-expression 클로저는 선언시 return 키워드를 생략함으로써 결과를 암시적으로 리턴할 수 있다.
reversedNames = names.sorted(by: { s1, s2 in s1 > s2 } )
(5) Shorthand Argument Names
스위프트는 인라인 클로저에 자동적으로 인수 약칭을 부여한다. $0, $1, $2 등으로 이어지는 이름을 통해 클로저의 인수의 값을 나타낸다.
이러한 인수명 약칭을 클로저 표현 안에 사용한다면 정의할 때 클로저의 인수 리스트를 생략할 수 있다. 인수명의 약칭의 타입은 함수의 타입에서 추론할 수 있고, 약칭의 숫자 중 가장 큰 것을 통해 클로저가 받는 인수의 개수가 결정된다. in 키워드 또한 생략될 수 있다.
reversedNames = names.sorted(by: { $0 > $1 } )
$1이 약칭중 가장 큰 숫자이기 때문에 해당 클로저는 2개의 인수를 받는다고 이해하면 된다. 또한 sorted(by:) 함수가 여기서는 인수가 둘 다 문자열인 클로저를 받기 때문에 $0과 $1 둘 다 문자열 타입이라는 것을 알 수 있다.
(6) Operator Methods
위의 클로저 표현을 더 짧게 쓰는 방법도 있다. 스위프트의 문자열 타입은 > 연산자를 문자열에서만 쓸 수 있는 방식을 정의하는데, 이는 문자열 타입 두 개의 매개변수를 갖고 Bool을 리턴한다. 즉 이는 sorted(by:) 메서드가 필요로 하는 타입과 일치하기 때문에, 단순히 > 연산자만 전달해도 스위프트는 해당 연산자를 string-specific하게 구현하는 것으로 추론할 것이다.
reversedNames = names.sorted(by: >)
2. Trailing Closures
함수의 마지막 인수로 클로저 표현을 넘겨야 하는데 해당 클로저 표현이 길다면, 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
}
위에서 작성한 문자열 소팅 클로저는 트레일링 클로저로써 sorted(by:) 메서드의 바깥에 적어줄 수 있다.
reversedNames = names.sorted() { $0 > $1 }
만일 클로저 표현이 함수나 메서드의 유일한 인수로 주어지고, 그것을 트레일링 클로저로 제공한다면 함수를 호출할 때 함수나 메서드의 이름 뒤에 괄호를 써줄 필요가 없다.
reversedNames = names.sorted { $0 > $1 }
트레일링 클로저는 클로저가 한 줄에 쓰기에는 너무 긴 경우에 유용하다. 일례로 스위프트의 배열 타입은 map(_:) 메서드를 갖는데, 이는 유일한 인수로 클로저 표현을 받는다. 배열의 모든 아이템에 대해 클로저가 각기 한번씩 호출되고 해당 아이템에 대해 매핑된 값을 리턴한다. 매핑의 성질과 리턴값의 타입은 맵 메서드에 넘기는 클로저에 코드를 적어줌으로써 구체화할 수 있다.
클로저를 각 배열 요소에 적용한 후에 map(_:) 메서드는 매핑된 값을 포함하는 새로운 배열을 리턴한다. 이는 기존의 배열에서 대응하는 요소와 같은 순서이다.
이하는 정수의 배열을 문자열의 배열로 변형시키는 트레일링 클로저를 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]
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의 타입을 명시해줄 필요가 없다. 매핑되는 어레이 안의 값들을 통해 타입이 추론될 수 있기 때문이다.
(코드 설명 생략)
함수가 여러 개의 클로저를 취한다면, 첫 번째 트레일링 클로저의 인수 라벨을 생략하고 나머지 트레일링 클로저에는 라벨을 붙인다.
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:) 함수는 네트워크 태스크를 백그라운드로 분배하고 네트워크 태스크가 끝나면 둘 중 하나의 완성 핸들러를 부른다. 이런 식으로 함수를 작성하는 것은 성공적인 경우와 실패하는 경우를 깔끔하게 분리할 수 있게 해준다.
'개발_iOS > 스위프트' 카테고리의 다른 글
[Swift 공식문서] Structures and Classes 정리 (1) | 2022.05.24 |
---|---|
[Swift 공식문서] Closures 정리 (2) (0) | 2022.05.23 |
[Swift 공식문서] Functions 정리 (0) | 2022.05.15 |
[Swift 공식문서] The Basics 정리 (9) - Assertions and Preconditions (0) | 2022.05.14 |
[Swift 공식문서] The Basics 정리 (8) - Error Handling (0) | 2022.05.14 |