- 저장 속성(Stored Properties)
값이 저장되는 일반적인 속성(변수)을 저장 속성이라고 함
struct Bird {
var name: String
var weight: Double
init(name: String, weight: Double) { // 기본값이 없으면, 생성자를 통해 값을 반드시 속성 값을 초기화해야함
self.name = name
self.weight = weight
}
func fly() {
print("날아갑니다.")
}
}
var aBird = Bird(name: "참새1", weight: 0.2)
aBird.name // 참새1
aBird.weight = 0.3
var bBird = Bird(name: "참새2", weight: 0.3)
- 저장 속성은 구조체와 클래스에서 동일하다.
- let(상수) 또는 var(변수)로 선언 가능 (만약 저장 속성을 let으로 선언하면 값을 바꿀 수 없음)
- 저장 속성(변수)은 각 속성 자체가 고유의 메모리 공간을 가짐 ⭐️
- 초기화 이전에 값을 가지고 있거나, 생성자 메서드를 통해 값을 반드시 초기화해야만 함.
- 지연(Lazy) 저장 속성
struct Bird1 {
var name: String
lazy var weight: Double = 0.2 // 지연 저장 속성
init(name: String) {
self.name = name
//self.weight = 0.2
}
func fly() {
print("날아갑니다.")
}
}
var aBird1 = Bird1(name: "새") // weight 속성 초기화 안됨
aBird1.weight // <============ 해당 변수에 접근하는 이 시점에 초기화됨 (메모리 공간이 생기고 숫자가 저장됨)
- 위의 weight이라는 지연 저장 속성은 초기화 시점에 메모리 공간이 생기는 것이 아님
- 예를 들어, 인스턴스가 생기고 난 후, aBird.weight 이렇게 접근하는 순간! 메모리 공간을 만들고 숫자를 저장하게 됨
이 코드의 생성자에서 self.weight = 0.2를 주석 처리한 이유는?
실제로 코드로 돌렸을 때 self.weight = 0.2 이런 식의 설정이 제대로 동작을 할 수도 있지만
그렇게 된다면 lazy로 선언하는 게 의미가 없기 때문!
lazy의 의도대로 선언했다면 속성 선언 시 기본값을 설정해야 하고, 생성자 내에서는 값을 설정하지 않아야 한다!
- 지연(lazy) 저장 속성 ===> 구조체, 클래스 동일
- 지연 저장 속성은 "해당 저장 속성"의 초기화를 지연시키는 것.
===> 즉, 인스턴스가 초기화되는 시점에 해당 속성이 값을 갖고 초기화되는 것이 아니라(메모리에 공간과 값을 갖는 것이 아니라),
해당 속성(변수)에 접근하는 순간에 (해당 저장 속성만) 개별적으로 초기화됨
- 생성자에서 초기화를 시키지 않기 때문에 "선언 시점에 기본값을 저장"해야 함
- 값을 넣거나, 표현식(함수 실행문)을 넣을 수 있음(모든 형태의 표현식)
- 함수 호출 코드, 계산 코드, 클로저 코드 등도 모두 가능 ===> 저장하려는 속성과 "리턴형"만 일치하면 됨
- 따라서, 상수로의 선언은 안되고 변수(var)로의 선언만 가능 ➡︎ lazy var만 가능(lazy let 불가능)
그렇다면, 지연 저장 속성을 사용하는 이유는?
class AView {
var a: Int
// 1) 메모리를 많이 차지할때
lazy var view = UIImageView() // 객체를 생성하는 형태
// 2) 다른 속성을 이용해야할때(다른 저장 속성에 의존해야만 할때)
lazy var b: Int = {
return a * 10
}()
init(num: Int) {
self.a = num
}
}
1) 메모리 공간을 많이 차지하는 이미지 등의 속성에 저장할 때
(반드시 메모리에 다 올릴 필요가 없으므로) 지연 저장 속성으로 선언한다. (메모리 낭비 막기 위함)
2) 다른 속성들을 이용해야 할 때
초기화 시점에 모든 속성들이 동시에 메모리 공간에 저장되므로 어떤 한 가지 속성이 다른 속성에 접근할 수가 없다.
그렇지만, 지연 저장 속성을 이용하는 경우 지연으로 저장된 속성은 먼저 초기화된 속성에 접근할 수 있게 된다.
- 저장 타입 속성
class Dog {
// static(고정적인/고정된)이라는 키워드를 추가한 저장 속성 - 저장 타입속성
static var species: String = "Dog"
var name: String
var weight: Double
init(name: String, weight: Double) {
self.name = name
self.weight = weight
}
}
let dog = Dog(name: "초코", weight: 15.0)
dog.name
dog.weight
//dog.s // =====> 인스턴스에서 .(점)을 찍어도 속성으로 보이지 않음
Dog.species // ====> 반드시 타입(형식)의 이름으로 접근해야함
class Circle {
// (저장) 타입 속성 (값이 항상 있어야 함)
static let pi: Double = 3.14
static var count: Int = 0 // 인스턴스를 (전체적으로)몇개를 찍어내는지 확인
// 저장 속성
var radius: Double // 반지름
// 생성자
init(radius: Double) {
self.radius = radius
Circle.count += 1
}
}
var circle1 = Circle(radius: 2) // 인스턴스를 +1 개 찍어냈다.
Circle.count // 1
var circle2 = Circle(radius: 3) // 인스턴스를 +1 개 찍어냈다.
Circle.count // 2
- 일반 저장 속성은 인스턴스를 생성할 때, 생성자에서 모든 속성을 초기화를 완료하고 해당 저장 속성은 각 인스턴스가 가진 고유한 값이다.
하지만, 저장 타입(형식) 속성은 생성자가 따로 없고 타입 자체(유형 그 자체)에 속한 속성이기 때문에 항상 기본값이 필요하다 (생략할 수 없음)
- 저장 타입 속성은 기본적으로 지연 속성 성격이다.(속성에 처음 접근하는 순간에 초기화됨)
(참고: 여러 스레드에서 동시에 액세스 하는 경우에도 한 번만 초기화되도록 보장됨. Thread-Safe)
- let 또는 var 둘 다 선언 가능
- 타입 속성은 특정 인스턴스에 속한 속성이 아니기 때문에 인스턴스 이름으로는 접근 불가
- 상속 시 재정의 불가
그렇다면, 어떤 경우에 타입 속성을 선언해야 하나?
➡︎ 모든 인스턴스가 동일하게 가져야 하는 속성이거나(해당 타입의 보편적인 속성), 모든 인스턴스가 공유해야 하는 성격에 가까운 경우!
- 속성 감시자
기본적으로 "속성 감시자"라는 이름이지만, 성격은 메서드 ➞ 저장 속성 감시
class Profile {
// 일반 저장 속성
var name: String = "이름"
var statusMessage: String = "기본 상태메세지" { // 저장 속성
willSet(message) { // 저장 속성이 변하는 시점을 관찰하는 메서드
print("메세지가 \(statusMessage)에서 \(message)로 변경될 예정입니다.")
}
}
}
let p = Profile()
p.name
p.name = "전지현"
p.statusMessage
p.statusMessage = "행복해"
p.statusMessage = "우울해"
저장 속성이 변하는 시점을 관찰, 저장 속성이 변하는 순간 ===> 딸려있는 메서드가 호출됨
속성 감시자는 새 값이 속성의 현재 값과 동일하더라도 속성 값이 설정될 때마다 호출됨
속성 감시자의 2가지 종류 - willSet / didSet
1) willSet은 값이 저장되기 직전에 호출됨
2) didSet은 새 값이 저장된 직후에 호출됨
class Profile1 {
// 일반 저장 속성
var name: String = "이름"
// var statusMessage: String {
// willSet(message) { // 바뀔 값이 파라미터로 전달
// print("메세지가 \(statusMessage)에서 \(message)로 변경될 예정입니다.")
// print("상태메세지 업데이트 준비")
// }
// didSet(message) { // 바뀌기 전의 과거값이 파라미터로 전달
// print("메세지가 \(message)에서 \(statusMessage)로 이미 변경되었습니다.")
// print("상태메세지 업데이트 완료")
// }
// }
// 파라미터의 생략 - oldValue / newValue (애플의 약속!)
var statusMessage = "기본 상태메세지" {
willSet {
print("메세지가 \(statusMessage)에서 \(newValue)로 변경될 예정입니다.")
print("상태메세지 업데이트 준비")
}
didSet {
print("메세지가 \(oldValue)에서 \(statusMessage)로 이미 변경되었습니다.")
print("상태메세지 업데이트 완료")
}
}
init(message: String) {
self.statusMessage = message
}
}
let profile1 = Profile1(message: "기본 상태메세지") // 초기화시, willSet/didSet이 호출되지는 않음
profile1.statusMessage = "기분 good"
- 클래스, 구조체, (열거형) 동일하게 적용
- 일반적으로는 willSet 또는 didSet 중에서 한 가지만 구현한다. (실제 프로젝트에서는 didSet을 많이 사용)
속성 감시자가 왜 필요할까?
변수 변하면, 변경 내용을 반영하고 싶을 때(업데이트) 유용하기 때문!
어떤 속성이 변하는 시점을 알아차리도록 시점에 제약을 만드는 코드를 짜기는 어렵다 ==> 그래서 실제 앱을 만들고 활용할 때 좋은 수단
속성 감시자를 추가 가능한 경우 ⭐️
- 1) 저장 속성 (원래, 상속한 경우 둘 다 가능)
- 2) 계산 속성 (상속해서 재정의하는 경우에만 가능) (단순 메서드 추가)
- 계산 속성의 경우, 속성 관찰자를 만드는 대신 계산 속성의 set블록에서 값 변경을 관찰할 수 있기 때문에
(재정의(상속)이 아닌 본래의 계산 속성에는 추가 불가)
- let(상수) 속성에는 당연히 추가 안됨 (값이 변하지 않으므로, 관찰할 필요가 없기 때문)
- 지연 저장 속성에 안됨
'iOS > Swift' 카테고리의 다른 글
메모리 누수가 발생하는 상황 : 강한 참조 사이클 + 해결방안? (0) | 2022.01.17 |
---|---|
힙 메모리 관리 - ARC 정리 (0) | 2022.01.13 |
클로저의 메모리 구조 + 캡처현상 / 캡처리스트 (2) | 2021.12.22 |
클래스&구조체 메모리 관점에서의 차이 (0) | 2021.12.17 |
[구조체&클래스] 속성 - 계산 속성, 계산 타입 속성 (0) | 2021.09.10 |