연산 프로퍼티는 생각보다 저장 프로퍼티의 문지기 역할을 많이 하지 않는다. 스위프트에는 setter observers라는 기능이 있기 때문이다. setter observer는 저장 프로퍼티 내부에 setter 기능을 포함시킨 것으로, 다른 코드가 저장 프로퍼티의 값을 설정한 직전이나 직후에 호출된다.
var s = "whatever" { // 1
willSet { // 2
print(newValue) // 3
}
didSet { // 4
print(oldValue) // 5
// self.s = "something else"
}
}
let
이 아닌 var
로 선언되어야 한다. 또한 초깃값을 가질 수 있다.willSet
함수는 변수가 새 값을 얻기 직전에 호출된다.willSet
함수는 기본적으로 새로 들어오는 값을 newValue
라는 이름으로 받는다. newValue가 아닌 다른 이름을 쓸 수도 있다. 또한 변경되기 전 변수의 값이 아직 저장 프로퍼티 내에 저장되어 있기 때문에 willSet
함수가 그 값에 접근할 수 있다.didSet
함수는 변수가 새 값을 얻은 직후에 호출된다.didSet
함수는 변경되기 전 변수의 값(oldValue
)을 가지고 있다. oldValue 역시 이름을 바꿀 수 있다. 변수의 새 값이 저장 프로퍼티 안에 존재하기 때문에 didSet
함수가 그 값에 접근할 수 있다. 또한 didSet
함수 내에서 저장 프로퍼티에 다른 값을 할당하는 것도 가능하다.💡 TIP) Setter observer는 저장 프로퍼티가 초기화되거나 didSet
함수가 저장 프로퍼티의 값을 바꿀 때는 호출되지 않는다.
실제 iOS 프로그래밍에서 setter observer는 프로퍼티와 인터페이스를 동기화할 때 유용하다.
// iOS 개발시 setter observer 사용 예시
var angle: CGFloat = 0 {
didSet {
// modify interface to match
self.transform = CGAffineTransform(rotationAngle: self.angle)
}
}
연산 프로퍼티는 setter observer를 가질 수 없다. 이미 setter 함수를 가지고 있기 때문이다. 단, property-wrapped 연산 프로퍼티는 setter observer를 가질 수 있다.
저장 프로퍼티가 선언시 초깃값을 가지고 lazy initialization을 하면 그 초깃값은 실제 값이 사용되기 전까지는 할당되지 않는다.
스위프트에서 lazy로 초기화될 수 있는 변수의 종류는 네 가지이다.
전역 변수는 자동으로 lazy로 선언된다. 앱이 실행되면 파일들과 그 파일들의 최상위에 있는 코드들이 나타난다. 앱이 실행되기 전에는 전역 변수를 초기화할 수 없다. 전역 변수의 초기화는 의미가 있는 시점에 이루어져야 한다. 따라서 전역 변수는 다른 코드가 처음으로 전역 변수를 참조하기 전에는 초기화되지 않는다. 이는 초기화를 한 번만 발생하거나 thread-safe로 만드는 방법 두 가지 모두로 구현된다.
static 프로퍼티도 역시 자동으로 lazy로 선언된다. static 프로퍼티는 전역 변수와 똑같이 동작하기 때문에 같은 이유로 lazy로 선언되어야 한다.
*스위프트에는 저장 class 프로퍼티가 없기 때문에 class 프로퍼티는 초기화될 수 없고, 따라서 lazy 초기화를 할 수 없다.
인스턴스 프로퍼티는 기본적으로는 lazy로 선언되지 않는다. 대신 변수를 선언할 때 lazy
키워드를 붙여주면 lazy로 선언할 수 있다. 이때 반드시 let
이 아닌 var
로 선언해야 한다.
스위프트 5.5부터 lazy var
로 지역 변수를 선언할 수 있다.
lazy로 선언된 변수가 한 번도 참조되지 않으면 변수의 이니셜라이저는 절대 실행되지 않는다. 변수의 초깃값이 생성되는 데 큰 비용이 들 때 변수를 lazy로 선언하면 실제로 그 값이 필요하기 전까지 값이 생성되는 걸 막을 수 있다.
lazy는 싱글톤(singleton)을 구현하기 위해 자주 사용된다. 싱글톤은 모든 코드가 특정 클래스의 단일 공유 인스턴스에 접근할 수 있는 패턴을 말한다.
class MyClass {
static let shared = MyClass()
}
다른 코드들은 MyClass.shared
로 MyClass의 싱글톤에 접근할 수 있다.
MyClass.shared
가 처음 참조되기 전에는 싱글톤 인스턴스는 생성되지 않는다. 또한 MyClass.shared
는 언제나 동일한 인스턴스를 반환한다.
lazy 이니셜라이저는 일반 이니셜라이저가 할 수 없는 일들을 할 수 있다.
대표적으로 lazy 이니셜라이저는 인스턴스를 참조할 수 있다. 일반 이니셜라이저는 실행되기 전에는 인스턴스가 존재하지 않기 때문에 인스턴스를 참조할 수 없다. 반면 lazy 이니셜라이저는 인스턴스가 존재한 후에 실행되는 것이 보장되기 때문에 인스턴스 참조가 가능하다.
class MyView: UIView {
lazy var arrow = self.arrowImage() // legal
func arrowImage () -> UIImage {
// ...
}
}
lazy 인스턴스 프로퍼티는 self
를 참조하는 define-and-call 익명 함수 내에서 흔하게 사용된다.
lazy var prog: UIProgressView = {
let p = UIProgressView(progressViewStyle: .default)
p.frame = CGRect(x: 0, y: 0, width: self.view.bounds.size.width, height: 20) // legal
// ...
return p
}()