본문 바로가기
Swift 공식 가이드/Swift 3

Properties

by 토끼찌짐 2017. 2. 27.

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