변수의 초깃값을 계산하기 위해 여러 줄의 코드를 작성하고 실행해야 할 때가 있다. 이를 구현하는 가장 간단하고 빠른 방법은 define-and-call 익명 함수를 사용하는 것이다.
class RootViewController: UITableViewController {
let cellBackgroundImage : UIImage = {
return imageOfSize(CGSize(width: 320, height: 44)) {
// ...
}
}()
// ...
}
위 예시처럼 인스턴스 프로퍼티를 초기화할 때도 이런 방법을 사용할 수 있다.
그럼 define-and-call을 사용해서 초기화하는 대신 인스턴스 메소드를 만들고 이를 호출해서 인스턴스 프로퍼티를 호출하는 방법은 어떨까?
class RootViewController: UITableViewController {
let cellBackgroundImage : UIImage = self.makeTheImage() // compile error
func makeTheImage() -> UIImage {
return imageOfSize(CGSize(width: 320, height: 44)) {
// ...
}
}
}
어림도 없다. 컴파일 에러가 발생한다.
인스턴스 프로퍼티가 초기화되는 시점에는 인스턴스가 아직 존재하지 않는다. 따라서 프로퍼티를 선언할 때 self
를 참조할 수 없다. define-and-call 익명 함수를 사용하는 건 가능하지만 여기서도 self
를 참조할 수는 없다.
우리가 지금까지 봐왔던 변수들은 모두 stored variable이다. 스위프트에서 변수는 완전히 다르게 동작할 수 있다. 다시 말해 연산을 할 수 있다. 이는 변수가 값이 아니라 함수를 가지고 있음을 의미한다.
setter
라는 함수는 변수가 할당될 때 호출되고 getter
라는 함수는 변수가 참조될 때 호출된다.
var now: String { // 1
get { // 2
return Date().description // 3
}
set { // 4
print(newValue) // 5
}
}
let
이 아닌 var
로 선언되어야 한다. 또한 자료형도 명시해야 한다.get
이다.return
키워드를 생략해도 된다.set
이다.newValue
라는 이름으로 setter 함수에 전달된다.now = "Howdy"
print(now)
computed variable을 사용하는 법은 다른 변수와 다르지 않다. 다만 변수의 값을 할당하거나 변수를 사용할 때는 setter와 getter 함수가 호출된다.
set
함수의 파라미터 이름을 바꿀 수 있다.
set (val) { // ...
setter 함수는 생략이 가능하다. setter 함수를 생략하면 변수는 읽기 전용 변수가 된다. 즉 let
변수와 같아져 값을 할당하려고 하면 컴파일 에러가 발생한다.
getter 함수는 반드시 있어야 한다. 단, setter가 없으면 get
과 중괄호는 생략이 가능하다. return
키워드 역시 상황에 따라 생략이 가능하다.
var now: String {
Date().description
}
실제로 computed variable은 인스턴스 프로퍼티에서 많이 사용한다. 연산 프로퍼티가 자주 유용하게 사용되는 경우는 다음과 같다.
필요할 때마다 값이 즉시 연산되어야 하는 경우에는 읽기 전용 연산 프로퍼티로 만드는 것이 좋다.
연산 프로퍼티의 getter는 여러 줄의 코드를 캡슐화 할 수 있으며, 메소드를 프로퍼티로 변환해준다.
var authorOfItem: String? {
guard let authorNodes =
self.extensionElements(
withXMLNamespace: "http://www.tidbits.com/dummy",
elementName: "app_author_name")
else { return nil }
guard let authorNode = authorNodes.last as? FPExtensionNode
else { return nil }
return authorNode.stringValue
}
위 예시에서 authorOfItem
프로퍼티 안에서 XML를 파싱하고 값을 추출한다. 이를 func authorOfItem() -> String?
메소드로 선언할 수도 있지만 메소드는 과정을 나타내는 반면 연산 프로퍼티는 더 직관적으로 어떤 것으로 특징짓는다.
연산 프로퍼티는 저장 프로퍼티의 앞에 서서 저장 프로퍼티의 문지기 역할을 할 수 있다. 이는 Objective-C의 accessor method와 유사하다.
이는 저장 프로퍼티의 값을 얻거나 변경할 때 추가로 다른 작업을 해야 하는 경우 유용하다.
private var _pp: Int = 0
var pp: Int {
get {
self._pp
}
set {
self._pp = max(min(newValue,5),0)
}
}
연산 인스턴스 프로퍼티의 getter와 setter는 다른 인스턴스 멤버를 참조할 수 있다. 중요한 것은 일반적으로 저장 프로퍼티의 이니셜라이저는 가능하지 못하다는 것이다. 연산 프로퍼티에서는 가능한 이유는 getter와 setter 함수가 인스턴스가 실제로 존재하기 전에는 호출이 되지 않기 때문이다.
동일한 로직을 갖는 연산 프로퍼티가 여러 개가 있다면 중복되는 코드가 매우 많아질 것이다. property wrapper
를 사용하면 공통되는 기능을 한 곳에 위치시킬 수 있다.
property wrapper를 선언할 때는 타입 앞에 @propertyWrapper
을 붙여주어야 한다. 또한 wrappedValue
라는 연산 프로퍼티를 반드시 가져야 한다.
@propertyWrapper struct Clamped {
private var _i: Int = 0
var wrappedValue: Int {
get {
self._i
}
set {
self._i = Swift.max(Swift.min(newValue,5),0)
}
}
}
위와 같이 property wrapper를 선언한 후부터는 getter나 setter 없이 위 구조체와 같은 이름의 속성(@Clamped
)으로 표시된 연산 프로퍼티를 선언할 수 있다.
@Clamped var p
p
는 연산 프로퍼티이기 때문에 초기화되지 않아도 된다. 또한 getter와 setter 역시 없어도 되며, 타입 역시 명시하지 않아도 된다. Clamped 구조체의 wrappedValue
연산 프로퍼티가 다 알아서 해주기 때문이다.
뒤에서는 실제 Clamped 인스턴스가 생성된다. self.p
에 값을 할당하면 할당된 값은 Clamped 인스턴스 wrappedValue
의 setter를 통과해서 Clamped 인스턴스의 _i
프로퍼티에 저장된다.
self.p
의 값을 가져올 때는 Clamepd 인스턴스의 wrappedValue
getter에서 리턴된 값을 가져오는 것이며, 이는 Clamped 인스턴스의 _i
프로퍼티의 값과 같다.
property wrapper를 사용하면 연산 프로퍼티의 패턴을 캡슐화할 수 있으며 같은 동작을 하는 또 다른 @Clamped
프로퍼티를 선언할 수 있다. 또한 이런 패턴들에 이름을 붙임으로써 연산 프로퍼티의 역할을 명시해줄 수도 있다.