상수와 변수의 값을 인스턴스의 일부로 저장한다. 클래스와 구조체에서만 사용된다.
struct Person {
let name: String = "John Doe"
var age: Int = 33
}
위와 같이 Person 구조체가 있을 때, name은 상수 저장 프로퍼티이므로 값 변경이 불가하다.
반면 age는 변수 저장 프로퍼티이므로 값을 변경해 사용할 수 있다.
흔히 아는 Dot Syntax를 Swift에서는 위와 같이 표현한다. 위에서 언급했듯 age는 변수 저장 프로퍼티이므로 인스턴스 생성 후 멤버인 age의 값을 dot으로 변경할 수 있다.
let p = Person()
p.age = 30
하지만p.name = "HEY"
와 같이 상수 저장 프로퍼티의 값을 변경하려고 하는 것은 안된다. 오류가 발생할 것이다.
수정은 안되더라도 읽는 것은 가능하다. 동일한 방식으로 명시적 맴버 표현식을 이용하면 된다.
print(p.name)
print(p.age)
지연 저장 속성은 다른 포스트에서 따로 정리했다.
프로퍼티를 사용할 때 특정 연산을 수행하여 값을 반환한다. 여기서 computed는 수학적으로 계산되는 것이 아니라, 다른 속성을 기반으로 속성값이 결정된다는 뜻이다.
저장 프로퍼티는 값을 저장할 메모리 공간을 가지는 반면, 연산 프로퍼티는 가지지 않는다. 다른 프로퍼티에 저장된 값을 읽어 연산 후 반환하는 것이다.
이런 특징때문에 프로퍼티에 접근할 때마다 다른 값이 반환될 수 있다. 그러므로 let이 아닌 var로 선언해야 한다. 연산 프로퍼티는 클래스와 구조체, 열거형에서 사용된다.
문법은 아래와 같다.
var name: Type {
get {
// statements
return value
}
set(name) {
// statements
}
}
선언 시점에 기본값을 저장하지 않는다. 따라서 형식 추론이 불가능하므로 반드시 Type을 지정해주어야 한다. getter에는 return 값을 명시해야 하며, setter는 값을 저장할 때 실행된다. 괄호와 파라미터는 생략 가능하며 파라미터값은 newValue 키워드로 접근할 수 있다.
아래는 연산 프로퍼티를 활용한 예제이다.
class Person {
var name: String
var yearOfBirth: Int
init(name: String, year: Int) {
self.name = name
self.yearOfBirth = year
}
var age: Int {
get {
let calendar = Calendar.current
let now = Date()
let year = calendar.component(.year, from: now)
return year - yearOfBirth
}
set {
let calendar = Calendar.current
let now = Date()
let year = calendar.component(.year, from: now)
yearOfBirth = year - newValue
}
}
}
출생년도yearOfBirth
프로퍼티의 값을 이용하여 나이age
를 계산하는 computed property를 선언하였다.
58번째 줄에서 p.age를 호출하면 p 생성자에 입력된 출생년도(year)인 2002를 기준으로 age의 get을 통해 연산된다.
60번째 줄에서 p.age의 값을 50으로 설정하고 있는데, 이때는 yearOfBirth를 이용하는 age 변수의 set 구문이 연산되면서 p.yearOfBirth가 1972가 되며, 61번째 줄 코드를 통해 확인할 수 있다.
또, 함께 작성된 주석을 보면 setter가 없으면 읽기 전용
이라는 것을 알 수 있다.
var name: Type {
get {
// statements
return expr
}
}
위와 같은 형태로 작성하면 읽기 전용이 되고, get 키워드와 그에 따른 괄호를 생략할 수도 있다.
⛔️ 할당 연산자를 넣게되면 클로저에 변수를 할당한다는 뜻이 되므로, 할당연산자(=)는 포함되지 않는 것을 유의하자.
⛔️ 쓰기 전용 연산 프로퍼티는 없으므로 set 구문만 쓰는 것도 불가능하다.
class, struct, enum에서 사용된다. static 키워드로 선언하며, 자동으로 lazy하게 작동한다. (lazy 키워드를 직접 붙일 필요가 없다.)
저장 타입 프로퍼티는 항상 초기값을 가져야 한다. 그 이유는 static으로 저장되는 저장 타입 프로퍼티의 경우, 초기화할 때 값을 할당할 initializer가 없기 때문이다.
class Math {
static let pi = 3.14
}
let m = Math()
위 예제에서 pi는 타입 프로퍼티로 선언되어 있으나, Math 클래스 내에 있고 Math의 인스턴스가 생성될 때 initializer에 의해 모든 프로퍼티가 초기화되지 않나?
타입 프로퍼티는 인스턴스가 생성될 때마다 매번 생성되는 기존 프로퍼티와 다르다. 인스턴스가 생성된다고 매번 해당 인스턴스의 멤버로 생성되는 것이 아니라, 한 번 호출되어 메모리에 올라가면 그 뒤로는 다시 호출되지 않으며 언제 어디서든 이 타입 프로퍼티에 접근할 수 있는 것이다.
m.pi // error!
따라서 pi라는 프로퍼티에 접근할 때도 위와 같이 인스턴스를 통한 방식으로는 접근할 수 없다.
Math.pi
타입 이름을 통해서만 접근이 가능하다. 앞서 타입 프로퍼티는 "한 번 호출되어 메모리에 올라가면"
이라고 언급했다. 위 예제코드에서라면 방금 Math.pi
코드를 통해 최초 호출되어 메모리에 올라갔다.
이런 방식은 lazy stored property와 동일하다고 볼 수 있다.
연산 타입 프로퍼티는 Subclass에서 오바라이딩이 가능하다. 이는 앞에 static
을 붙여주느냐, class
를 붙여주느냐로 구분한다.
class
로 선언한 연산 프로퍼티의 경우, Subclass에서 연산 타입 프로퍼티를 오버라이드해서 사용할 수 있다.
static
으로 선언한 연산 프로퍼티의 경우, 서브클래스에서 오버라이드가 불가능하다.
보통 모든 타입이 공통적인 값을 정의하는 데 유용하기 때문이다.
위에서 쓴 수학의 파이값을 그 예로 들 수 있고, 일반적인 예로는 싱글톤이 있다.
willSet
속성에 값이 지정되기 전에 호출, newValue
라는 기본 파라미터가 제공된다.
didSet
값이 저장된 직후에 호출된다. 이전 값이 파라미터로 전달되며 oldValue
가 기본 파라미터로 제공된다.
아래와 같이 활용할 수 있다.
class Size {
var width = 0.0 {
willSet {
print(width, "=>", newValue)
}
didSet {
print(oldValue, "=>", width)
}
}
}
let s = Size()
s.width = 123
self
구조체, 클래스 모두 사용한다.
super
상속과 관련있으므로 클래스에서만 사용한다.