2. Functions (4)

Seoyoung Lee·2022년 7월 15일
0
post-thumbnail
post-custom-banner

Define-and-Call

스위프트에서는 한 번에 익명 함수를 정의하고 호출할 수 있다. 이 패턴은 스위프트에서 흔하게 볼 수 있다.

{
	// ...
}()

중괄호({})는 익명 함수의 본문을 정의하고 괄호(())는 익명 함수를 호출한다. 저자는 이를 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))

Closures

스위프트 함수는 클로저 이며, 클로저는 본문 안에서 참조되는 외부(클로저 밖) 변수의 상태를 알 수 있다(capture, 획득). 또한 클로저는 본문 내에서 외부 변수의 값을 변경할 수도 있다.

Closure Preserving Captured Environment

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 의 값이 초기화되지 않고 유지되는 이유는 ctcountAdder 가 리턴하는 익명 함수 안에서 선언되지 않았기 때문이다. ct 는 한 번 0으로 초기화되고 익명 함수가 이 변수를 획득한다. ctcountedGreet 의 환경 속에서 유지되는 것이다.

Escaping Closure

값으로 전달되는 함수가 즉시 실행되지 않고 나중에 실행되기 위해 보존되는 경우, 이 함수는 자신의 환경을 시간이 흐르는 동안 파악하고 보존하는 클로저이다. 이런 클로저를 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 Lists (획득 목록)

함수 밖에 있는 변수를 획득하지 않고 단순히 변수의 값을 얻기 위해 변수를 참조하고 싶을 때는 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 는 이를 신경쓰지 않는다. 캡쳐 리스트를 사용했기 때문에 ff 가 선언되기 전에 x 의 값(0)을 캡쳐해둔다.

만약 fx 의 값을 바꾸려 하면 컴파일러가 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)
}

Curried Functions

함수형 프로그래밍에서 커링(Currying)은 여러 인자를 입력받는 함수를 인자 하나만 입력받는 함수들의 시퀀스로 변환하는 것을 말한다.

이에 관한 개념은 이 블로그에서 더 쉽게 설명하고 있다.

Function References and Selectors

함수를 이름으로 참조하고 싶을 때, 즉 함수를 다른 함수의 인자로 전달하려고 할 때, 함수의 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)

위 코드에서 whatToAnimatewhatToDoLater 은 bare name으로, 함수의 레퍼런스이다.

괄호를 제외하고 함수의 이름을 지칭하는 것은 함수를 호출하는 게 아니라 참조하는 것임을 명확하게 알려준다. 단, bare name을 가진 함수가 스코프 안에 단 하나일 때만 괄호를 붙이지 않는 것이 가능하다.

그럼 같은 이름을 가진 함수가 여러 개일 때는?

스위프트에는 더 정확하게 함수를 참조하기 위한 표기법이 존재한다. 이 표기법에는 두 가지 종류가 있다.

  1. Full name
    • 파라미터의 외부 이름, 괄호를 포함한 함수의 이름
  2. Signature
    • 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 () -> ())

시그니처를 명시적으로 표현해야 하는 대표적인 상황은 함수가 오버로딩되는 경우이다.

Function Reference Scope

스코프 안에 존재하는 함수를 참조할 때는 컴파일러에게 함수가 정의된 위치를 말하지 않아도 된다.

그러나 함수가 정의된 곳을 반드시 알려줘야 할 때도 있다. 함수의 위치는 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

Selectors

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 를 사용할 때 장점은 다음과 같다.

  1. 컴파일러가 함수 레퍼런스를 유효하게 만들어준다.
    • 함수 레퍼런스가 유효하지 않으면 코드가 컴파일되지 않는다. 또한 컴파일러는 전달할 함수가 Objective-C가 볼 수 있는 함수인지도 확인한다. Objective-C visibility를 갖기 위해 메소드를 선언할 때 반드시 @objc 라는 키워드가 붙어 있어야 한다.
  2. 컴파일러가 Objective-C selector를 만들어준다.
    • 코드가 정상적으로 컴파일된다면 파라미터로 전달되는 selector는 무조건 문제가 없는 메소드이다. 따라서 이전처럼 unrecognized selector 충돌이 일어나지 않는다.

✔️ 가끔 개발자가 selector를 직접 만들어야 할 때가 있다. 이때는 문자열을 사용하거나 Selector를 인스턴스화하면 된다(예: Selector("woohoo:").

✔️ action 메시지를 잘못된 target에 전달하면 #selector 를 사용해도 충돌이 일어날 수 있다.

예: selector로 사용된 메소드가 ViewController에 선언되어 있는데 target을 UIButton으로 설정한 경우 (self.button.addTarget(self.button, action: #selector(buttonPressed), for: .touchUpInside))

profile
나의 내일은 파래 🐳
post-custom-banner

0개의 댓글