스위프트에서 정말 많이 쓰는 클로저 문법. 쓸 때마다 너무 헷갈려서 한번 제대로 정리를 해보려고 한다!
참고한 자료(거의 베낀 수준) : 야곰님의 스위프트 프로그래밍 3판
클로저란 무엇일까? 클로저는 변수나 상수가 선언된 위치에서 참조를 획득하고 저장할 수 있다. (Reference Capture)
이게 대체 뭔소린가...
아마 이 본문을 다 읽고나면 이해가 되지 않을까!
클로저에는 크게 세 가지 형태가 존재한다.
이러한 형태들이 존재한다고 한다. 애플은 클로저가 정갈하고 깔끔한 스타일이라고 주장하는데, 나는 굉장히 반대...하지만 클로저는 스위프트를 사용함에 있어서 필수적이기 때문에...싫어도 꼭 알고 자유롭게 사용할 줄 알아야 한다!
가장 중요한 클로저의 표현방법은 4가지가 있는데,
클로저의 기본 형식
{ (매개변수들) -> (반환 타입) in
실행코드
}
클로저의 기본 형식이 이렇다는 것은 알아두자.
먼저, 기본적으로 클로저를 이용하여 동일한 기능을 하는 코드를 어떻게 간결하게 표현하는지 알아보자.
미리 알아두어야 할 점: swift는 매개 변수로 함수가 올 수 있다!!
우리가 사용할 함수는 sorted(by: )
함수이다.
이 함수는 (by:)
항목에 들어간 규칙대로 정렬을 하는 기본함수이다. 애플 공식문서에서 함수의 기본형을 확인해보자.
func sorted(by areInIncreasingOrder: (Element, Element) throws -> Bool) rethrows -> [Element]
보면은 배열의 타입과 같은 두 개의 매개변수를 가지며 Bool 타입을 반환하는 함수를 전달인자로 받을 수 있는 것인데, 이 것을 클로저로 작성을 할 수 있다는 얘기다.
먼저 익숙한 방식인 함수로 작성을 해보고, 그걸 클로저로 바꿔보자.
func compares(fst: String, second: String) -> Bool { return (fst > second) }
// names 배열은 비교할 이름 배열이라고 가정하고, 여기선는 따로 넣어주지 않겠다.
let test:[String] = names.sorted(by: compares)
이 함수의 정의는 첫 번째 문자열이 두번째 문자열보다 크다면 true를 반환하는, 그러니까 큰게 앞으로 와야한다는 것이다.
이 함수를 클로저로 변환해보면 ,
let test:[String] = names.sorted(by: { (fst: String, second: String) -> Bool in return (fst > second) } )
여기서 보면 매개 변수 개수와 타입, 반환 타입이 모두 같다. 그러니까 훨씬 간결한 축약형이라는 것.
만약 compares
라는 함수를 자주 사용할 것이 아니라면 굳이 따로 정의를 해서 찾아다닐 필요가 없이 구현할 때 바로바로 클로저로 정의를 해서 넣어줄 수 있다는 것. 용도를 따지면 정말 간편하긴 하다.
함수나 메서드의 마지막 전달인자로 위치하는 클로저는 메서드의 소괄호를 닫은 후 작성해도 된다. 이것이 후행 클로저. 훨씬 간결하고 가독성이 좋게 변환할 수 있다.
// 소괄호를 생략 안한 경우.
let test:[String] = names.sorted(){ (fst:String, second: String) -> Bool in return fst>second }
// 소괄호까지 생략한 경우
let test:[String] = names.sorted{ (fst:String, second: String) -> Bool in return fst>second }
소괄호까지 생략한 게 훨씬 보기 좋은 것 같다.
앞서 말했듯이, 쓸데없이 똑똑한 우리 클로저는 문맥으로 타입을 유추할 수 있을 경우에는 굳이 타입을 명시하지 않아도 된다.
let test:[String] = name.sorted{ (fst,second) in return fst > second
}
여기서 보면 매개변수의 타입과 반환 타입을 생략해 주었는데, 엄청 간단해 졌다. 이걸 클로저 문법을 배우지 않고 읽기나 쓰기에는 힘든데, 배우고 보니 엄청 간결하고 좋다.
매개변수의 이름을 생략할 수 있다.
스위프트에서는 클로저에서 매개변수의 이름을 명시해주지 않았을 때, 암묵적으로 $0
, $1
이런식으로 매개변수 이름을 정의하도록 약속하였다. 물론 숫자는 순서대로 0,1,2,3,.... 이렇게 된다.
이를 활용하면 장점은 실행코드를 구분하기 위해 사용되었던 in
키워드마저 생략이 되어버린다.
let test:[String] = names.sorted{
return $0 > $1
}
뭐지...이게뭐야... 말이되나 이게.. 대박이다
여기서 더 줄일수 없을 거라고 생각하면 오산이다. 저기서 의미 없어보이는 return
이라는 키워드까지 없애버릴 수 있다.
클로저가 반환 값을 갖는 클로저이고, 내부의 실행문이 단 한줄이라면 이 키워드마저 제거가 가능하다. 암시적으로 반환 값이라고 생각하기 때문이다!!
let test:[String] = names.sorted { $0 > $1 }
와 처음에 사용했던 함수랑 비교해보면 진짜 말도 안되게 간결해지긴 한다.
클로저는 자신이 정의된 위치의 주변 문맥을 통해 상수나 변수를 획득할 수 있다고 한다. 이게 무슨 의미냐 하면, 주변에 정의한 상수나 변수가 더 이상 존재하지 않더라도 그 값을 자신 내부에서 수정하거나 참조할 수 있다는 것이다.
왜 이렇게 사용하냐 하면, 클로저가 비동기 작업에 많이 사용되기 때문이라고 한다. 비동기 작업 Call-Back을 작성하는 경우, 현재 상태를 미리 획득해두지 않으면 실제 클로저의 기능을 실행하는 순간에는 주변 상수나 변수가 이미 메모리에 존재하지 않는 경우가 발생할 수 있다!
이 책에서 예시로 중첩 함수 주변의 변수나 상수를 획득해 놓는 예시를 들었다.
func makeIncremeter(forIncrement amount:Int) -> ( () -> Int ) {
var runningTotal = 0
func incremeter() -> Int {
runningTotal += amount
return runningTotal
}
return incremeter
}
여기서 중첩함수인 incremeter
는 주변에 있는 runningTotal
과 amount
값을 획득 한다고 한다.
makeIncremeter
함수의 반환 타입을 보면 () -> Int
로 함수객체 반환을 의미한다.
반환하는 함수는 매개변수를 받지 않고 반환 값이 Int 타입인 함수인 것이다!
makeIncremeter
함수가 실행 될 때 중첩함수는 참조를 획득하고, 이 함수의 실행이 끝나도 사라지지 않는다. 게다가 중첩함수가 호출이 될 때마다 계속해서 사용할 수 있다!
좋은 예시를 책에서 들어주었다.
let incrementByTwo(): (()->Int) = makeIncremeter(forIncrement: 2)
let incrementByTwo2(): (()->Int) = makeIncremeter(forIncrement: 2)
let incrementByTen(): (()->Int) = makeIncremeter(forIncrement: 10)
// 3가지 상수에 함수를 할당해주었다.
let first:Int = incrementByTwo() // 2
let second:Int = incrementByTwo() // 4
let third:Int = incrementByTwo() // 6
let first:Int = incrementByTwo2() // 2
let second:Int = incrementByTwo2() // 4
let third:Int = incrementByTwo2() // 6
let first:Int = incrementByTen() // 10
let second:Int = incrementByTen() // 20
let third:Int = incrementByTen() // 30
각각의 함수는 언제 호출이 되더라도 자신만의 변수를 갖고 카운트하게 되며, 다른 함수의 영향을 전혀 받지 않는다. 왜냐하면 참조를 미리 획득했기 때문이다.
여기서 드는 의문점 : 클래스 인스턴스의 프로퍼티의 경우에는 ?
이 경우에는 강한참조 순환 문제가 발생할 수 있으나, 이 문제는 획득목록을 통해 없앨 수 있다고 한다.
자세한 내용은 이 책 뒤에서 다룬다고 하는데, 시간이 되면 한번 포스팅해볼것.
클로저와 함수는 참조타입이다.
참조가 무엇인지는 굳이 설명하지 않겠고, 예시로 바로 들어가자.
let incrementByTwo(): (()->Int) = makeIncremeter(forIncrement: 2)
let sameinc(): (()->Int) = incrementByTwo()
// 같은 클로저를 참조하고 있다.
let test:Int = incrementByTwo() // 2
let test2:Int = sameinc() // 4
같은 클로저를 참조하는 경우, 동작 결과가 같은 것을 알 수 있다.