Stored Properties

Stored property : class / struct 의 instance 의 일부로 저장되는 상수, 변수

정의부에 stored property 에 기본값을 제공할 수 있음. 초기화 도중 stored property 의 초기값을 설정/변경 가능

struct FixedLengthRange {
    var firstValue: Int
    let length: Int // 생성된 후 변경 불가
}
var rangeOfThreeItems = FixedLengthRange(firstValue: 0, length: 3)
// the range represents integer values 0, 1, and 2
rangeOfThreeItems.firstValue = 6
// the range now represents integer values 6, 7, and 8

Stored Properties of Constant Structure Instances

Struct 인스턴스를 생성해서 상수에 할당했을 때는 인스턴스 property 가 variable 로 선언되었다해도 변경할 수 없음

let rangeOfFourItems = FixedLengthRange(firstValue: 0, length: 4)
// this range represents integer values 0, 1, 2, and 3
rangeOfFourItems.firstValue = 6
// ❗️ this will report an error, even though firstValue is a variable property

이는 struct 가 value 타입이기 때문. value type 의 인스턴스가 상수로 표기된 경우 property 도 변경할 수 없음.

Class 의 경우 reference 타입이기 때문에 인스턴스를 constant 에 할당해도 인스턴스의 property 를 바꿀 수 있음

Lazy Stored Properties

처음 사용되기 전까지 initial value 가 연산되지 않은 property. lazy modifier 를 써서 표시

Lazy property 는 항상 variable 로 선언해야 함. 인스턴스 초기화가 끝나기까지 초기값을 가지지 않기 때문. Constant property 는 항상 초기화가 끝나기 전까지 항상 값을 가지기 때문에 lazy 로 선언할 수 없음

초깃값을 초기화가 끝나기 전까지 값을 알 수 없는 외부의 값에 의존하는 property 에 유용함. 또한 필요하지 않은 한 수행할 필요없는 복잡한 연산이 초깃값 설정에 필요한 경우 유용.

class DataImporter {
    /*
    DataImporter is a class to import data from an external file.
    The class is assumed to take a nontrivial amount of time to initialize.
    */
    var filename = "data.txt"
    // the DataImporter class would provide data importing functionality here
}


class DataManager {
	// 초기화 하는데 많은 시간이 필요한 경우
    lazy var importer = DataImporter()
    var data: [String] = []
    // the DataManager class would provide data management functionality here
}


let manager = DataManager()
manager.data.append("Some data")
manager.data.append("Some more data")
// the DataImporter instance for the importer property hasn't yet been created

// importer prperty 가 처음 접근됐을 때 인스턴스가 생성됨
print(manager.importer.filename) 
// the DataImporter instance for the importer property has now been created 
// Prints "data.txt".

lazy modifier 로 표기된 프로퍼티가 여러 thread 에서 동시에 접근됐는데 property 가 아직 초기화되지 않은 경우 property 가 한 번만 초기화 될 것이라는 보장이 없음

Stored Properties and Instance Variables

(Obj-C vs Swift property storage design)

Class 인스턴스의 일부로 값/참조를 저장하는 두 가지 방법이 obj-c 에 존재.

  1. property
  2. instance variable 을 property 에 저장될 값들을 위한 backing store 로 활용

Swift property 는 대응되는 instance variable 이 없고, property 를 위한 backing store 가 직접적으로 접근되지 않음. 이런 접근은 다른 context 들에서 값이 어떻게 접근되는지에 대한 혼란을 없애고 property 의 정의를 하나의 문장으로 간단하게 만듦.

objc

@interface Person : NSObject {
    NSString *_name;   // instance variable
}
@end
@property (nonatomic, strong) NSString *name;

@synthesize name = _name; // ivar 가 필요

즉 하나의 값을 위해 두 개의 다른 정의가 필요하게 되는 셈

@property declaration
        
backed by ivar (_name)

self.name      // uses getter/setter methods
_name          // direct ivar access (bypasses logic)

Computed Properties

Class, struct, enum 은 computed property 를 가질 수 있음.

Computed property : 실제로 값을 저장하고 있지 않음. Getter 와 setter(optional) 을 제공해서 값을 indirect 하게 값을 가져오고 설정함

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()
    
    // computed property
    // origin 과 size 에 의해 결정되기 때문에 명시적인 Point 값으로 값을 저장할 필요 없음
    var center: Point {
	    // getter, setter 를 통해 center 가 마치 stored property 인 마냥 작업 가능
        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 square = Rect(origin: Point(x: 0.0, y: 0.0),
    size: Size(width: 10.0, height: 10.0))

// getter 가 연산됨
let initialSquareCenter = square.center
// initialSquareCenter is at (5.0, 5.0)

// setter 가 호출됨
square.center = Point(x: 15.0, y: 15.0)
print("square.origin is now at (\(square.origin.x), \(square.origin.y))")
// Prints "square.origin is now at (10.0, 10.0)".

Shorthand Setter Declaration

Computed property 의 setter 가 새로 설정되는 값에 대한 이름을 정하지 않았을 때 기본 이름 newValue 가 사용됨

struct AlternativeRect {
    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)
        }
    }
}

Shorthand Getter Declaration

Getter 의 전체 body 가 하나의 expression 일 때 getter 는 암묵적으로 해당 expression 을 return 함.

struct CompactRect {
    var origin = Point()
    var size = Size()
    var center: Point {
        get {
            Point(x: origin.x + (size.width / 2),
                  y: origin.y + (size.height / 2))
        }
        set {
            origin.x = newValue.x - (size.width / 2)
            origin.y = newValue.y - (size.height / 2)
        }
    }
}

Read-Only Computed Properties

Setter 없는 computed property. Read-Only property 는 항상 값을 리턴할 수 있지만 다른 값으로 설정할 수 없음

struct Cuboid {
    var width = 0.0, height = 0.0, depth = 0.0
    var volume: Double {
        return width * height * depth
    }
}

let fourByFiveByTwo = Cuboid(width: 4.0, height: 5.0, depth: 2.0)
print("the volume of fourByFiveByTwo is \(fourByFiveByTwo.volume)")
// Prints "the volume of fourByFiveByTwo is 40.0".

Property Observers

Property observer 는 property 의 값을 관찰해서 변화에 반응함. Property 의 값이 새로 set 될 때마다 호출되며, 새로운 값이 이전 값과 같더라도 호출됨.

Property observer 는 다음 상황에서 추가 가능

상속한 property 에 대해서 상속한 클래스에서 해당 property 를 overriding 해서 property observer 를 추가할 수 있음. 직접 정의한 computed property 의 경우 property 의 setter 를 사용해서 property observer 역할을 할 수 있음.

다음의 observer 를 property 에 정의할 수 있음

Superclass 의 willSet, didSet observer 는 property 가 superclass 의 initializer 가 호출된 후에 subclass 의 initializer 에서 설정됐을 때 호출됨. Class 가 initializer body 에서 자신의 property 를 설정하고 있을 때는 observer 가 호출되지 않음

class StepCounter {
    var totalSteps: Int = 0 {
        willSet(newTotalSteps) {
            print("About to set totalSteps to \(newTotalSteps)")
        }
        didSet {
            if totalSteps > oldValue  {
                print("Added \(totalSteps - oldValue) steps")
            }
        }
    }
}
let stepCounter = StepCounter()
stepCounter.totalSteps = 200
// About to set totalSteps to 200
// Added 200 steps
stepCounter.totalSteps = 360
// About to set totalSteps to 360
// Added 160 steps
stepCounter.totalSteps = 896
// About to set totalSteps to 896
// Added 536 steps

Observer 를 가진 property 를 함수의 in-out parameter 로 전달할 경우 willSet, didSet observer 는 항상 호출됨. 이는 in-out parameter 의 copy-in copy-out 메모리 모델 때문. 값은 항상 함수의 끝에서 property 에 다시 쓰여짐. (함수 안에서 아무것도 안해도 호출됨)

Property Wrappers

Property wrapper 는 해당 property 가 어떻게 저장되는지를 관리하는 코드와 property 를 정의하는 코드 간에 separation layer 를 추가함.

‘Adds a layer of separation between code that manages how a property is stored(in wrapper) and the code that defines a property(inside my class)’

=>

Property Wrapper O Property Wrapper X
Property + 기타 로직이 함께 섞임 Property 정의 부분이 깔끔해짐
반복되는 코드 Logic 은 한 번만 작성
유지보수하기 어려움 재사용 용이

e.g. 모든 property 가 thread-safe 해야 하는 상황에서, 아래와 같이 모든 property 에 locking 하는 code 를 작성해야 함. Property 를 작성할 때마다 이 logic 을 반복해서 작성해야 함.

class Example {
    private let lock = NSLock()
    private var _count: Int = 0
    
    var count: Int {
        get {
            lock.lock()
            defer { lock.unlock() }
            return _count
        }
        set {
            lock.lock()
            defer { lock.unlock() }
            _count = newValue
        }
    }
}

Property 는 반복되는 logic 을 하나의 reusable type 으로 만들어서 아래와 같이 작성할 수 있게 해줌

@propertyWrapper
struct ThreadSafe<Value> {
    private var value: Value
    private let lock = NSLock()

    init(wrappedValue: Value) {
        self.value = wrappedValue
    }

    var wrappedValue: Value {
        get {
            lock.lock()
            defer { lock.unlock() }
            return value
        }
        set {
            lock.lock()
            defer { lock.unlock() }
            value = newValue
        }
    }
}

class Example {
    @ThreadSafe var count = 0
    @ThreadSafe var name = "YJ"
}

wrappedValue property 를 정의하는 struct/enum/class 를 만들어서 property wrapper 정의.

@propertyWrapper
struct TwelveOrLess {
	// private 으로 정의돼서 내부에서만 사용가능하도록 함
    private var number = 0
    // wrappedValue 의 getter 와 setter 를 통해 간접적으로 접근할 수 있게 함
    var wrappedValue: Int {
        get { return number }
        set { number = min(newValue, 12) }
    }
}

Attribute 로 property 이름 전에 wrapper 의 이름을 써서 property 에 wrapper 적용함.

struct SmallRectangle {
    @TwelveOrLess var height: Int
    @TwelveOrLess var width: Int
}


var rectangle = SmallRectangle()
print(rectangle.height)
// Prints "0".


rectangle.height = 10
print(rectangle.height)
// Prints "10".


rectangle.height = 24
print(rectangle.height)
// Prints "12".

Property 에 wrapper 를 추가하면 compiler 는 wrapper 를 위한 storage 를 제공하는 코드와 wrapper 를 통해 property 에 대한 접근을 제공하는 코드를 동기화함. => compiler 가 자동으로 코드를 아래와 같이 변환해줌

// 이렇게 작성했을 때 swift는 바로 저장하는게 아니라,
@TweleveOrLess var height: Int

// compiler 가 아래와 같은 코드로 변환함
private var _height = TwelveOrLess()
var height: Int {
	get { _height.wrappedValue }
	set { _height.wrappedValue = newValue }
}

‘The property wrapper is responsible for storing the wrapped value, so there’s no syntesized code for that’ => wrapper 가 이미 storage 를 가지고 있기 때문에 실제 값을 위한 storage 를 생성하지 않음.

struct TwelveOrLess {
    private var number = 0 // 이미 값이 여기서 저장되고 있음 (storage)
    var wrappedValue: Int {
        get { number }
        set { number = min(newValue, 12) }
    }
}

// 따라서 아래와 같이 코드를 작성했을 때 오직 _height 만 생성되지 height 를 위한 storage 는 생성되지 않음
@TweleveOrLess var height: Int

Property wrapper 문법을 직접 사용하지 않고서도 같은 동작을 하게 코드를 작성할 수 있음

struct SmallRectangle {
    private var _height = TwelveOrLess()
    private var _width = TwelveOrLess()
    var height: Int {
        get { return _height.wrappedValue }
        set { _height.wrappedValue = newValue }
    }
    var width: Int {
        get { return _width.wrappedValue }
        set { _width.wrappedValue = newValue }
    }
}

Setting Initial Values for Wrapped Properties

Property 에 initializer 를 추가해서 초깃값 등을 설정할 수 있음

@propertyWrapper
struct SmallNumber {
    private var maximum: Int
    private var number: Int

    var wrappedValue: Int {
        get { return number }
        set { number = min(newValue, maximum) }
    }

    init() {
        maximum = 12
        number = 0
    }
    init(wrappedValue: Int) {
        maximum = 12
        number = min(wrappedValue, maximum)
    }
    init(wrappedValue: Int, maximum: Int) {
        self.maximum = maximum
        number = min(wrappedValue, maximum)
    }
}

struct ZeroRectangle {
	// 기본 init() 사용
    @SmallNumber var height: Int
    @SmallNumber var width: Int
}

var zeroRectangle = ZeroRectangle()
print(zeroRectangle.height, zeroRectangle.width)
// Prints "0 0".

struct UnitRectangle {
	// init(wrappedValue: Int) 사용
    @SmallNumber var height: Int = 1
    @SmallNumber var width: Int = 1
}

var unitRectangle = UnitRectangle()
print(unitRectangle.height, unitRectangle.width)
// Prints "1 1".

struct NarrowRectangle {
	// init(wrappedValue: Int, maximum: Int) 사용
    @SmallNumber(wrappedValue: 2, maximum: 5) var height: Int
    @SmallNumber(wrappedValue: 3, maximum: 4) var width: Int
}


var narrowRectangle = NarrowRectangle()
print(narrowRectangle.height, narrowRectangle.width)
// Prints "2 3".

narrowRectangle.height = 100
narrowRectangle.width = 100
print(narrowRectangle.height, narrowRectangle.width)
// Prints "5 4".

struct MixedRectangle {
    @SmallNumber var height: Int = 1
    // init(wrappedValue: 2, maximum: 9) 사용
    @SmallNumber(maximum: 9) var width: Int = 2
}

Projecting a Value From a Property Wrapper

Property wrapper 는 projected value 를 정의해서 추가 기능을 가질 수 있음. Projected vlaue 의 이름은 wrapped value 와 같지만 dollar sign ($) 이 붙음.

@propertyWrapper
struct SmallNumber {
    private var number: Int
    private(set) var projectedValue: Bool

    var wrappedValue: Int {
        get { return number }
        set {
            if newValue > 12 {
                number = 12
                projectedValue = true
            } else {
                number = newValue
                projectedValue = false
            }
        }
    }

    init() {
        self.number = 0
        self.projectedValue = false
    }
}
struct SomeStructure {
    @SmallNumber var someNumber: Int
}
var someStructure = SomeStructure()

someStructure.someNumber = 4
print(someStructure.$someNumber) // wrapper 의 projected vlaue 에 접근
// Prints "false".

someStructure.someNumber = 55
print(someStructure.$someNumber)
// Prints "true".

Property wrapper 는 projected value 로 어떤 타입이든 return 가능. 만약 더 많은 정보를 노출해야 하는 필요가 있으면 projected value 로 다른 인스턴스를 return 하거나 self 를 노출할 수 있음

@propertyWrapper
struct Clamped {
	private var value: Int
	var range: ClosedRange<Int> = 0...10
	
	var wrappedValue: Int {
		get { value }
		set { value = min(newValue, value) }
	}
	
	var projectedValue: Clamped { self }
}

struct Box {
	@Clamped var height: Int = 5
}

let box = Box()
box.$height.range // 이렇게 더 세부적인/많은 정보에 접근 가능

Global and Local Variables

Property observer 는 모두 global variable 에도 적용 가능함.

Global constants / variable 는 항상 lazy 하게 연산됨. Lazy stored property 와는 다르게 lazy modifier 가 필요하지 않음. Local constants / variables 는 절대로 lazy 하게 연산되지 않음

Property wrapper 는 global variable, computed property, constants 에 적용될 수 없음

Global variable 에 property wrapper 적용하게 되면 (Property wrappers are not yet supported in top-level code) compile error 발생

func someFunction() {
    @SmallNumber var myNumber: Int = 0

    myNumber = 10
    // now myNumber is 10

    myNumber = 24
    // now myNumber is 12
}

Type Properties

Instance property 의 경우 특정 type 의 instance 에 속하는 property 였음. 해당 type 의 새로운 instance 를 생성할때마다 다른 instance 와는 독립적인 자신만의 property 값을 가짐

Type property : 타입 자체에 속하고, instance 에 속하지 않는 property. Type property 는 생성된 instance 의 개수와는 상관 없이 오직 하나의 copy 만 생성됨. 특정 type 의 모든 instance 에서 사용 가능한 값을 정의해야 하는 경우 유용함.

인스턴스 property 와는 다르게 stored type property 의 경우 default 값을 항상 줘야 함. 이는 type 자체는 초기화때 값을 할당할 수 있는 initializer 를 갖고 있지 않기 때문.

Stored type property 는 처음으로 접근됐을 때 lazy 하게 초기화됨. 여러 thread 에서 동시에 접근되더라도 오직 한 번만 초기화하도록 보장되며 lazy modifier 로 표시되지 않아도 됨

Type Property Syntax

static 키워드로 type property 를 정의함. Class type 의 computed type property 에는 class 키워드를 써서 subclass 가 superclass 의 구현을 overriding 할 수 있게 할 수 있음

struct SomeStructure {
    static var storedTypeProperty = "Some value."
    static var computedTypeProperty: Int {
        return 1
    }
}
enum SomeEnumeration {
    static var storedTypeProperty = "Some value."
    static var computedTypeProperty: Int {
        return 6
    }
}
class SomeClass {
    static var storedTypeProperty = "Some value."
    static var computedTypeProperty: Int {
        return 27
    }
    class var overrideableComputedTypeProperty: Int {
        return 107
    }
}

Querying and Setting Type Properties

Dot syntax 를 통해 접근되고 값을 설정할 수 있음

print(SomeStructure.storedTypeProperty)
// Prints "Some value."
SomeStructure.storedTypeProperty = "Another value."
print(SomeStructure.storedTypeProperty)
// Prints "Another value."
print(SomeEnumeration.computedTypeProperty)
// Prints "6".
print(SomeClass.computedTypeProperty)
// Prints "27".