스위프트에서는 한 번에 익명 함수를 정의하고 호출할 수 있다. 이 패턴은 스위프트에서 흔하게 볼 수 있다.
{
// ...
}()
중괄호({}
)는 익명 함수의 본문을 정의하고 괄호(()
)는 익명 함수를 호출한다. 저자는 이를 define-and-call
이라고 부른다고 한다.
Define-and-call 방법을 사용하면 일련의 준비 단계 없이 원하는 지점에서 바로 함수를 실행할 수 있다.
(그래서 특히 UI 컴포넌트들을 선언할 때 이런 방법으로 코드를 많이 작성하는 거였구나!)
// define-and-call 사용 전
let para = NSMutableParagraphStyle()
para.headIndent = 10
para.firstLineHeadIndent = 10
// ... more configuration of para ...
content.addAttribute(
.paragraphStyle,
value: para,
range: NSRange(location: 0, range: 1))
// define-and-call 사용 후
content.addAttribute(
.paragraphStyle,
value: {
let para = NSMutableParagraphStyle()
para.headIndent = 10
para.firstLineHeadIndent = 10
// ... more configuration of para ...
return para
}(),
range: NSRange(location: 0, range: 1))
스위프트 함수는 클로저
이며, 클로저는 본문 안에서 참조되는 외부(클로저 밖) 변수의 상태를 알 수 있다(capture, 획득). 또한 클로저는 본문 내에서 외부 변수의 값을 변경할 수도 있다.
func countAdder(_ f: @escaping () -> ()) -> () -> () {
var ct = 0
return {
ct = ct + 1
print("count is \(ct)")
f()
}
}
func greet() {
print("howdy")
}
let countedGreet = countAdder(greet)
countedGreet() // count is 1
countedGreet() // count is 2
countedGreet() // count is 3
위 코드에서 ct
의 값이 초기화되지 않고 유지되는 이유는 ct
가 countAdder
가 리턴하는 익명 함수 안에서 선언되지 않았기 때문이다. ct
는 한 번 0으로 초기화되고 익명 함수가 이 변수를 획득한다. ct
는 countedGreet
의 환경 속에서 유지되는 것이다.
값으로 전달되는 함수가 즉시 실행되지 않고 나중에 실행되기 위해 보존되는 경우, 이 함수는 자신의 환경을 시간이 흐르는 동안 파악하고 보존하는 클로저이다. 이런 클로저를 escaping closure
라고 한다. 가끔 이런 함수의 타입에 @escaping
을 반드시 명시해야 하는 경우들이 있다.
// 예시 1
func funcCaller(f:() -> ()) {
f()
}
위 함수는 매개 변수로 받은 함수를 즉시 실행하기 때문에 문제가 되지 않는다. (Non-Escaping Closure)
// 예시 2
func funcMarker() -> () -> () {
return { print("hello world") }
}
위 함수의 print()
함수는 즉시 실행되지 않지만 함수 내부에서 만들어졌기 때문에 @escaping
키워드를 붙이지 않아도 된다.
// 예시 3 - 컴파일 에러
func funcPasser(f:() -> ()) -> () -> () {
return f
}
// 예시 3 - 올바른 예시
func funcPasser(f:@escaping () -> ()) -> () -> () {
return f
}
위 함수는 함수를 파라미터로 받고 나중에 실행될 함수를 리턴하기 때문에 매개변수 타입 앞에 반드시 @escaping
키워드를 붙여주어야 한다.
예시 2, 3은 단순히 클로저를 리턴할 뿐, 실행하는 것이 아니기 때문에 리턴되는 클로저는 escaping closure이다. (escaping closure는 함수의 인자로 전달됐을 때 함수의 실행이 종료된 후 실행되는 클로저로, 함수 밖에서 실행(escaping)되는 클로저임을 다시 한 번 기억하자!)
escaping closure의 또 다른 특징은 함수 내부에서 self
의 프로퍼티나 메소드를 참조하는 경우, self
를 명시해주어야 한다는 것이다. 이는 이러한 참조가 self
를 획득하며, 컴파일러가 self
를 명시함으로써 이 사실을 인지시키고 싶어하기 때문이다. (이 이야기는 나중에 메모리 관리와 관련된 부분에서 다시 언급되는 것 같다. 나중에 다시 살펴보자!)
함수 밖에 있는 변수를 획득하지 않고 단순히 변수의 값을 얻기 위해 변수를 참조하고 싶을 때는 capture list
를 사용할 수 있다.
Capture list는 함수가 익명 함수일 때만 사용할 수 있다. Capture list를 사용하기 위해서는 익명 함수 본문의 첫 줄에 참조할 변수를 담은 리스트([]
)를 적는다. 또한 반드시 in
키워드를 포함해야 한다.
Capture list를 사용하면 값이 클로저에 의해 캡쳐되는 것이 아니라 파라미터로 전달되는 것처럼 동작한다. 또한 파라미터처럼 let
으로 선언된 것처럼 동작한다.
var x = 0
let f: () -> () = { [x] in
print(x)
}
f() // 0
x = 1
f() // 0
두 번째 f
가 호출되기 전 x
의 값이 1로 바뀌었지만 f
는 이를 신경쓰지 않는다. 캡쳐 리스트를 사용했기 때문에 f
는 f
가 선언되기 전에 x
의 값(0)을 캡쳐해둔다.
만약 f
가 x
의 값을 바꾸려 하면 컴파일러가 Cannot assign to value: x is an immutable capture.
라는 메시지를 띄운다.
또한 캡쳐 리스트 안에 있는 변수에 다른 이름을 붙여도 된다. (예: [y=x]
) 그러면 함수 안에서는 새로 붙인 이름을 사용한다.
스위프트 5.3 이후부터는 escpaing closure에서 self
키워드를 생략하기 위해 캡쳐 리스트를 사용하기도 한다. 캡쳐 리스트 안에 self
를 넣으면 self
를 반드시 명시하지 않아도 된다.
let f2 = funcPasser { [self] in
print(view.bounds)
}
함수형 프로그래밍에서 커링(Currying)은 여러 인자를 입력받는 함수를 인자 하나만 입력받는 함수들의 시퀀스로 변환하는 것을 말한다.
이에 관한 개념은 이 블로그에서 더 쉽게 설명하고 있다.
함수를 이름으로 참조하고 싶을 때, 즉 함수를 다른 함수의 인자로 전달하려고 할 때, 함수의 bare name
을 사용할 수 있다. bare name
은 괄호를 제외한 함수의 이름을 말한다.
func whatToAnimate() {
self.myButton.frame.origin.y += 20
}
func whatToDoLater(finished:Bool) {
print("finished: \(finished)")
}
UIView.animate(withDuration:0.4, animations: whatToAnimate, completion: whatToDoLater)
위 코드에서 whatToAnimate
와 whatToDoLater
은 bare name으로, 함수의 레퍼런스이다.
괄호를 제외하고 함수의 이름을 지칭하는 것은 함수를 호출하는 게 아니라 참조하는 것임을 명확하게 알려준다. 단, bare name을 가진 함수가 스코프 안에 단 하나일 때만 괄호를 붙이지 않는 것이 가능하다.
스위프트에는 더 정확하게 함수를 참조하기 위한 표기법이 존재한다. 이 표기법에는 두 가지 종류가 있다.
as
키워드를 이용해서 함수의 bare name이나 full name 옆에 함수의 시그니처 표현func say(_ s: String, times: Int) {
위 함수의 full name은 say(_:times:)
이고 시그니처는 say as (String, Int) -> ()
이다.
class Dog {
func bark() {
// ...
}
func bark(_ loudly: Bool) {
// ...
}
func test() {
let barkFunction = bark(_:) // fine
}
}
위 코드는 문제 없이 동작하지만 파라미터가 없는 함수를 참조하고자 할 때는 full name을 사용하는 것만으로 문제가 해결되지 않는다. 파라미터가 없는 경우에는 full name이 bare name으로 맥락이 모호하기 때문이다. 따라서 파라미터가 없는 함수는 시그니처를 사용해야 한다. (ex. bark as () -> ()
)
시그니처를 명시적으로 표현해야 하는 대표적인 상황은 함수가 오버로딩되는 경우이다.
스코프 안에 존재하는 함수를 참조할 때는 컴파일러에게 함수가 정의된 위치를 말하지 않아도 된다.
그러나 함수가 정의된 곳을 반드시 알려줘야 할 때도 있다. 함수의 위치는 dot-notation을 이용해서 나타낼 수 있다. self
키워드를 붙여야 할 때나 다른 타입의 인스턴스 메소드를 참조하고자 할 때는 dot-notation을 사용해서 함수의 위치를 나타낸다.
타입을 dot-notation으로 명시하는 것도 가능하다.
class Cat {
func purr() {
}
}
class Dog {
func bark() {
}
func test() {
let barkFunction = Dog.bark
let purrFunction = Cat.purr
}
}
여기서 추가로 함수의 시그니처까지 명시한다면 그때 시그니처는 반드시 인스턴스 메소드의 curried static 또는 class 버전을 표현하고 있어야 한다.
let purrFUnction = Cat.purr as (Cat) -> () -> Void
Objective-C에서 selector는 메소드를 참조하는 방법 중 하나이다. iOS 프로그래밍에서는 selector를 파라미터로 갖는 Cocoa 메소드를 호출할 때가 많다.
이때 파라미터의 이름은 주로 selector:
또는 action:
이다. 또한 이런 메소드들은 target
(오브젝트 레퍼런스)를 필요로 한다. 런타임은 selector를 메시지로 변환하고 target에 메시지를 보냄으로써 메소드를 나중에 호출할 수 있다.
이런 아키텍처를 사용할 때 주의해야 할 점이 있다. selector를 만들 때 반드시 메소드의 Objective-C 이름을 대표하는 문자열을 사용해야 한다. 그래서 selector를 사용할 때 selector string을 만드는 규칙을 지키지 않거나 오타가 있다면 프로그램이 실행되지 않는다. (unrecognized selector)
// 에러 발생
// "buttonPressed:"라고 작성해야 함
self.button.addTarget(self, action: "buttonPressed", for: .touchUpInside)
@objc func buttonPressed(_ sender: Any) {
// ...
}
스위프트에서는 이런 실수를 방지하기 위해 #selector
라는 문법을 사용해서 컴파일러가 selector를 만들 수 있게 해준다.
self.button.addTarget(self, action: #selector(buttonPressed), for: .touchUpInside)
#selector
를 사용할 때 장점은 다음과 같다.
@objc
라는 키워드가 붙어 있어야 한다.✔️ 가끔 개발자가 selector를 직접 만들어야 할 때가 있다. 이때는 문자열을 사용하거나 Selector를 인스턴스화하면 된다(예: Selector("woohoo:")
.
✔️ action 메시지를 잘못된 target에 전달하면 #selector
를 사용해도 충돌이 일어날 수 있다.
예: selector로 사용된 메소드가 ViewController
에 선언되어 있는데 target을 UIButton
으로 설정한 경우 (self.button.addTarget(self.button, action: #selector(buttonPressed), for: .touchUpInside)
)