[스위프트 대충보기] 6. 클로저(Closure)
-
클로저(closure)는 코드 안에서 여기저기 전달할 수 있고 사용할 수 있는 독립된 기능 블록이다. (흐미 이게 함수 정의랑 뭔 차이다냐?)
-
클로저는 자신이 정의되는 시점에 주변 환경(문맥, context)으로부터 여러 상수나 변수에 대한 참조를 잡아내서(capture) 저장한다
-
이렇게 문맥에서 참조를 잡아서 저장하는 과정을 클로징(closing)이라고 하고, 상수와 변수에 대해 그렇게 닫기 때문에 클로저(closure)라 부른다.
[사족: 굳이 따지자면 사실 클로저와 함수(내포함수나 무명함수)는 다르다. 클로저는 함수와 그 주변 환경에서 함수를 계산하는데 꼭 필요한 정보들을 묶어서 저장해 둔 덩어리를 부르는 것이다. 무명함수를 지원한다고 해도 클로저를 지원하지 않을 수 있고, 무명함수는 지원하지 않지만 클로저를 지원할 수는 있다. 그런 언어가 있는지는 잘 기억이 안나지만(왜냐하면 보통은 1등객체-무명함수-클로저를 언어에 세트로 넣곤 하니까). 하지만 그냥 혼용하곤 한다. 어차피 인간은 문맥에서 파악이 가능한 동물이니까.]
-
함수에서 언급한 내포 함수나 전역(global) 함수는 클로저의 특수한 경우이다. 차이를 보면 다음과 같다.
-
전역함수는 이름이 있고, 아무 값도 잡아두지 않는다. (잔소리: 전역변수는? 하고 생각하는 사람이 있을 지 모르겠지만, 전역변수가 왜 전역변수인가? 전역변수는 어디서든 액세스 가능하므로 굳이 잡아둘 이유가 없다.)
-
내포 함수는 이름이 있고, 자신을 포함하는 함수로 부터 값을 잡아둔다. (즉, 자신을 포함하는 함수에서 정의한 지역 변수나 자신을 포함하는 함수의 파라미터를 잡아둘 수 있다)
-
클로저 식은 가벼운 문법(lightweight syntax)으로 쓰여진 이름 없는 클로저로, 그 식이 들어있는 주변 문맥의 값(상수나 변수)을 잡아낼 수 있다.
-
-
클로저 식: { (매개변수들) -> 반환타입 in 본문 }
let names = ["Chris", "Alex", "Ewa", "Barry", "Daniella"] let reversed = sort(names, { (s1: String, s2: String) -> Bool in return s1 > s2 })
-스위프트는 클로저의 타입을 문맥으로부터 추론해 결정한다. 따라서, 위에서 sort에 전달한 클로저에서 타입을 제고하고, 반환 타입을 제거한 다음, 인자 튜플 주변의 괄호도 제거할 수 있다.
let reversed2 = sort(names, { s1, s2 in return s1 > s2 })
-
클로저 본문의 문장이 오직 return 밖에 없다면 return을 적지 않고 식만 하나 적어도 그 식을 계산한 결과를 자동으로 반환해준다.
let reversed3 = sort(names, { s1, s2 in s1 > s2 })
-
매개변수 이름을 사용하지 않고, 인자로 넘어오는 순서대로 $0, $1, ...를 사용하면 in과 그 앞의 매개변수 목록도 없앨 수 있다.
let reversed3 = sort(names, { $0 > $1 })
-
이렇게 두 인자를 받아서 연산자(>)에 넘기는 경우, > 자체가 저 클로저의 타입 (String, String) -> Bool 과 타입이 같다. 이런 경우 연산자를 바로 넘겨도 된다. (잡설: 함수 이름을 함수를 파라미터로 받는 함수에 넘길 수 있다면, 연산자를 넘기지 못할 이유도 딱히 없다.)
-
어떤 함수의 마지막 매개변수가 클로저인데, 그 함수에 넘길 클로저가 너무 길다면, 호출시 괄호 안에 클로저를 넣는 대신, 괄호를 닫은 뒤에 클로저를 붙일 수 있다. 이를 trailing closure라고 한다.
func 클로저를받는함수(closure: () -> ()) { ... } 클로저를받는함수({ ... }) // 클로저를 () 안에 넣기 클로저를받는함수(){...} // 클로저를 () 다음에 넣기 // 앞의 sort 예제를 더 깔끔하게 정리 let reversed4 = sort(names) { $0 > $1 }
-
클로저가 유일한 인자인 경우, trailing closure를 사용할 때 매개변수 목록의 ()를 생략 가능하다.
클로저를받는함수{...} // 유일한 인자로 클로저만 전달하는 경우
-
값 잡아내기 예제
func makeIncrementor(forIncrement amount: Int) -> () -> Int { var runningTotal = 0 func incrementor() -> Int { runningTotal += amount return runningTotal } return incrementor } let incrementByTen = makeIncrementor(forIncrement: 10) let incrementBySeven = makeIncrementor(forIncrement: 7) incrementByTen() // 10 반환 incrementBySeven() // 7 반환 incrementByTen() // 20 반환 incrementByTen() // 30 반환
-
위 예제에서 makeIncrementor를 매번 호출해 새로운 클로저를 생성할 때마다 새로 runningTotal 변수가 만들어지기 때문에, incrementByTen()과 incrementBySeven()은 각 클로저 내부의 runningTotal의 상태에 영향을 끼치지 못한다.
-
클로저는 참조 타입이다.
- 위 예에서처럼, incrementBySeven나 incrementByTen 자체는 상수(let으로 정의)였지만, 클로저에 잡혀있는 변수(runningTotal)는 호출할 때마다 변할 수 있다. 왜냐하면 incrementBySeven나 incrementByTen가 상수라서 가리키는 대상(참조)을 바꿀 수는 없지만, 그 대상 (클로저) 자체는 상태가 바뀔 수 있기 때문이다.
- 참조이기 때문에, 클로저를 다시 다른 상수나 변수에 대입하면, 같은 대상 클로저를 가리킨다.
let alsoIncrementByTen = incrementByTen alsoIncrementByTen() // 40 반환
감상: 역시나 무난