티스토리 뷰
(프로퍼티를 속성이라고 불렀습니다.)
INDEX
1. 저장 속성(Stored) / 지연(lazy)저장 속성(Properties)
2. 계산속성(computed Properties)
3. 타입속성(Type Properties)
4. 속성 감시자(Property Observers)
5. 마무리
1. 저장속성(Stored) / 지연(lazy)저장속성
1-1 저장 속성(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
aBird.weight = 0.3
1-2 지연 저장 속성(Lazy Stored Properties)
인스턴스를 생성시 저장속성에 필연적으로 값이 필요 없다면 옵셔널타입으로 선언해줄 수 있었다. 하지만 그것과는 조금 다른 용도로 필요할 때 지연 저장 속성을 사용한다.
lazy키워드를 붙이며 변수선언만 가능하기에 lazy var 통채로 외워놓는게 편하다. ex) lazy var num: Int = 1
why lazy var? 지연 저장속성은 처음 사용되기 이전에는 값을 갖지 않는다.
지연 저장 속성은 해당 저장 속성의 초기화를 지연시킨다.
즉 일반적인 초기화 방식(인스턴스가 초기화되는 시점에 해당 속성이 값을 갖고 초기화되어 메모리 공간과 값을 갖는것)이
아니라, 해당 지연 저장 속성(변수)에 접근하는 순간에 개별적으로 초기화된다. 코드를 보면 무슨 말인지 알 수 있다.
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 // <= 해당 변수에 접근하는 이 시점에 초기화된다! (메모리 공간이 생기고 숫자가 저장됨)
지연 저장 속성은 언제 사용할까?
클래스 인스턴스의 저장 프로퍼티로 다른 클래스 인스턴스나 구조체 인스턴스를 할당해야 하는 다소 복잡한 상황이 발생할 수도 있다.
이럴 때 인스턴스를 초기화하면서 저장 프로퍼티로 쓰이는 인스턴스들이 한 번에 생성되어야 한다면?
굳이 모든 저장 프로퍼티를 사용할 필요가 없다면?
위 질문의 답이 지연 저장 프로퍼티 사용이다.
지연 저장 속성을 사용하는 이유는?
1) 메모리를 많이 차지할때
: 꼭 인스턴스 생성시, 메모리에 한번에 다 올릴 필요는 없을때 지연 저장 속성으로 선언한다
=> 메모리 낭비 막을 수 있다.
2) 다른 속성을 이용해야할때 (접근성에 볼드)
: 아래 코드와 같이 다른 저장 속성에 의존해야만 하는 상황
초기화 시점에서는 속성 서로간의 접근이 불가능. But 지연 저장 속성으로는 먼저 초기화된 속성에 접근 가능
=> 속성간의 접근이 가능해진다.
class AView {
var a: Int
// 1) 메모리를 많이 차지할때. UIImageView()는 이미지(image)
lazy var view = UIImageView() // 객체를 생성하는 형태
// 2) 다른 속성을 이용해야할때(다른 저장 속성에 의존해야만 할때)
lazy var b: Int = {
return a * 10
}()
init(num: Int) {
self.a = num
}
}
2. 계산속성
2. 계산 속성(Computed Properties):⭐️ 메서드(함수)의 기능을 하는 속성(변수)⭐️
계산속성 생성시 주의점
1. 계산속성은 get블록은 반드시 선언 해야한다.(값을 얻는 것은 필수, 값을 set하는 것은 선택)
2. 항상 변하는 값이므로, var로 선언해야한다 (let로 선언불가)
3. 자료형 선언을 해야한다(함수의 기능(파라미터를 받고 리턴형이 필요)을 하기 때문이다)
계산속성은 getter와 setter가 등장한다. 편의상 get블록, set블록이라고 하겠다.
계산속성이라는 것이 왜 필요할까?
인스턴스 외부에서 메서드를 통해 인스턴스 내부 값에 접근하려면 메서드를 두 개(접근자, 설정자)를 구현해야 한다. 이는 하나의 메서드로 가능한 걸 2개로 분산해 구현한다는 점에서 코드의 가독성을 떨어뜨리고, 직관적이지 못하다. 아래의 코드를 통해 그 불편함과 필요성을 느껴보자.
class Person {
var name: String = "사람"
var height: Double = 160.0
var weight: Double = 60.0
func calculateBMI() -> Double {
let bmi = weight / (height * height) * 10000
return bmi
}
}
let p = Person()
p.height = 165 // 키 165
p.weight = 65 // 몸무게 65
p.calculateBMI() // 23.875
BMI를 계산하는 위 산식은 파라미터가 없고,내부에 가지고 있는 저장 속성값을 접근해서 이용해,
계산한 후 결과값을 리턴한다. 하지만 이를 계산 속성을 이용한다면?
//get블록(getter)만
class Person1 {
var name: String = "사람"
var height: Double = 160.0
var weight: Double = 60.0
var bmi: Double {
//get블록(getter)
get { // (getter) ===> 값을 얻는다는 의미
return weight / (height * height) * 10000
}
}
// var bmi2: Double {
// return weight / (height * height) * 10000
// }
//get블록만 있다면(읽기전용 계산속성)이라면, bmi2처럼 get 생략 가능
}
let p1 = Person1()
p1.height = 165 // 키 165
p1.weight = 65 // 몸무게 65
p1.bmi // 함수가 아닌 (계산)속성으로 얻어낸 값
항상 다른 저장 속성에 접근하고 계산하여 그 결과로 구현되는 방식의 메서드인 경우 계산속성을 사용한다.
위의 사례는 값셋팅이 불가한 읽기 전용 계산속성(get-only property). 이를 읽기/쓰기가 가능한 계산속성으로 변경하면?
//set블록
class Person2 {
var name: String = "사람"
var height: Double = 160.0
var weight: Double = 60.0
var bmi: Double {
get {
let bmi = weight / (height * height) * 10000
return bmi
}
//set블록(setter)
set(bmi) { //setter ===> 값을 세팅한다(넣는다)는 의미
weight = bmi * height * height / 10000
}
}
// 만약에 쓰기 계산속성(set)을 메서드로 구현했다면
// func setWeightWithBMI(bmi: Double) {
// weight = bmi * height * height / 10000
// }
}
let p2 = Person2()
p2.height = 165 // 키 165
p2.weight = 65 // 몸무게 65
p2.bmi
p2.bmi = 25
p2.weight
set블록의 newValue
set블록의 파라미터를 생략하고 'newValue'라는 키워드로 대체가능
class Person4 {
var name: String = "사람"
var height: Double = 160.0
var weight: Double = 60.0
var bmi: Double {
get {
let bmi = weight / (height * height) * 10000
return bmi
}
set {
weight = newValue * height * height / 10000 //newValue
}
}
아래는 공식문서의 예제.
struct Point {
var x = 0.0, y = 0.0
}
struct Size {
var width = 0.0, height = 0.0
}
//Rect는 원점과 크기로 사각형 정의
struct ARect {
var origin = Point()
var size = Size()
var center: Point {
//get블록과 set블록
get {
let centerX = origin.x + (size.width / 2)
let centerY = origin.y + (size.height / 2)
return Point(x: centerX, y: centerY)
}
set(newCenter) {
origin.x = newCenter.x - (size.width / 2)
origin.y = newCenter.y - (size.height / 2)
}
}
}
//위코드의 set블록에서 newValue파라미터 이용하면?
struct BRect {
var origin = Point()
var size = Size()
var center: Point {
get {
let centerX = origin.x + (size.width / 2)
let centerY = origin.y + (size.height / 2)
return Point(x: centerX, y: centerY)
}
set {
origin.x = newValue.x - (size.width / 2)
origin.y = newValue.y - (size.height / 2)
}
}
}
3. 타입속성
타입속성: 타입자체에 영향을 미치는 속성(프로퍼티)를 말하며 static키워드로 선언한다.
주의점
1. let, var둘다 선언이 가능하다 ex) static let pi = 3.14, static var count = 0
2. 인스턴스의 이름으로 접근불가하다.
:인스턴스의 생성 여부와 상관없이 타입 프로퍼티의 값은 하나이며, 그 타입은 모든 인스턴스가 공통으로 사용하는 값(C 언어의 static constant와 유사), 모든 인스턴스에서 공용으로 접근하고 값을 변경할 수 있는 변수(C 언어의 static 변수와 유사) 등을 정의할 때 유용하다. 아래코드를 보면
class Dog{
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.species // 반드시 타입(형식)의 이름으로 접근
저장 타입 속성과 계산 타입 속성
저장 타입 속성
1. 변수, 상수 선언 가능
2. 반드시 기본값 설정, 지연연산 된다.
3. 다중 스레드 환경이라도 단 한번만 초기화 된다.
계산 타입 속성
변수로만 선언가능
//⭐️저장 타입 속성
class Circle{
//저장 타입 속성
static let pi: Double = 3.14
static var count: Int = 0
var radius: Double
var diameter: Double{
get { return radius * 2}
set { radius = newValue / 2}
}
init(radius: Double) {
self.radius = radius
Circle.count += 1
}
}
var myCircle = Circle(radius: 2.5)
myCircle.radius
myCircle.diameter = 10.6
myCircle.radius
Circle.pi
Circle.count
// 실제 사용 예시 -> 타입프로퍼티로 접근
Int.max
Int.min
Double.pi
//⭐️계산 타입 속성
class Circle1 {
// 저장 타입 속성
static let pi: Double = 3.14
static var count: Int = 0
var radius: Double
//계산 타입 속성
static var multiPi: Double {
return pi * 2
}
// 생성자
init(radius: Double) {
self.radius = radius
Circle.count += 1
}
}
var toCircle = Circle1(radius: 10)
Circle1.multiPi // 6.28
타입 속성의 메모리구조 이해하기
일반적인 저장속성은 인스턴스 생성시, 생성자의 초기화로 값이 저장되고, 해당 저장속성은 각 인스턴스가 가지는 고유한 값이다.
하지만 타입 저장 속성은 생성자가 따로 존재하지 않고, 타입(Type)자체에 속한 속성이기에, 선언시 기본값까지 들어가 있어야 한다.
지연속성의 성격을 지녔다? => 타입속성에 처음 접근하는 순간에 초기화되기 때문이다.
어떤 경우에 타입 속성을 선언해야 할까?
모든 인스턴스가 동일하게 가져야하는 속성이거나 공유해야하는 성격에 가까운 속성일때
상속시 재정의(overriding)
1. 저장 타입 속성은 재정의가 불가능
2. 계산 타입 속성은 상위클래스에서 static대신 class키워드를 붙였을때 재정의 가능
4. 속성 감시자(Property Observers)
: 저장속성(변수)의 변하는 시점을 관찰하며 일반적으로는 willSet과 didSet중 한가지만 구현한다.
willset과 didSet의 호출시점은?
저장 속성이 변하는 순간 willSet과 didset메서드가 호출되는데,
willSet메서드는 값이 저장되기 직전, didSet 메서드는 값이 변경된 직후에 호출된다.
willSet메서드와 didSet메서드에 전달되는 argument는 어떤 차이가 있을까?
willSet메서드에 전달되는 argument는 현시점의 프로퍼티가 변경될 값이다
didSet메서드에 전달되는 argument는 현시점의 프로퍼티로 변경되기 이전의 값이다.
willSet메서드와 didSet메서드가 기본파라미터로 가지는 것은?
두 메서드에 parameter 이름을 지정해주지 않으면, willSet은 newValue, didSet은 oldValue를 기본 파라미터로 가진다.
(보통 파라미터이름을 지어도 willSet(new로시작하는값) didSet(old로시작하는값)로 하는 경우가 많다.)
속성 감시자는 주로 언제 사용할까?
실제 프로젝트에서 오류를 찾아내거나 바뀐 값이 무엇일지 확인해야할때, 변수가 바뀔때 변경내용을 반영하고자할때
속성 감시자의 적용을 받는 건 왜 (변수)저장속성뿐일까?
우선 값이 불변하는 상수는 관찰불가하고 지연저장속성 기본적인 정의와 접근의 관점에서 보면 관찰을 할 수 없다.
계산속성은 본래의 계산속성은 관찰불가하며(어차피 계산속성은 set블록으로 관찰가능하다), 상속을 받은 계산속성에는 적용가능하다.
//(생성자 X, willSet만)
class Profile{
var name: String = "이름"
var statusMessage:String = "기본 상태메세지" { //기본값 할당
willSet(newMessage) {
print("메세지가 \(statusMessage)에서 \(newMessage)로 변경될 예정입니다.")
print("메세지 업데이트 준비")
}
}
}
let a = Profile()
a.name
a.name = "전지현"
//속성 감시자는 새 값이 속성의 현재값과 동일하더라도, 속성값이 설정되면 호출된다.
a.statusMessage = "행복해" //메세지가 기본 상태메세지에서 행복해로 변경될 예정입니다. 메세지 업데이트 준비
a.statusMessage = "행복해" //메세지가 기본 상태메세지에서 행복해로 변경될 예정입니다. 메세지 업데이트 준비
//생성자의 파라미터로 willSet과 didSet
class Profile1 {
// 일반 저장 속성
var name: String = "이름"
var statusMessage: String {
willSet(newMessage) { // 바뀔 값이 파라미터로 전달
print("메세지가 \(statusMessage)에서 \(newMessage)로 변경될 예정입니다.")
print("상태메세지 업데이트 준비")
}
didSet(oldMessage) { // 바뀌기 전의 과거값이 파라미터로 전달
print("메세지가 \(oldMessage)에서 \(statusMessage)로 이미 변경되었습니다.")
print("상태메세지 업데이트 완료")
}
}
init(message: String) {
self.statusMessage = message
}
}
let profile1 = Profile1(message: "기본 상태메세지") // ❗️초기화시, willSet/didSet이 호출되지는 않음❗️
profile1.statusMessage = "기분 좋아졌으"
//메세지가 기본 상태메세지에서 기분 좋아졌으로 변경될 예정입니다.상태메세지 업데이트 준비.
//메세지가 기본 상태메세지에서 기분 좋아졌으로 이미 변경되었습니다. 상태메세지 업데이트 완료
//위코드를 파라미터 생략후 - oldValue / newValue
class Profile2 {
// 일반 저장 속성
var name: String = "이름"
var statusMessage = "기본 상태메세지" {
willSet {
print("메세지가 \(statusMessage)에서 \(newValue)로 변경될 예정입니다.") //newValue
print("상태메세지 업데이트 준비")
}
didSet {
print("메세지가 \(oldValue)에서 \(statusMessage)로 이미 변경되었습니다.") //oldValue
print("상태메세지 업데이트 완료")
}
}
}
let a2 = Profile2()
a2.name
a2.name = "전지현"
a2.statusMessage
a2.statusMessage = "행복해"
//메세지가 기본 상태메세지에서 행복해로 변경될 예정입니다. 상태메세지 업데이트 준비
//메세지가 기본 상태메세지에서 행복해로 이미 변경되었습니다. 상태메세지 업데이트 완료
5. 마무리
지금까지 총 5가지의 속성(프로퍼티)에 관해서 정리해 보았다.
각 INDEX별로 기억에 남아야 할 것들은
저장속성은 쉽게 떠올릴 수 있을것이고 초기화를 해야한다는 점,
지연저장속성은 lazy를 붙이고, 메모리에 올라가는 시점이 다르고 속성간의 접근이 가능해진다는 점,
계산속성은 함수의 기능을하는 속성으로 get블럭과 set블럭을 통한 bmi를 구하는 예제,
타입속성은 static키워드를 사용해 인스턴스의 이름으로 접근 불가한 타입자체의 프로퍼티라는 점,
마지막으로 속성 감시자(프로퍼티옵져버)는 willSet과 didSet메서드로 저장속성의 값이 바뀌는 시점을 알 수 있다는 점, 기본 파라미터로
newValue, oldValue가 있었다는 점을 기억하고 간단히 위 속성들을 정의 할 수 있다면 PASS
출처
Inflearn Allen님의 강의 노트
Swift 공식문서
야곰님의 스위프트 프로그래밍 3판
'Swift' 카테고리의 다른 글
Swift [상속과 재정의(Inheritance/Overriding)] (0) | 2023.02.18 |
---|---|
Swift [Methods, Subscripts, Access Control, Singleton] (0) | 2023.02.17 |
Swift [구조체와 클래스 (Structures and Classes) 이해하기] (0) | 2023.02.13 |
Swift [열거형(Enumerations)] (0) | 2023.02.12 |
Swift [Collection Types] (0) | 2023.02.10 |