클래스와 구조체를 구성하는 요소 중 하나인 프로퍼티는 우리 말로 번역하면 '속성'으로, 값을 저장하기 위한 목적으로 정의된 변수나 상수라고 알고있다. 사실 프로퍼티가 하는 정확한 역할은 값을 제공하는 것이다.
값을 제공하는 것과 저장하는 것 사이에 차이가 없다고 생각할 수 있다. 저장이 아니라 제공에 목적이 있다고 하는 것은, 프로퍼티 일부는 값을 저장하지는 않지만 값을 제공하는 특성을 갖기 때문이다.
설명한 것처럼 프로퍼티는 값에 대한 저장 여부를 기준으로 두 가지 종류로 나눌 수 있다. 이를 저장 프로퍼티와 연산 프로퍼티라고 한다. 두 프로퍼티의 차이는 아래와 같다.
- 저장 프로퍼티
- 입력된 값을 저장하거나 저장된 값을 제공하는 역할
- 상수 및 변수를 사용해서 정의 가능
- 클래스와 구조체에서는 사용이 가능하지만, 열거형 에서는 사용할 수 없음
- 연산 프로퍼티
- 특정 연산을 통해 값을 만들어 제공하는 역할
- 변수만 사용해서 정의 가능
- 클래스, 구조체, 열거형 모두 사용 가능
저장 프로퍼티와 연산 프로퍼티는 대체로 클래스나 구조체를 바탕으로 만들어진 개별 인스턴스에 소속되어 값을 저장하거나 연산 처리하는 역할을 한다. 따라서 프로퍼티를 사용하려면 인스턴스가 필요하다. 인스턴스를 생성한 다음 이 인스턴스를 통해서 프로퍼티를 참조하거나 값을 할당해야 한다. 예외적으로 일부 프로퍼티는 클래스와 구조체 자체에 소속되어 값을 가지기도 한다. 이런 프로퍼티들을 타입 프로퍼티(Type Properties)라고 한다. 타입 프로퍼티는 인스턴스를 생성하지 않아도 사용할 수 있다.
스위프트에서는 프로퍼티 값을 모니터링 하기 위해 프로퍼티 옵저버(Property Observer)를 정의하여, 사용자가 정의한 특정 액션과 반응하도록 처리할 수 있다. 프로퍼티 옵저버는 우리가 직접 정의한 저장 프로퍼티에 추가할 수 있으며 슈퍼 클래스로부터 상속받은 서브 클래스에서도 추가할 수 있다.
저장 프로퍼티
저장 프로퍼티(Stored Property)는 클래스 내에서 선언된 변수나 상수를 부르는 이름이다. 일반 변수나 상수를 선언할 때 초기값을 할당할 수 있는 것처럼 저장 프로퍼티를 선언할 때에도 초기값을 할당할 수 있다. 하지만 선언하는 시점에서 초기값을 할당해야 하는 것은 아니다. 초기화 구문에서 초기값을 설정해도 된다. 구조체의 멤버와이즈 구문이 이같은 역할을 한다.
하지만, 클래스에서 프로퍼티를 선언할 때 초기값을 함께 할당해 주지 않으면 신경쓸 것이 있다. 우선 프로퍼티 선언 시 초기값이 할당되지 않은 저장 프로퍼티는 반드시 옵셔널 타입으로 선언해 주어야 한다. 스위프트에서는 클래스의 프로퍼티에 값이 비어 있으면 인스턴스를 생성할 때 무조건 nil값으로 초기화하기 때문이다.
옵셔널 타입으로 프로퍼티를 선언할 때에는 일반 옵셔널 타입과 묵시적 옵셔널 해제 타입 중에서 선택해서 정의할 수 있다. 묵시적 옵셔널 타입 해제 구문은 값을 사용할 시점에서는 절때 nil이 되지 않지만, 선언할 때에는 초기값을 할당해줄 수 없어서 옵셔널로 선언해야 하는 저장 프로퍼티에 사용된다. 묵시적 옵셔널 타입으로 지정해두면 이 값을 사용할 때 옵셔널 해제 처리할 필요 없이 일반 변수처럼 쓸 수 있기 때문에 편리하다.
저장 프로퍼티를 선언할 때 초기값을 주지 않으면서도 옵셔널 타입으로 선언하지 않을 수 있는 방법이 있다. 바로 초기화 구문에서 프로퍼티의 값을 초기화하는 것이다. 어차피 클래스의 프로퍼티는 인스턴스를 생성할 때 초기화되기 때문에, 프로퍼티의 초기값은 인스턴스를 생성하기 전까지만 할당해 줄 수 있으면 문제가 되지 않는다. 따라서 초기화 구문 내에서 프로퍼티의 값을 할당해줄 수 있으면 이 프로퍼티의 타입은 옵셔널로 선언하지 않아도 된다.
반면, 구조체는 이같은 초기값으로부터 자유로워서, 초기값을 할당하지 않고 선언만 하더라도 프로퍼티의 타입을 옵셔널로 지정하지 않아도 된다. 멤버와이즈 초기화 구문이 제공되기 때문이다. 멤버와이즈 초기화 구문은 인스턴스 생성 시 인자값을 받아 프로퍼티의 값을 초기화시켜주는 역할을 한다. 이런 멤버와이즈 구문이 값의 초기화를 보장해주기 때문에 옵셔널 타입으로 지정하지 않아도 된다. 아래는 예제이다.
class User {
var name: String
}
일반 타입의 문자열로 선언된 이 프로퍼티에 값이 할당되어 있지 않으므로 컴파일러는 오류를 발생시킨다. 이를 해결하기 위해 세 개의 해결책이 있다.
// 첫 번째 해결책 - 초기화 구문을작성하고, 그 안에서 초기값을 할당해 준다.
class User {
var name: String
init() {
self.name = ""
}
}
init 메소드 내부에 작성된 구문은 인스턴스가 생성될 때 실행된다. 여기서 self 라는 키워드가 중요한데 클래스에 선언된 프로퍼티나 메소드는 self 키워드를 붙여서 구분한다.
// 두 번쨰 해결책 - 프로퍼티를 옵셔널 타입으로 바꿔준다.
class User {
var name: String?
}
// 혹은
class User {
var name: String!
}
옵셔널 타입으로 프로퍼티를 선언할 경우, 초기화하지 않았더라도 시스템이 자동으로 초기화를 해주므로 문제가 발생하지 않는다. 프로퍼티가 nil이 되지 않을 자신이 있다면 묵시적 옵셔널 해제 타입을 사용하는 것이 편리하다.
// 세 번쨰 해결책 - 프로퍼티에 초기값을 할당해준다.
class User {
var name: String = ""
}
처음부터 빈 초기값을 입력하면 많은 문제로부터 벗어날 수 있다.
저장 프로퍼티의 분류
저장 프로퍼티는 다음 두 가지로 나눌 수 있다.
- var 키워드로 정의되는 변수형 저장 프로퍼티(멤버 변수라고 부름)
- let 키워드로 정의되는 상수형 저장 프로퍼티(멤버 상수라고 부름)
var 키워드로 정의한 멤버 변수는 값을 얼마든지 수정할 수 있는 반면, let 키워드로 정의한 멤버 상수는 최초에 할당된 값이 변경 없이 그대로 유지된다. 구조체에서의 저장 프로퍼티를 살펴볼 것이다.
// 고정 길이 범위 구조체
struct FixedLengthRange {
var startValue: Int // 시작값
let length: Int //값의 범위
}
// 가변 길이 범위 구조체
struct FlexibleLengthRange {
let startValue: Int // 시작값
var length: Int // 값의 범위
}
// 아래 구조체 인스턴스는 정수값 0, 1, 2를 의미한다.
var rangeOfFixedintegers = FixedLengthRange(startValue: 0, length: 3)
// 아래처럼 시작점을 변경하면 객체 인스턴스는 정수값 4, 5, 6을 의미하게 된다.
rangeOfFixedintegers.startValue = 4
// 아래 구조체 인스턴스는 정수값 0, 1, 2를 의미한다.
var rangeOfFlexibleIntegers = FlexibleLengthRange(startValue: 0, length: 3)
// 아래처럼 범위값을 변경하면 객체 인스턴스는 정수값 0, 1, 2, 3, 4를 의미하게 된다.
rangeOfFlexibleIntegers.length = 5
주의해야 할 점은 구조체 인스턴스를 상수에 할당할 경우이다. 인스턴스를 변수에 할당하면 구조체 내에서 변수로 정의한 저장 프로퍼티는 개발자가 원할 때 얼마든지 값을 수정할 수 있다.
// 변수에 할당된 구조체 인스턴스라면
var variablesOfInstance = FixedLengthRange(startValue: 3, length: 4)
// 아래와 같이 저장 프로퍼티를 수정할 수 있음
variablesOfInstance.startValue = 0 // 가능
// 반면, 상수에 할당된 구조체 인스턴스라면
let constantsOfInstance = FixedLengthRange(startValue: 3, length: 4)
// 아래와 같이 저장 프로퍼티를 수정하려고 하면 오류가 발생한다.
constantsOfInstance.startValue = 0 // 불가능
하지만 인스턴스를 상수에 할당하면 비록 구조체 내에서 저장 프로퍼티를 변수로 정의했더라도 값을 변경할 수 없다.
반면, 클래스는 클래스 인스턴스를 상수에 할당하더라도 클래스 내에서 변수로 선언한 저장 프로퍼티는 얼마든지 값을 수정할 수 있다.
이러한 차이는 구조체와 클래스의 값 전달 방식에 차이에서 비롯된다. 구조체는 값에 의한 전달 방식으로 인스턴스가 변수나 상수에 할당되고, 클래스는 참조에 의한 전달 방식으로 인스턴스의 레퍼런스가 변수나 상수에 할당되기 때문이다. 따라서 구조체는 저장 프로퍼티의 값이 바뀌면 상수에 할당된 인스턴스 전체가 변경되고, 클래스는 저장 프로퍼티의 값이 바뀌더라도 상수에 할당된 인스턴스 레퍼런스는 변경되지 않는다.
지연 저장 프로퍼티
일반적으로 저장 프로퍼티는 클래스 인스턴스가 처음 생성될 때 함께 초기화되지만, 저장 프로퍼티 정의 앞에 lazy라는 키워드가 붙으면 예외이다. 키워드에서 짐작할 수 있듯이, 이 키워드는 저장 프로퍼티의 초기화를 지연시킨다. 클래스 인스턴스가 생성되어 모든 저장 프로퍼티가 만들어지더라도 lazy 키워드가 붙은 프로퍼티는 선언만 될 뿐 초기화되지 않고 계속 대기하고 있다가 프로퍼티가 호출되는 순간 초기화 된다.
class OnCreate {
init() {
print("OnCreate!!")
}
}
class LazyTest {
var base = 0
lazy var late = OnCreate()
init() {
print("Lazy Test")
}
}
LazyTest 클래스 내부에 late라는 저장 프로퍼티를 선언하면 여기에 lazy 키워드를 붙여 지연 저장 프로퍼티로 만들었다. 이 프로퍼티의 초기값은 OnCreate 클래스의 인스턴스이다. 따라서 프로퍼티가 초기화될 때 OnCreate 클래스의 인스턴스가 만들어진다. lazyTest 클래스 역시 초기화될 때 LazyTest 라는 구문을 출력한다.
두 클래스를 정의한 이후 호출해 본 예제 코드이다.
let lz = LazyTest()
// Lazy Test
인스턴스가 초기화되면서 내부에 정의된 초기화 블록도 함께 실행되며 출력 구문이 표시된다. 하지만 아직 OnCreate 클래스의 출력 구문은 보이지 않는다. late 프로퍼티가 초기화되지 않았다는 뜻이다.
lz.late
// OnCreate!!
late 프로퍼티를 호출해야 이제서야 구문이 출력된다. 즉 지연 저장 프로퍼티에 대입된 인스턴스는 프로퍼티가 처음 호출되는 시점에서 생성된다는 것을 알 수 있다. 처음으로 호출이 발생할 때 값을 평가하여 초기화되며, 이후 두 번째 호출부터는 처음 초기화된 값을 그대로 사용할 뿐 다시 초기화되지는 않는다.
클로저를 이용한 저장 프로퍼티 초기화
저장 프로퍼티 중 일부는 연산이나 로직 처리를 통해 얻어진 값을 이용하여 초기화해야 하는 경우가 있다. 스위프트에서는 클로저를 사용하여 필요한 로직을 실행한 후 반환되는 값을 이용하여 저장 프로퍼티를초기화할 수 있도록 지원한다.
let/var 프로퍼티명: 타입 = {
정의 내용
return 반환값
}
이렇게 정의된 클로저 구문은 클래스나 구조체의 인스턴스가 생성될 때 함께 실행되어 초기값을 반환하고, 이후로는 해당 인스턴스 내에서 재실행되지 않는다. 비슷한 구문의 형식이지만 연산 프로퍼티가 참조될 때마다 매번 재평가된 값을 반환하는 것과 결정적으로 다르다. 아래는 예제 코드이다.
class PropertyInit {
// 저장 프로퍼티 - 인스턴스 생성 시 최초 한 번만 실행
var value01: String! = {
print("value01 execute")
return "value01"
}()
// 저장 프로퍼티 - 인스턴스 생성 시 최초 한 번만 실행
let value02: String! = {
print("value02 execute")
return "value02"
}()
}
let s = PropertyInit()
// 실행 결과
// value01 execute
// value02 execute
단순히 클래스의 인스턴스를 생성했을 뿐인데, 실행 결과에 두 개의 메세지가 출력된 것을 볼 수 있다. 각각 value01, value02 프로퍼티의 초기값을 대신하는 클로저 구문이다. 아래는 이 프로퍼티를 참조하는 예제 코드이다.
s.value01
s.value02
// 실행결과 - 없음
저장 프로퍼티를 단순히 참조만 하면 아무런 새로운 로그도 출력되지 않는다. 저장 프로퍼티에 정의된 클로저 구문이 더 이상 실행되지 않기 때문이다.
저장 프로퍼티는 클래스 인스턴스가 생성될 때 자동으로 값을 평가하므로 메모리 자원의 낭비로 이어질 수 있다. 이 때에는 앞에서 배운 lazy를 이용하면 된다. 클래스 인스턴스가 생성될 때 실행하는 것이 아니라 실제로 값을 참조하는 시점에 실행되고, 처음 한 번 실행되면 다시 값을 평가하지 않는다.
lazy var 프로퍼티명: 타입 = {
정의 내용
return 반환값
}
PropertyInit 에 이 구문을 추가 하여 코드를 실행한다.
class PropertyInit {
... 중략 ...
// 프로퍼티 참조 시 한번만 실행
lazy var value03: String! = {
print("value03 execute")
return "value03"
}()
}
let s1 = PropertyInit()
// 실행 결과
// value01 execute
// value02 execute
s1.value03
// 실행 결과
// value03 execute
콘솔에 value03에 대한 로그 메세지가 출력되었다. 초기화 클러작 실행된 것이다. 한 번 더 value03을 참조해 볼 것이다.
s1.value03
// 실행 결과 - 없음
두 번째 참조에서는 아무런 로그 메세지가 출력되지 않는다. 이처럼 lazy 키워드를 붙여서 정의한 저장 프로퍼티를 클로저 구문으로 초기화하면 최초 한 번만 로직이 실행되고 참조되는 시점에 초기화되어 메모리 낭비를 줄일 수 있다.
연산 프로퍼티
연산 프로퍼티(computed property)는 필요한 값을 제공한다는 점에서 저장 프로퍼티와 같지만, 실제 값을 저장했다가 반환하지는 않고 대신 다른 프로퍼티의 값을 연산 처리하여 간접적으로 값을 제공한다. 프로퍼티의 값을 참조하기 위해 내부적으로 get 구문을 사용하고, 선택적으로 set 구문을 추가할 수 있다. set 구문은 선택적이지만 get 구문은 필수적이다. 연산 프로퍼티는 항상 클래스나 구조체 또는 열거형 내부에서만 사용할 수 있다.
class/struct/enum 객체명 {
...
var 프로퍼티명: 타입 {
get {
필요한 연산 과정
return 반환값
}
set(매개변수명) {
필요한 연산 구문
}
}
}
연산 프로퍼티는 다른 프로퍼티에 의존적이거나, 혹은 특정 연산을 통해 얻을 수 있는 값을 정의할 때 사용한다. 대표적으로 나이가 이에 속한다. 나이는 출생 연도에 의존적이며, 현재 연도를 기준으로 계산해야 하기 때문이다. 아래는 예제 코드이다.
import Foundation
struct UserInfo {
// 저장 프로퍼티 : 태어난 연도
var birth: Int!
// 연산 프로퍼티 : 올해가 몇년도인지 계산
var thisYear: Int! {
get {
let df = DateFormatter()
df.dateFormat = "yyyy"
return Int(df.string(from: Date()))
}
}
// 연산 프로퍼티 : 올해 - 태어난 연도 + 1
var age: Int {
get {
return (self.thisYear - self.birth) + 1
}
}
}
let info = UserInfo(birth: 2000)
print(info.age)
// 실행 결과
// 25
이번에는 특정 사각형에 대한 정보를 저장하는 구조체에서 연산 프로퍼티를 사용하여 사각형의 중심 좌표를 구하는 예제이다.
struct Rect {
// 사각형이 위치한 기준 좌표(좌측 상단 기준)
var originX: Double = 0.0
var originY: Double = 0.0
// 가로 세로 길이
var sizeWidth: Double = 0.0
var sizeHeight: Double = 0.0
// 사각형의 X 좌표 중심
var centerX: Double {
get {
return self.originX + (sizeWidth / 2)
}
set (newCenterX){
originX = newCenterX - (sizeWidth / 2)
}
}
// 사각형의 Y 좌표 중심
var centerY: Double {
get{
return self.originY + (self.sizeHeight / 2)
}
set(newCenterY) {
self.originY = newCenterY - (self.sizeHeight / 2)
}
}
}
var square = Rect(originX: 0.0, originY: 0.0, sizeWidth: 10.0, sizeHeight: 10.0)
print("square.centerX = \(square.centerX), square.centerY = \(square.centerY)")
// 실행 결과
// square.centerX = 5.0, square.centerY = 5.0
사각형의 기준 좌표 x, y와 가로세로 길이는 모두 저장 프로퍼티로 정의된다. 그런데 사각형의 중심 좌표를 저장 프로퍼티로 정의하기는 좀 곤란하다. 이 값은 도형의 기준 좌표 x, y와 가로세로 길이의 관계에서 얻어지는 의존적인 속성이기 때문이다. 다시 말해 기준 좌표가 변경되거나 가로세로 길이가 변하면 그에 따라 중심 좌표가 변경된다.
연산 프로퍼티를 사용하지 않고 프로퍼티 값 하나하나를 받아 직접 계산할 수도 있다. 하지만 매번 중심 좌표를 구해야 한다면 같은 코드가 계속 사용이 되어야 할 것이다. 그 대신 연산 프로퍼티에 연산 구문을 정의해 놓으면 이 클래스를 사용하는 내내 중심 좌표를 구하기 위해 반복적으로 코드를 작성해야 하는 일은 없어질 것이다.
이번에는 두 개의 구조체를 정의하여 구조를 바꿔볼 것이다.
struct Position {
var x: Double = 0.0
var y: Double = 0.0
}
struct Size {
var width: Double = 0.0
var height: Double = 0.0
}
좌표는 X와 Y값이 항상 함께 있어야 의미가 있고, 크기 역시 가로와 세로가 함께 있는 것이 좋다. 이 때문에 각각 묶어 구조체를 정의하였고, 사각형 Rect 구조체의 모습도 다음과 같이 변경된다.
struct Rect {
// 사각형이 위치한 기준 좌표(좌측 상단 기준)
var origin = Position()
// 가로 세로 길이
var size = Size()
// 사각형의 X 좌표 중심
var center: Position {
get {
let centerX = self.origin.x + (self.size.width / 2)
let centerY = self.origin.y + (self.size.height / 2)
return Position(x: centerX, y: centerY)
}
set (newCenter){
self.origin.x = newCenter.x - (size.width / 2)
self.origin.y = newCenter.y - (size.height / 2)
}
}
}
let p = Position(x: 0.0, y: 0.0)
let size = Size(width: 10.0, height: 10.0)
var square = Rect(origin: p, size:size)
print("square.centerX = \(square.center.x), square.centerY = \(square.center.y)")
// 실행 결과
// square.centerX = 5.0, square.centerY = 5.0
center 프로퍼티의 set 구문을 살펴본다. 우리가 연산 프로퍼티에 값을 할당하면 여기에 정의된 구문이 실행된다. 프로퍼티에 할당된 값은 set 다음에 오는 괄호의 인자값으로 전달되는데, 이 때 인자값의 참조를 위해 매개변수가 사용된다. 앞의 예제를 본다면 newCenter가 매개변수의 이름인 것이다. 만약 매개변수의 이름이 생략되면 newValue라는 기본 인자명이 사용된다.
매개변수만 있고 타입이 없는 이유는 타입이 이미 앞에 정의되어 있기 때문이다.
이번에는 중심 좌표의 값을 변경할 것이다. 일반 프로퍼티에 값을 대입하는 것처럼 바꿀 중심 좌표를 적절한 타입으로 넣어주면 된다.
square.center = Position(x: 20.0, y: 20.0)
print("seqare.x = \(square.origin.x), square.y = \(square.origin.y)")
// 실행 결과
// seqare.x = 15.0, square.y = 15.0
center에 값을 할당하면 해당 인스턴스를 인자값으로 하는 set 구문이 실행된다. prigin 프로퍼티의 x, y 서브 프로퍼티의 값이 모두 바뀐다.
앞에서 정의한 center에 set 구문이 정의되어 있지 않으면 프로퍼티를 통해 값을 읽기만 할 뿐 할당은 불가능하다. 이를 읽기 전용 프로퍼티라고 한다.
var center: Position {
get {
let centerX = self.origin.x + (self.size.width / 2)
let centerY = self.origin.y + (self.size.height / 2)
return Position(x: centerX, y: centerY)
}
}
// 위 center와 같음(get 블록 생략 가능)
var center: Position {
let centerX = self.origin.x + (self.size.width / 2)
let centerY = self.origin.y + (self.size.height / 2)
return Position(x: centerX, y: centerY)
}
// 위와 기능 같음(함수로 표현)
func getCenter() -> Position {
let centerX = self.origin.x + (self.size.width / 2)
let centerY = self.origin.y + (self.size.height / 2)
return Position(x: centerX, y: centerY)
}
읽기 전용 프로퍼티는 get블록만 작성하면 되는데, 사실 get 블록을 생략하고 내용만 작성해도 된다. 또한 같은 기능을 매개변수가 없는 함수로도 표현 가능한데, 실무에서는 매개변수가 없는 함수 대신 읽기 전용 프로퍼티를 많이 사용하는 편이다.
프로퍼티 옵저버
프로퍼티 옵저버(Property Observer)는 특정 프로퍼티를 계속 관찰하고 있다가 프로퍼티의 값이 변경되면 이를 알아차리고 반응한다. 프로퍼티의 값이 설정되면 무조건 호출되며, 동일한 값이 재할당되더라도 호출된다. 프로퍼티 옵저버에는 willSet과 didSet 두 종류가 있다.
- willSet - 프로퍼티 값이 변경되기 직전에 호출되는 옵저버
- didSet - 프로퍼티의 값이 변경된 직후에 호출되는 옵저버
willSet 옵저버를 구현해 둔 프로퍼티에 값을 대입하면 그 값이 프로퍼티에 대입되기 직전에 willSet 옵저버가 실행된다. 이 때 프로퍼티에 대입되는 값이 옵저버의 실행 블록에 매개상수 형식으로 함께 전달된다. 프로퍼티의 값이 변경되기 전에 처리해야 할 뭔가가 있다면 이 값을 이용하여 처리하면 된다. 단 전달된 값은 참조할 수는 있지만, 수정할 수는 없다. 어쨌거나 상수 형태로 전달하는 값이기 때문이다. 매개상수에 이름을 부여해도, 하지 않아도 되는데 부여하지 않을 때는 매개상수 이름과 괄호를 모두 생략하면 된다. 생략한 경우에는 기본 상수명인 newValue라는 이름으로 전달되므로 이 상수를 이용해서 필요한 작업을 처리할 수 있다.
var <프로퍼티 명> : <타입> [ = <초기값>] {
willSet [ (<인자 명>) ] {
<프로퍼티 값이 변경되기 전에 실행할 내용>
}
}
문법 형식에서 대괄호 []에 둘러싸여 표시되는 부분은 생략이 가능한 부분이다. 실제 구문을 작성할 때에는 대괄호를 표시하지 않는다.
didSet의 경우는 프로퍼티에 값이 대입된 후 호출된다. 새로 할당된 값이 아닌 이전에 저장되어 있던 값이 매개상수 형태로 전달된다. 그 매개상수에 이름을 부여할 수 있지만 생략하더라도 oldValue라는 이름으로 자동 전달된다.
var <프로퍼티 명> : <타입> [ = <초기값> ] {
didSet [ (<인자 명>) ] {
<프로퍼티 값이 변경된 후에 실행할 내용>
}
}
willSet, didSet을 함께 구현해야 하는 것은 아니다. 한쪽 옵저버만 필요한 경우에는 나머지 하나는 구현할 필요 없이 선택적으로 구현하면 된다.
struct Job {
var income: Int = 0 {
willSet(newIncome) {
print("이번 달 월급은 \(newIncome)원 입니다.")
}
didSet {
if income > oldValue {
print("월급이 \(income - oldValue)원 증가하셨네요. 소득세가 상향조정될 예정입니다.")
} else {
print("저런, 월급이 삭감되셨군요. 그래도 소득세는 깎아드리지 않아요. 알죠?")
}
}
}
}
직장인의 월급을 주시하고 있다가 월급이 오르면 추가된 소득세를 챙겨가는 국세청을 프로퍼티 옵저버로 표현한 예제이다.
willSet 구문에서 newIncome으로 인자를 받아 income에 대입된다. didSet구문에는 파라미터가 없는데, 기본 파라미터인 oldValue를 사용하여 새로 대입된 income과 oldValue의 값을 비교하여 문장을 출력한다.
월급의 초기값을 백만 원으로 입력해서 Job 인스턴스를 생성한다.
var job = Job(income: 1000000)
월급이 백만 원인 Job 인스턴스가 생성되어 job 변수에 할당되었다. 상수가 아닌 변수에 할당한 것은 조금 후 income 프로퍼티의 속성을 변경하기 위해서이다. 월급을 올려볼 것이다.
job.income = 2000000
// 실행 결과
// 이번 달 월급은 2000000원 입니다.
// 월급이 1000000원 증가하셨네요. 소득세가 상향조정될 예정입니다.
실행 결과를 보면 willSet 구문이 먼저 실행되고 프로퍼티의 값이 변경된 뒤 didSet 구문이 실행된 것을 알 수 있다.
이번엔 월급을 삭감해볼 것이다.
job.income = 1500000
// 실행 결과
// 이번 달 월급은 1500000원 입니다.
// 저런, 월급이 삭감되셨군요. 그래도 소득세는 깎아드리지 않아요. 알죠?
동일하게 willSet -> 프로퍼티 값 변경 -> didSet 임을 알 수 있다.
타입 프로퍼티
저장 프로퍼티나 연산 프로퍼티는 클래스 또는 구조체 인스턴스를 생성한 후 이 인스턴스를 통해서만 참조할 수 있는 프로퍼티였다. 이는 이들 프로퍼티가 인스턴스에 관련된 값을 저장하고 다루므로 인스턴스 프로퍼티라고 부른다. 하지만 인스턴스에 관련된 값이 아니라 클래스나 구조체, 또는 열거형과 같은 객체 자체에 관련된 값을 다루어야 할 때도 있는데, 이 때는 인스턴스를 생성하지 않고 클래스나 구조체 자체에 값을 저장하게 되며 이를 타입 프로퍼티라고 부른다.
타입 프로퍼티는 인스턴스를 생성하지 않고 클래스나 구조체 자체에 저장하게 되며, 저장된 값은 모든 인스턴스가 공통으로 사용할 수 있다. 이 값은 복사된 값이 아니라 실제 하나의 값이므로 하나의 인스턴스에서 값을 변경하면 나머지 인스턴스들이 일괄적으로 변경된 값을 적용받는다. 따라서 모든 인스턴스들이 공유해야 하는 값을 정의할 때 유용하다.
타입 프로퍼티를 선언하는 방법은 클래스와 구조체에서 같다. 사용할 프로퍼티 앞에 static 키워드만 추가하면 된다. 타입 프로퍼티를 정의하는 또 다른 키워드인 class는 클래스에서 연산 프로퍼티에만 붙일 수 있는 키워드이다. 이 키워드를 사용하면 타입 프로퍼티를 선언하면 상속받은 하위 클래스에서 재정의할 수 있는 타입 프로퍼티가 된다.
static let/var 프로퍼티명 = 초기값
또는
class let/var 프로퍼티명: 타입 {
get {
return 반환값
}
set {
...
}
}
타입 프로퍼티는 인스턴스 생성 과정에서 초기값을 할당할 수 없으므로 반드시 초기값을 할당해야 한다. 자세한 예시는 아래 코드를 보면 된다.
struct Foo {
// 타입 저장 프로퍼티
static var sFoo = "구조체 타입 프로퍼티값"
// 타입 연산 프로퍼티
static var cFoo: Int {
return 1
}
}
class Boo {
// 타입 저장 프로퍼티
static var sBoo = "클래스 타입 프로퍼티값"
// 타입 연산 프로퍼티
static var cBoo: Int {
return 10
}
// 재정의가 가능한 타입 연산 프로퍼티
class var oBoo: Int {
return 100
}
}
하나 설명할 점은 class 키워들르 사용하여 정의한 oBoo 는 Boo 클래스를 상속받는 하위 클래스에서 재정의할 수 있는 프로퍼티라는 점이 cBoo와 다른 점이다. 이렇게 선언된 타입 프로퍼티들은 인스턴스 생성 없이 사용이 가능하고, 클래스나 구조체 자체에 점 구문을 이용하여 타입 프로퍼티를 참조하면 된다.
print(Foo.sFoo)
// 구조체 타입 프로퍼티 값
Foo.sFoo = "새로운 값"
print(Foo.sFoo)
// 새로운 값
print(Boo.sBoo)
// 클래스 타입 프로퍼티값
print(Boo.cBoo)
// 10
타입 프로퍼티는 인스턴스에 속하지 않으므로 만약 인스턴스를 생성한 다음 점 구문을 이용하여 타입 프로퍼티를 읽으려고 하면 선언되지 않은 프로퍼티라는 오류가 발생한다. 타입 프로퍼티는 반드시 클래스나 구조체, 또는 열거형 자체와 함께 사용해야 한다.
'swift' 카테고리의 다른 글
| 스위프트 상속 (1) | 2024.04.16 |
|---|---|
| 스위프트 메소드 (0) | 2024.04.16 |
| 스위프트 구조체와 클래스 (0) | 2024.04.15 |
| 스위프트 클로저 (0) | 2024.04.12 |
| 스위프트 일급 객체로서의 함수 (0) | 2024.04.12 |