수업 때 가장 어려웠던 클로저!
코드 안에서 사용되고 전달될 수 있는 코드 블럭이다.
클로저는 어떤 변수나 상수의 값을 캡쳐해 저장할 수 있다고 하는데, 무슨 말인지 잘 이해가 안된다.
밑으로 더 내려가보자.
클로저는 3가지 형태를 가질 수 있는데,
Swift에서는 이 클로저가 코드를 clean, clear style, brief, clutter-free syntax로 만들어 준다고 한다.
여러 가지 장점들이 있는데,
중첩 함수는 그보다 상위 함수의 일부로 독립적인 코드 블록을 명명하고 정의하는 편리한 수단이다.
그러나 그보다 짧게 쓰는 함수가 때로는 더 효과적이고 필요할 때가 있음. 이때 클로저를 사용하면 좋다.
Closure Expression 은 클로저를 의도와 명확성을 떨어트리지 않고 더 짧게 쓸 수 있는 방법들을 제공한다.
클로저를 기본적으로 선언하는 방법은 다음과 같음.
클로저 안의 매개변수는 in-out 변수가 될 수 있지만, 초기값은 가질 수가 없다.
배열을 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"]
이걸 클로저를 활용해 간단하게 만들어주면 다음과 같이 쓸 수 있다.
reversedNames = names.sorted(by: { (s1: String, s2: String) -> Bool in
return s1 > s2
})
sorted 안의 by에 클로저를 전달함으로써 names 배열을 오름차순 정렬한다.
지금 만들어둔 클로저가 메소드의 인자로 들어가기 때문에 Swift는 이 클로저가 가지는 매개변수와 반환하는 값의 타입을 추론할 수가 있다.
위에서 sorted(by:) 의 인자로 클로저를 전달했는데, 이 메소드는 (String, String) -> Bool 타입의 인자를 받아야 하는 것을 알고 있다.
따라서 이 클로저에서 타입을 생략할 수가 있다.
타입을 다 생략할 수가 있기 때문에, 매개변수를 감싸는 괄호와 반환값을 표현하는 화살표( -> )도 마찬가지로 생략할 수 있다.
reversedNames = names.sorted(by: { s1, s2 in return s1 > s2 })
이런 식으로 쓸 수 있다.
이렇게 함수나 메소드에 인라인 클로저 형태로 클로저를 전달하면 항상 매개변수 타입과 반환값 타입을 생략하는 것이 가능하다.
그니까 함수에 인자로 inline closure을 넣을 때는 타입을 안 써줘도 된다는 거임!! 개편함
근데 코드 읽는 사람이 헷갈리지 않게 하려면 타입을 써 주는 것도 장려되긴 함.
위의 sorted에서도 이게 String타입을 정렬하는 것이라고 독자에게 말해주는 것이 더 안전할 수도 있다.
클로저 안 문장이 한 개라면 return을 생략해도 상관없다.
reversedNames = names.sorted(by: { s1, s2 in s1 > s2 } )
Swift는 shorthand argument를 제공하는데, $0, $1, $2, .... 이런식임.
이걸 사용하면 클로저의 인자 정의를 없앨 수 있다.
shorthand argument의 타입은 함수 타입에서 추론이 가능하고, 가장 큰 shorthand argument의 번호가 사용하는 매개변수의 갯수를 의미한다.
in 키워드도 없앨 수 있으므로, 다음과 같이 표시할 수 있음!
reversedNames = names.sorted(by: { $0 > $1 } )
여기서 더 짧게 쓸 수도 있음!!
Swift에서 String을 비교할 때 > 로 비교하는데, 요 연산자는 Bool을 반환한다.
sorted(by:) 가 원하는 반환값 타입과 동일하므로!이거만 전달해줘도 Swift가 알아서
"아~문자열 연산을 사용하고 싶구나~"를 추론하게 되는것
reversedNames = names.sorted(by: >)
이렇게 된다.
사실 이렇게까지 줄일 필요가 있나 싶기도 하고, 어렵기도 하고..
shorthand argument까지는 이해하겠는데 operator method를 사용하는 건 좀 힘들 것 같다.
함수의 마지막 인자로 클로저를 넣을 때 Closure Expression이 좀 길다면, 이 Trailing Closure를 사용하면 좋다.
함수 호출 괄호 뒤에 클로저를 쓸 수 있음!
만약 이런 함수가 있다고 치자.
func someFunc(closure: () -> Void) {
// 함수 실행문 어쩌구 저쩌구
}
이 함수를 후위 클로저 없이 사용하려면
someFunctionThatTakesAClosure(closure: {
// 클로저 실행문
})
후위 클로저를 사용하면?
someFunctionThatTakesAClosure() {
// 클로저 실행문
}
요런 식으로 간단하게 바뀐다.
위에서 했던 걸 이런 식으로 바꾸면
reversedNames = names.sorted() { $0 > $1 }
이렇게 바뀐다.
근데 만약에! 클로저가 함수의 유일한 인자라면?
소괄호도 없애도 된다!!!
reversedNames = names.sorted { $0 > $1 }
map(_:) 를 사용할 때, 이놈은 클로저 하나만 전달받는다. 그래서 후위 클로저를 사용하면 좋음!
이 클로저는 배열의 원소 하나마다 호출되고, mapping된 새로운 값을 뱉어준다.
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"]
이런 식의 코드로 변환이 된다.
digitNames[number % 10]! 에서 느낌표가 달린 이유는?
딕셔너리의 subscript([ ] 처럼 인덱스같은거) 는 딕셔너리 검색이 실패할 수도 있기 때문에 옵셔널 타입이기 때문이다!
위에서는 항상 값이 있을 것이 명확하기 때문에 강제 언래핑으로 값을 벗겨낸 것.
만약 함수 인자가 여러 개의 클로저를 가진다면?
첫 번째 클로저의 이름을 생략할 수 있고, 나머지 클로저들은 그대로 쓰면 된다.
// 이런 함수가 있다면,
func loadPicture(from server: Server, completion: (Picture) -> Void, onFailure: () -> Void) {
if let picture = download("photo.jpg", from: server) {
completion(picture)
} else {
onFailure()
}
}
// completion을 생략할 수 있고, 그 후 onFailure을 그대로 쓰면 된다.
loadPicture(from: someServer) { picture in
someView.currentPicture = picture
} onFailure: {
print("Couldn't download the next picture.")
}