Stored property : class / struct 의 instance 의 일부로 저장되는 상수, 변수
varlet정의부에 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
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 를 바꿀 수 있음
처음 사용되기 전까지 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 가 한 번만 초기화 될 것이라는 보장이 없음
(Obj-C vs Swift property storage design)
Class 인스턴스의 일부로 값/참조를 저장하는 두 가지 방법이 obj-c 에 존재.
Swift property 는 대응되는 instance variable 이 없고, property 를 위한 backing store 가 직접적으로 접근되지 않음. 이런 접근은 다른 context 들에서 값이 어떻게 접근되는지에 대한 혼란을 없애고 property 의 정의를 하나의 문장으로 간단하게 만듦.
@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)
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)".
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)
}
}
}
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)
}
}
}
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 observer 는 property 의 값을 관찰해서 변화에 반응함. Property 의 값이 새로 set 될 때마다 호출되며, 새로운 값이 이전 값과 같더라도 호출됨.
Property observer 는 다음 상황에서 추가 가능
상속한 property 에 대해서 상속한 클래스에서 해당 property 를 overriding 해서 property observer 를 추가할 수 있음. 직접 정의한 computed property 의 경우 property 의 setter 를 사용해서 property observer 역할을 할 수 있음.
다음의 observer 를 property 에 정의할 수 있음
willSet : 값이 저장되기 직전에 호출됨. 새로운 값이 상수 parameter 로 전달됨. 만약 parameter 의 이름을 명시적으로 쓰지 않았다면 default parameter 이름 newValue 로 접근 가능didSet : 새로운 값이 저장된 직후에 호출됨. 이전 값을 constant property 로 넘기게 됨. oldValue 라는 default parameter name 을 갖고 있음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 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 }
}
}
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
}
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 // 이렇게 더 세부적인/많은 정보에 접근 가능
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
}
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 로 표시되지 않아도 됨
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
}
}
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".