티스토리 뷰

Swift 공식 가이드/Swift 3

Properties

찜토끼 2017. 2. 27. 16:46

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
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
«   2024/03   »
1 2
3 4 5 6 7 8 9
10 11 12 13 14 15 16
17 18 19 20 21 22 23
24 25 26 27 28 29 30
31
글 보관함