Apple 제공 Swift 프로그래밍 가이드(3.0.1)의 Properties 부분을 공부하며 정리한 글입니다. 개인적인 생각도 조금 들어가있습니다.
들어가며
프로퍼티는 특정 클래스, 구조체, ENUM과 값을 연결해준다. Stored Property는 상수/변수값을 인스턴스의 한 부분으로 저장할 수 있게 해주고, Computed Property는 값을 저장하는 게 아니라 그때그때 계산해서 반환할 수 있게 해준다. Computed Property는 클래스, 구조체, ENUM이 가질 수 있고 Stored Property의 경우는 오직 클래스와 구조체만 가질 수 있다.
인스턴스가 아니라 타입 그 자체와 연결되는 Type Property라는 것도 있다.
또한 프로퍼티의 값이 변하는 것을 모니터링할 수 있도록 Property Observer를 정의할 수도 있다. Property Observer는 직접 정의한 Stored Property는 물론이고 부모클래스에서 상속받은 Property도 감시하게 할 수 있다.
(1) Stored Properties
Stored 프로퍼티는 간단히 설명하면 특정 클래스나 구조체의 인스턴스의 한 부분이 되는 상수/변수이다.
var 키워드나 let 키워드로 클래스/구조체 안에서 정의 (ENUM에서는 X)
Default Property Value : 정의할 때 default 값을 정의할 수 있다.
클래스/구조체의 initialization 과정에서 stored 프로퍼티의 값을 셋팅/변경할 수 있다. 심지어 let으로 선언한 constant stored 프로퍼티도 가능. (단, default값이 없을 때만. 즉 값을 최초로 한 번 할당하면 바꿀 수 없다는 말이다)
struct FixedLengthRange {
var firstValue: Int
let length: Int
}
var rangeOfThreeItems = FixedLengthRange(firstValue: 0, length: 3)
// constant stored property인 length는 default 값을 가지지 않는다
// 따라서 initialization과 함께 값을 셋팅할 수 있는 것이다
rangeOfThreeItems.firstValue = 6 // var니까 나중에 변경 가능
<Stored Properties of Constant Structure Instances>
구조체는 Value type이다. 따라서 구조체가 var로 선언되면 내부 프로퍼티의 값은 변경될 수 있고, let으로 선언되면 내부 프로퍼티의 값은 변경될 수 없다. 내부 프로퍼티가 var로 선언되어도 변경될 수 없다.
반대로 클래스의 경우에는 Reference type이므로 var, let 어느 것으로 선언되든지간에 내부 프로퍼티의 값은 변경할 수 있다.
class TestClass {
var name: String = ""
}
struct FixedLengthRange {
var firstValue: Int
let length: Int
var testClass:TestClass
}
let rangeOfThreeItems = FixedLengthRange(firstValue: 0,
length: 3, testClass: TestClass())
rangeOfThreeItems.firstValue = 6
// error (구조체가 let으로 선언되었으므로 내부 프로퍼티의 값을 변경불가)
// 추가설명
rangeOfThreeItems.testClass.name = "ss" // not error
//(구조체가 let으로 선언되었으므로 내부값 변경불가.
// 따라서 testClass의 값 자체는 변할 수 없지만,
// testClass가 reference type이기 때문에
// testClass 내부 프로퍼티인 name의 값은 변경가능)
<Lazy Stored Properties>
Lazy Stored Property는 실제로 사용되기 전까지는 값이 계산되지 않는 프로퍼티이다. lazy 키워드를 사용해 정의한다. 인스턴스 초기화 시점에서는 값을 알 수 없거나, 값 계산에 많은 시간이 걸릴 때 사용하면 좋다.
<note> lazy 프로퍼티는 항상 변수가 되어야 한다(var 키워드). 상수 프로퍼티(let 키워드)는 초기화 과정에서 반드시 값이 정해져야 하기 때문이다.
class DataImporter {
// 외부 파일에서 데이터를 가져오는 클래스.
// 데이터를 메모리에 올릴 때 엄청난 시간이 걸림
var fileName = "data.txt"
}
class DataManager {
lazy var importer = DataImporter()
var data = [String]()
}
let manager = DataManager()
manager.data.append("Some data")
manager.data.append("Some more data")
// 이 시점에서는 importer 프로퍼티가 아직 생성되지 않음
print(manager.importer.fileName)
// 이 때 비로소 importer 프로퍼티가 생성된다
<note> lazy 프로퍼티에 동시에 여러 쓰레드가 접근한다면, 이 프로퍼티가 오직 한 번만 초기화 된다는 보장은 없다.
<Stored Properties and Instance Variables>
Objective-C에서는 프로퍼티 외에도 인스턴스 변수(내부변수)를 선언하여 값을 저장할 수 있었다. @synthesize 키워드를 사용하여 프로퍼티와 내부변수를 대응시키거나 하기도 했지만, Swift에서는 아예 프로퍼티 하나에 이런 개념을 통합시켜버렸다. 즉 프로퍼티에 대응하는 내부변수가 없고 컴파일러가 알아서 처리를 해준다. 서로 다른 맥락에서 값이 접근되는 방식에 대한 혼란을 방지하기 위함이다.
(2) Computed Properties
Computed 프로퍼티는 실제로 값을 저장하고 있는 게 아니라 getter를 통해 값을 계산해 반환해주고, setter를 통해 값의 계산을 위한 요소에 영향을 끼치는 프로퍼티다.
- 클래스/구조체/ENUM 안에서 var키워드로 정의 가능 (let 키워드는 X)
- getter는 필수지만 setter는 선택적으로 정의
struct Point {
var x = 0.0, y = 0.0
}
struct Size {
var width = 0.0, height = 0.0
}
struct Rect {
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(newCenter) {
origin.x = newCenter.x - (size.width / 2)
origin.y = newCenter.y - (size.height / 2)
}
}
}
var point = Point(x: 0.0, y: 0.0)
var size = Size(width: 10.0, height: 10.0)
var square = Rect(origin: point, size: size)
square.center = Point(x: 15.0, y: 15.0)
// center의 setter에 의해서 origin은 이제 (0.0, 0.0) -> (10.0, 10.0)이 된다
<Shorthand Setter Declaration>
Computed 프로퍼티의 setter에서 변수이름을 지정하지 않아도 newValue라는 디폴트 이름을 사용할 수 있다.
set {
origin.x = newValue.x - (size.width / 2)
origin.y = newValue.y - (size.height / 2)
}
<Read-Only Computed Properties>
setter가 없고 getter만 있는 Computed 프로퍼티를 정의할 수 있다. 이 경우 getter라는 키워드를 다음과 같이 생략할 수 있다.
struct Rect {
var origin = Point()
var size = Size()
var center: Point {
let centerX = origin.x + (size.width / 2)
let centerY = origin.y + (size.height / 2)
return Point(x: centerX, y: centerY)
}
}
Property Observers
프로퍼티 옵저버는 프로퍼티의 값이 변화할 때를 감지할 수 있다. 프로퍼티의 값이 set 될 때마다 프로퍼티 옵저버가 매번 호출된다. 따라서 새로운 값이 이전값과 같을 때에도 호출된다.
- 모든 stored 프로퍼티에 옵저버 추가 가능하지만 lazy stored 프로퍼티는 제외(why?? -> 추측만 하고있다. http://stackoverflow.com/questions/29543537/why-cant-property-observers-be-added-to-lazy-properties 를 참조)
- 상속해서 오버라이드한 프로퍼티에도 옵저버 추가 가능 (stored, computed 모두)
- 오버라이드되지 않은 computed 프로퍼티는 어차피 getter/setter를 통해 값이 변화하는 시점을 알 수 있으므로 옵저버가 딱히 필요없음
프로퍼티 옵저버는 두 가지 종류가 있다.
- willSet : 값이 set 되기 전에 호출됨. 새롭게 set 될 값이 매개변수로 넘어옴. 매개변수 이름은 지정하지 않으면 디폴트로 newValue
- didSet : 값이 set 된 후에 호출됨. set 되기 이전 값이 매개변수로 넘어옴. 매개변수 이름은 지정하지 않으면 디폴트로 oldValue
class StepCounter {
var totalSteps: Int = 0 {
willSet(newTotalSteps) {
print("willSet : totalSteps를 \(newTotalSteps)로 셋팅할 것이다")
}
didSet {
print("didSet : totalSteps를 \(oldValue)에서 \(totalSteps)로 셋팅했다")
}
}
}
let stepCounter = StepCounter()
stepCounter.totalSteps = 200
// willSet : totalSteps를 200로 셋팅할 것이다
// didSet : totalSteps를 0에서 200로 셋팅했다
stepCounter.totalSteps = 360
// willSet : totalSteps를 360로 셋팅할 것이다
// didSet : totalSteps를 200에서 360로 셋팅했다
<note> willSet 과 didSet 옵저버는 클래스의 initialize 과정에서 프로퍼티값을 set할 때는 호출되지 않지만, 해당 클래스를 상속받은 서브클래스의 initializer 과정에서 프로퍼티가 set될 때는 호출이 된다.
class StepCounter {
var totalSteps: Int = 0 {
willSet {
print("willSet : totalSteps를 \(newValue)로 셋팅할 것이다")
}
didSet {
print("didSet : totalSteps를 \(oldValue)에서 \(totalSteps)로 셋팅했다")
}
}
}
class PenaltyStepCounter: StepCounter {
override init() {
super.init()
self.totalSteps = 1000
// 여기서 옵저버가 불린다.
//willSet : totalSteps를 1000로 셋팅할 것이다
// didSet : totalSteps를 0에서 1000로 셋팅했다
}
}
let stepCounter = PenaltyStepCounter()
<note> 옵저버가 붙어있는 프로퍼티를 in-out 파라미터로 함수에 넘기면 willSet과 didSet은 항상 불린다. 심지어 함수 안에서 값을 바꾸지 않아도 불린다. in-out 파라미터의 copy-in copy-out 메모리 모델 때문에 함수가 종료될 때 프로퍼티에 다시 값이 쓰여지기 때문이다. 자세한 것은 나중 챕터에서 설명한다.
class StepCounter {
var totalSteps: Int = 0 {
willSet(newTotalSteps) {
print("willSet : totalSteps를 \(newTotalSteps)로 셋팅할 것이다")
}
didSet {
print("didSet : totalSteps를 \(oldValue)에서 \(totalSteps)로 셋팅했다")
}
}
}
func doNothing(inout currentSteps:Int) {
}
let stepCounter = StepCounter()
doNothing(&(stepCounter.totalSteps))
// willSet : totalSteps를 0로 셋팅할 것이다
// didSet : totalSteps를 0에서 0로 셋팅했다
Global and Local Variables
- 글로벌 변수와 로컬변수에도 옵저버를 추가할 수 있다. (Stored 변수일 때)
- 글로벌변수와 로컬변수도 Computed 변수로 정의할 수 있다. (Computed 프로퍼티와 동일한 방식이 된다)
- 글로벌상수/변수는 항상 지연 계산이 된다. Lazy Stored 프로퍼티와 같은 방식이다. (로컬상수/변수는 절대 지연 계산이 되지 않는다) (lazy stored 프로퍼티는 옵저버를 붙일 수 없는데, 똑같이 lazy computed가 되는 글로벌변수에는 왜 옵저버를 붙일 수 있을까?)
Type Properties
특정 타입에 대한 각각의 인스턴스에 속하는 인스턴스 프로퍼티와 달리, 타입 그 자체에 속하는 프로퍼티를 타입 프로퍼티라고 한다. C에서의 static 상수/변수같은 역할을 할 수 있다. 간단히 말하면 클래스 프로퍼티, 구조체 프로퍼티, ENUM 프로퍼티 이렇게 생각하면 된다.
- stored 타입 프로퍼티는 let/var 모두, computed 타입 프로퍼티는 var만 정의 가능 (인스턴스 프로퍼티와 같다)
- stored 타입 프로퍼티는 반드시 default 값 필요 (타입 자체는 initializer가 없기 때문이다. stored 인스턴스 프로퍼티는 default값이 없어도 된다)
- stored 타입 프로퍼티는 lazily initialized 되며 동시에 여러 쓰레드에서 접근해도 오직 한 번만 초기화 된다는 것이 보장된다 (stored 인스턴스 프로퍼티는 보장이 안 된다)
- stored 타입 프로퍼티는 ENUM에 대해서도 정의 가능 (stored 인스턴스 프로퍼티는 안 됐다)
<Type Property Syntax>
타입 프로퍼티는 static 키워드를 붙여서 정의한다. 클래스에 대한 Computed 타입 프로퍼티에 class 키워드를 붙이면, 서브클래스에게 해당 타입 프로퍼티의 오버라이드를 허락할 수 있다.
struct SomeStructure {
static var storedTypeProperty = "Some value."
static var computedTypeProperty: Int {
return 1
}
}
enum SomeEnumeration {
static var storedTypeProperty = "Some values."
static var computedTypeProperty: Int {
return 6
}
}
class SomeClass {
static var storedTypeProperty = "Some values."
static var computedTypeProperty: Int {
return 27
}
class var overrideableComputedTypeProperty: Int {
return 101
}
}
// 추가설명
class SomeChildClass: SomeClass {
override static var computedTypeProperty: Int {
// Compile error
return 1
}
override static var overrideableComputedTypeProperty: Int {
// OK
return 1
}
}
SomeClass.overrideableComputedTypeProperty // 101
SomeChildClass.overrideableComputedTypeProperty // 1
<Querying and Setting Type Properties>
- 타입 프로퍼티는 타입.프로퍼티이름 으로 get/set 한다.
- 타입 프로퍼티에도 옵저버를 추가할 수 있다.
- 프로퍼티 옵저버 안에서 해당 프로퍼티에 값을 셋팅하는 경우, 옵저버가 다시 호출되지는 않는다.
'Swift 공식 가이드 > Swift 3' 카테고리의 다른 글
Subscripts (0) | 2017.03.01 |
---|---|
Methods (0) | 2017.03.01 |
Classes and Structures (0) | 2017.02.27 |
Enumerations (0) | 2017.02.27 |
Closures (0) | 2017.02.21 |