-
PropertiesiOS/Swift 공식문서 2022. 2. 18. 20:33
Stored Properties
클래스나 구조체 인스턴스의 한 부분으로서 저장되는 상수/변수를 말한다. 클래스/구조체를 정의할 때 값을 할당하여 기본값을 설정할 수 있다.
struct FixedLengthRange { var firstValue: Int = 1 let length: Int } var rangeOfThreeItems = FixedLengthRange(firstValue: 0, length: 3) rangeOfThreeItems.firstValue = 6
인스턴스 초기화를 통해서도 프로퍼티 값을 바꿀 수 있는데, 이 경우 상수 프로퍼티도 값을 바꿀 수 있다.
rangeOfThreeItems = FixedLengthRange(firstValue: 6, length: 4)
Stored Properties of Constant Structure Instances
구조체 인스턴스를 상수로 선언한 경우 변수 프로퍼티도 값을 바꿀 수 없다. 구조체의 경우 value type이기 때문이다. 따라서 클래스에게는 적용되지 않는다. 클래스 인스턴스는 상수로 선언하더라도 변수 프로퍼티는 값을 바꿀 수 있다.
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
Lazy Stored Properties
처음으로 사용되기 전까지 계산되지 않는 프로퍼티를 말한다. 프로퍼티 선언 앞에
lazy
키워드를 붙여서 사용할 수 있다. 처음 사용되기 전에는 값을 갖지 않기 때문에 반드시 변수로 선언해야 한다.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
위 코드에서는 아직
manager
변수의importer
프로퍼티가 사용되지 않아서DataImporter
인스턴스가 생성되지 않았다. 아래 코드에서 처음으로 사용함으로써 인스턴스가 생성된다.print(manager.importer.filename) // the DataImporter instance for the importer property has now been created // Prints "data.txt"
note
여러 스레드에 의해 접근되는 경우 인스턴스가 한 번만 생성된다는 보장이 없으므로 주의하자.
Computed Properties
값을 저장하지 않고 접근할 때마다 계산하는 프로퍼티를 말한다. getter와 선택적으로 setter를 제공하여 다른 프로퍼티나 값에 간접적으로 접근할 수 있게 한다.
아래 코드의 경우
center
라는 프로퍼티의 경우 getter의 경우origin
프로퍼티로부터 값을 계산하여 반환하고, setter를 통해center
값을 설정하는 경우origin
프로퍼티의 값이 바뀌는 것을 확인할 수 있다.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) } } }
다음과 같이 getter를 통해
center
값을 반환받을 수 있다.var square = Rect(origin: Point(x: 0.0, y: 0.0), size: Size(width: 10.0, height: 10.0)) let initialSquareCenter = square.center // initialSquareCenter is at (5.0, 5.0)
다음과 같이 할당 연산자를 통해 setter를 호출할 수 있다. 우변의
Point
인스턴스가 setter의 매개변수newCenter
의 값으로 들어가서origin
프로퍼티의 값을 바꾸게 된다.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
매개변수 이름을 따로 명시하지 않는 경우,
newValue
라는 기본 이름을 사용할 수 있다. 아래 예시에서는 setter의newValue
가 이전 예시의newCenter
의 역할을 하는 것을 알 수 있다.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이라면, 암시적으로 해당 expression을 리턴한다.
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
computed property 중 getter만 제공하는 경우를 말한다. setter가 없어서 다른 값으로 설정하는 것이 불가능하다.
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
값이 바뀌는 이벤트를 감지하기 위해 사용한다. 프로퍼티 값이 set될 때마다 호출하며, 새 값이 이전 값과 같을 때에도 마찬가지다. 프로퍼티 옵저버를 사용할 수 있는 프로퍼티는 다음과 같다.
a. 클래스/구조체에서 선언한 stored property
b. 클래스/구조체에서 상속받은 stored property
c. 클래스/구조체에서 상속받은 computed property상속받은 프로퍼티의 경우 오버라이딩을 통하여 옵저버를 추가할 수 있다. computed property의 경우 옵저버를 만드는 대신, setter를 사용하여 프로퍼티를 관찰할 수 있다.
프로퍼티 옵저버의 종류는 다음과 같다.
a.
willSet
는 값이 바뀌기 직전 호출된다.
b.didSet
은 값이 바뀐 직후 호출된다.willSet
의 경우 프로퍼티의 새로운 값이 상수 인자값으로 넘겨진다. 매개변수 이름을 명시할 수도 있고, 명시하지 않고 괄호도 없다면,newValue
라는 기본 이름을 통해 접근할 수 있다.didSet
의 경우 이전 프로퍼티 값이 상수 인자값으로 넘겨진다. 매개변수 이름을 명시하지 않는 경우oldValue
라는 기본 이름으로 접근할 수 있다.note
superclass의 옵저버들은 subclass의 프로퍼티가 initializer 호출로 인해 값이 설정될 때 호출된다. 이 경우 superclass의 initializer가 호출된 이후 호출된다. superclass의 initilaizer가 subclass가 자신의 프로퍼티 값을 설정하는 경우는 호출되지 않는다.
다음 예시를 보면 프로퍼티에 값이 할당될 때마다
willSet
과didSet
이 호출되는 것을 관찰할 수 있다.didSet
의 경우 매개변수 이름을 명시하지 않았기 때문에 기본 이름인oldValue
를 사용해 이전 값에 접근한다.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
note
프로퍼티를 in-out parameter로 넘겨주는 경우
willSet
과didSet
는 항상 호출된다. 이는 in-out parameter에 적용되는 copy-in copy-out memory model 때문이다. 항상 함수 호출이 끝날 때 값이 쓰여진다.Property Wrappers
프로퍼티를 저장하는 부분과 프로퍼티를 정의하는 코드를 분리하는 layer 역할을 한다. 예를 들어 Thread-safety check나 DB에 데이터를 저장하는 기능을 제공하는 프로퍼티들의 경우, 해당 코드를 모든 프로퍼티에 대하여 작성해야 한다. property wrapper를 사용하면, 이러한 기능을 하는 코드를 한번만 작성하여 여러 프로퍼티에 재사용할 수 있다.
property wrapper를 정의하기 위해서는,
@propertyWrapper
어노테이션을 사용하고,wrappedValue
프로퍼티를 사용하는 열거형, 구조체, 혹은 클래스를 만들어야 한다. 다음 구조체의 경우 감싸는 값이 12 이하의 값을 갖도록 한다. 더 큰 값을 저장하려 해도 12를 저장한다.@propertyWrapper struct TwelveOrLess { private var number = 0 var wrappedValue: Int { get { return number } set { number = min(newValue, 12) } } }
해당 열거형/구조체/클래스 이름을 통해 프로퍼티에 wrapper를 적용할 수 있다. 다음 예시에서 height, width 모두 12보다 큰 값을 할당해도 12를 저장하게 된다.
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"
프로퍼티에 wrapper를 적용하면, 컴파일러가 값을 저장하는 코드와 프로퍼티에 접근하는 코드를 합성한다. 다음과 같이 attribute syntax의 이점을 사용하지 않고도 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 wrapper의 initializer를 사용하여 감싸지는 프로퍼티의 초깃값을 설정할 수 있다. 다음 예시에서는 구조체에 여러 initailizer가 있다.
@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) } }
초깃값을 명시하지 않는 경우
init()
이 호출되어 wrapper를 생성한다.struct ZeroRectangle { @SmallNumber var height: Int @SmallNumber var width: Int } var zeroRectangle = ZeroRectangle() print(zeroRectangle.height, zeroRectangle.width) // Prints "0 0"
할당 연산자로 초깃값을 설정하는 경우
init(wrappedValue:)
가 호출된다.struct UnitRectangle { @SmallNumber var height: Int = 1 @SmallNumber var width: Int = 1 } var unitRectangle = UnitRectangle() print(unitRectangle.height, unitRectangle.width) // Prints "1 1"
custom attribute 다음 괄호 안에 인자들을 적으면, 그 인자들에 해당되는 initializer가 호출된다. 이 방식이 가장 일반적인 방식이다.
struct NarrowRectangle { @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"
인자들 중
wrapperValue
의 경우 할당 연산으로 값을 설정할 수 있다.struct MixedRectangle { @SmallNumber var height: Int = 1 @SmallNumber(maximum: 9) var width: Int = 2 } var mixedRectangle = MixedRectangle() print(mixedRectangle.height) // Prints "1" mixedRectangle.height = 20 print(mixedRectangle.height) // Prints "12"
Projecting a Value From a Property Wrapper
property wrapper는 projected value를 정의함으로써 추가적인 기능을 노출할 수도 있다. projected value의 값은 wrapped value의 이름 앞에
$
을 붙인 것이다. 아래의 코드를 살펴보자. property wrapper 안에서projectedValue
라는 프로퍼티를 선언하여wrappedValue
의 setter에서 접근하고 있다.@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 } }
앞에서 설명한 것과 같이 projected value에 접근할 경우 wrapper가 적용된 프로퍼티 이름 앞에
$
을 붙이면 된다. 어느 타입이라도 projected value로 사용할 수 있다. 다음 예시와 같이 projected value는 추가적인 정보를 보여주기 위해 사용할 수 있다.struct SomeStructure { @SmallNumber var someNumber: Int } var someStructure = SomeStructure() someStructure.someNumber = 4 print(someStructure.$someNumber) // Prints "false" someStructure.someNumber = 55 print(someStructure.$someNumber) // Prints "true"
프로퍼티의 getter나 인스턴스 메서드 등의 커스텀 타입 내부의 코드에서 projected value에 접근할 경우,
self
를 생략할 수 있다.enum Size { case small, large } struct SizedRectangle { @SmallNumber var height: Int @SmallNumber var width: Int mutating func resize(to size: Size) -> Bool { switch size { case .small: height = 10 width = 20 case .large: height = 100 width = 100 } return $height || $width } }
Global and Local Variables
전역 변수는 모든 함수, 메서드, 클로져, 혹은 타입 컨텍스트의 밖에서 정의된 함수이다. 지역 변수는 함수, 메서드, 클로져 안에서 정의된 함수를 말한다. 프로퍼티를 계산하거나 관찰할 때의 기능들을 전역/지역 변수에서도 사용할 수 있다. 전역/지역 변수들도 stored 뿐만 아니라 computed variable로도 정의할 수 있다. 또한 stored variable의 경우 옵저버를 정의할 수 있다.
note
전역 상수/변수는 항상 lazy하게 계산된다. 따라서 프로퍼티처럼
lazy
키워드를 적을 필요가 없다. 반면 지역 상수/변수는 절대 lazy하게 계산되지 않는다.local stored variable의 경우 property wrapper를 적용할 수 있다. 전역 변수나 computed variable의 경우 불가능하다.
func someFunction() { @SmallNumber var myNumber: Int = 0 myNumber = 10 // now myNumber is 10 myNumber = 24 // now myNumber is 12 }
Type Properties
인스턴스 프로퍼티는 각 인스턴스마다 값을 가지는 프로퍼티이다. 반면, 타입 자체마다 하나씩 가지는 프로퍼티도 존재하는데, 이를 타입 프로퍼티라 한다. 타입의 모든 인스턴스가 공통으로 가지는 값을 정의할 때 유용하다. 인스턴스 프로퍼티처럼 stored의 경우 상수/변수 모두 가능하고, computed의 경우 변수만 가능하다.
note
타입 프로퍼티는 항상 기본값을 할당해야 한다. 타입 자체는 initializer가 없어서 초기화 시 값을 할당할 수 없기 때문이다. stored인 타입 프로퍼티는 lazy하게 초기화되어 처음 접근할 때 초기화된다. 따라서
lazy
키워드가 필요 없다. 여러 스레드가 동시에 접근할 때도 한 번만 초기화되는게 보장된다.Type Property Syntax
프로퍼티 선언 앞에
static
키워드를 적으면 된다. computed인 타입 프로퍼티의 경우 오버라이딩을 허용하려면static
대신class
키워드를 적으면 된다.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
다른 언어와 마찬가지로
(타입 이름).(프로퍼티 이름)
으로 접근하면 된다.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"
Reference
https://docs.swift.org/swift-book/LanguageGuide/Properties.html
'iOS > Swift 공식문서' 카테고리의 다른 글
Subscripts (0) 2022.02.19 Methods (0) 2022.02.19 Structures and Classes (0) 2022.02.17 Enumerations (0) 2022.02.16 Closures (0) 2022.02.16