소피it블로그
[Swift 공식문서]Properties 정리 (2) 본문
https://docs.swift.org/swift-book/LanguageGuide/Properties.html
4. 프라퍼티 래퍼(Property Wrappers)
프라퍼티 래퍼는 프라퍼티가 어떻게 저장되는지를 관리하는 코드와 프라퍼티를 정의하는 코드를 나누는 경계를 더한다.
프라퍼티 래퍼를 정의하기 위해서는 wrappedValue 프라퍼티를 정의하는 구조체, 에뉴머레이션, 또는 클래스를 만들어줘야 한다. 아래의 코드에서 TwelveOrLess 구조체는 감싸는 값이 항상 12 이하의 숫자만 포함하도록 해준다. 더 큰 숫자를 저장하라고 해도 12를 저장할 것이다.
@propertyWrapper
struct TwelveOrLess {
private var number = 0
var wrappedValue: Int {
get { return number }
set { number = min(newValue, 12) }
}
}
세터는 새 값이 12 이하일 수 있도록 만들며, 게터는 저장된 값을 리턴한다.
위의 예시에서 숫자의 선언은 해당 변수를 private로 적고 있는데, 이는 해당 숫자가 TwelveOrLess의 수행 안에서만 사용된다는 것을 보장한다. 그 이외의 곳에서 적힌 코드는 wrappedValue의 게터와 세터를 통해 값에 접근하며, 직접적으로 숫자를 사용할 수 없다.
래퍼의 이름을 프라퍼티 앞에 attribute로 적어줌으로써 프라퍼티에 래퍼를 적용할 수 있다. 이하는 TwelveOrLess 프라퍼티 래퍼를 사용하여 차원이 항상 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"
height와 width 프라퍼티는 TwelveOrLess의 정의를 통해 초깃값을 얻는데, 이는 TwelveOrLess.number를 0으로 설정한다. TwelveOrLess의 세터는 10을 유효한 값으로 여기기 때문에 숫자 10을 rectangle.height에 저장하는 것은 진행된다. 그러나 24의 경우 TwelveOrLess가 허용하는 값보다 크기 때문에 24를 저장하려고 하는 시도는 rectangle.height를 허용된 것 중 가장 큰 숫자인 12로 설정하는 결과로 끝난다.
프라퍼티에 래퍼를 적용할 경우 컴파일러는 래퍼에 저장공간을 주는 코드와 래퍼를 통해 프라퍼티에 접근할 수 있게 해주는 코드를 통합한다(프라퍼티 래퍼는 래핑된 값을 저장하는 역할을 하기 때문에 이에 대한 통합된 코드는 없다). special attribute syntax를 사용하지 않고서도 프라퍼티 래퍼의 태도를 사용하는 코드를 적을 수 있다. 다음은 어트리뷰트로서 @TwelveOrLess라고 적어주는 대신, TwelveOrLess 구조체에서 명확하게 프라퍼티를 래핑하는 버전의 SmallRectangle이다.
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 }
}
}
_height와 _width 프라퍼티는 TwelveOrLess 프라퍼티 래퍼의 인스턴스를 저장한다. height와 width의 게터와 세터는 wrappedValue 프라퍼티로의 접근을 래핑한다.
(1) 래핑된 프라퍼티의 초깃값을 설정하기
위의 예시들에서의 코드는 래핑된 프라퍼티의 초깃값을 TwelveOrLess를 정의할 때 숫자를 초깃값으로 줌으로써 설정한다. 이 프라퍼티 래퍼들을 사용하는 코드는 TwelveOrLess에 의해 래핑된 프라퍼티의 초깃값을 다른 것으로 구체화하지 못한다. 예를 들어 SmallRectangle의 정의는 height나 width에 초깃값을 줄 수 없다. 프라퍼티 래퍼는 이니셜라이저를 추가해야만 초깃값을 설정하거나 다른 커스텀을 할 수 있다. 이하는 SmallNumber라고 불리는 TwelveOrLess의 확장된 버전으로, 래핑된 값과 최댓값을 설정하는 이니셜라이저를 정의한다.
@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)
}
}
SmallNumber의 정의는 init(), init(wrappedValue:) 그리고 init(wrappedValue:maximum:)이라는 세 가지 이니셜라이저를 포함한다. 아래의 예시에서 이들을 래핑된 값과 최댓값을 설정하는데 사용한다.
프라퍼티에 래퍼를 적용할 때 초깃값을 구체화하지 않으면 스위프트는 init() 이니셜라이저를 사용하여 래퍼를 설정한다.
struct ZeroRectangle {
@SmallNumber var height: Int
@SmallNumber var width: Int
}
var zeroRectangle = ZeroRectangle()
print(zeroRectangle.height, zeroRectangle.width)
// Prints "0 0"
height와 width를 래핑하는 SmallNumber의 인스턴스들은 SmallNumber()를 호출함으로써 생성된다. 그 이니셜라이저 안에 있는 코드는 0과 12의 디폴트값을 아용하여 초기의 래핑된 값과 초기의 최댓값을 설정해준다. 프라퍼티 래퍼는 앞의 SmallRectangle 안에서 TwelveOrLess를 사용해주었던 예시에서와 같이 모든 초깃값을 전부 제공해준다. 그러나 그 예시와 다르게 SmallNumber는 해당 초깃값들을 프라퍼티를 선언하는 것의 일부로 적을 수 있도록 해준다.
프라퍼티의 초깃값을 정할 때 스위프트는 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"
프라퍼티에 래퍼와 함께 =1를 적어주면, 이는 init(wrappedValue:) 이니셜라이저를 호출하는 것으로 해석된다. height와 width를 래핑하는 SmallNumber의 인스턴스들은 SmallNumber(wrappedValue: 1)를 호출함으로써 생성된다. 그 이니셜라이저는 이곳에 구체화된 래핑된 값을 사용하며, 디폴트 최댓값인 12를 사용한다.
커스텀 애트리뷰트 뒤에 괄호 안에 인수를 적어줄 경우 스위프트는 래퍼를 설정하는데 해당 인수들을 받아들이는 이니셜라이저를 사용한다. 예를 들어 초깃값과 최댓값을 제공하면 스위프트는 init(wrappedValue:maximum:) 이니셜라이저를 사용한다.
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"
height를 래핑하는 SmallNumber의 인스턴스는 SmallNumber(wrappedValue: 2, maximum: 5)를 호출함으로써 생성되고, width를 래핑하는 인스턴스는 SmallNumber(wrappedValue: 3, maximum: 4)를 호출함으로써 생성된다.
프라퍼티 래퍼에 인수를 포함시킴으로써 래퍼의 초깃값을 설정해주거나 래퍼가 생성될 때 다른 롭션을 건네줄 수 있다. 이 문법이 프라퍼티 래퍼를 사용하는 가장 일반적인 방법이다. 애트리뷰트에 필요한 인수로 무엇이든 제공할 수 있고, 그것들은 이니셜라이저에 넘겨진다.
프라퍼티 래퍼 인수를 포함시킬 경우 할당을 통해 초깃값을 구체화해줄 수 있다. 스위프트는 할당을 wrappedValue 인수처럼 취급하고, 포함되는 인수를 받아들이는 이니셜라이저를 사용한다.
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"
height를 래핑하는 SmallNumber의 인스턴스는 SmallNumber(wrappedValue: 1)을 통해 생성되는데, 이는 디폴트 최댓값인 12를 사용한다. width를 래핑하는 인스턴스는 SmallNumber(wrappedValue: 2, maximum: 9)를 통해 생성된다.
(2) Projecting a Value From a Property Wrapper
래핑된 값에 더하여 프라퍼티 래퍼는 projected 값을 정의함으로써 추가적인 기능을 드러낼 수 있다.
위의 SmallNumber 예시에서 프라퍼티를 너무 큰 숫자로 설정하려고 하면 프라퍼티 래퍼가 저장하기 전에 숫자를 조정한다. 아래의 코드는 새 값을 저장하기 전에 프라퍼티 래퍼가 해당 새 값을 조정했는지를 확인할 수 있도록 SmallNumber 구조체에 projectedValue 프라퍼티를 더한다.
@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)
// Prints "false"
someStructure.someNumber = 55
print(someStructure.$someNumber)
// Prints "true"
someStructure.$someNumber를 적어주는 것은 래퍼의 projected 값을 접근하게 해준다. 4와 같이 작은 숫자를 저장한 후에 someStructure.$someNumber의 값은 false가 된다. 그러나 55와 같이 지나치게 큰 숫자를 저장하려고 하면 projected 값은 true가 된다.
프라퍼티 래퍼는 projected 값으로 어느 타입의 값이든 리턴할 수 있다. 이 예시에서 프라퍼티 래퍼는 정보의 일부분(숫자가 조정되었는지 여부)만을 드러내며, projected 값으로 그 불리언 값만을 드러낸다. 더 많은 정보를 드러내야 하는 래퍼는 다른 데이터 타입의 인스턴스를 리턴하거나 projected 값으로 래퍼의 인스턴스를 드러내기 위해 self를 리턴할 수도 있다.
코드에서 프라퍼티의 getter나 인스턴스의 메서드와 같은 타입의 일부인 projected 값에 접근할 때면, 다른 프라퍼티에 접근할 때처럼 프라퍼티 이름 앞의 self.을 생략해도 된다. 이어지는 예시에서의 코드는 height와 width 주변의 래퍼의 projected 값을 $height와 $width라고 한다.
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
}
}
프라퍼티 래퍼 문법이 게터와 세터가 있는 프라퍼티를 위한 syntactic sugar이기 때문에, height와 width에 접근하는 것은 다른 프라퍼티에 접근하는 것과 같다. 예를 들어 resize(to:) 코드는 height와 width에 프라퍼티 래퍼를 통해 접근한다. resize(to: .large)를 호출할 경우 .large에 대한 스위치 케이스는 직사각형의 높이와 넓이를 100으로 설정한다. 래퍼는 해당 프라퍼티들의 값이 12 이상이 되는 것을 막고, 값을 조정했다는 사실을 기록하기 위해 projected 값을 true로 설정한다. resize(to:)의 마지막에서 리턴 문구는 $height와 $width를 체크하여 프라퍼티 래퍼가 height나 width를 수정했는지를 확인한다.
5. 전역변수와 지역변수
계산된 프라퍼티와 관찰된 프라퍼티의 능력은 전역변수와 지역변수에도 적용된다. 전역변수(global variables)는 함수, 메서드, 클로저, 타입 컨텍스트 바깥에서 정의되는 변수들을 의미한다. 지역변수(local variables)는 함수, 메서드 또는 클로저 컨텍스트 안에서 정의되는 변수들을 의미한다.
앞선 챕터들에서 보았던 전역변수와 지역변수는 전부 저장된 변수였다. 저장된 프라퍼티와 마찬가지로 저장된 변수는 특정 타입의 값에 저장소를 제공하고, 해당 값이 설정되고 사용될 수 있도록 한다.
그러나 전역적으로도 지역적으로도, 계산된 변수를 정의하거나 저장된 변수에 대한 옵저버를 정의할 수도 있다. 계산된 변수는 값을 저장하기보다는 계산하고, 계산된 프라퍼티와 같은 방식으로 쓰인다.
전역상수와 변수는 lazy stored properties처럼 늘 lazy하게 계산된다. lazy stored properties와 달리 전역상수와 변수는 lazy 수식어로 표시해줄 필요가 없다. 한편, 지역상수와 변수는 lazy하게 계산되지 않는다.
지역적인 저장된 변수에는 프라퍼티 래퍼를 적용해줄 수 있지만 전역변수나 계산된 변수에는 해줄 수 없다. 아래의 코드에서 myNumber은 SmallNumber를 프라퍼티 래퍼로 사용하고 있다.
func someFunction() {
@SmallNumber var myNumber: Int = 0
myNumber = 10
// now myNumber is 10
myNumber = 24
// now myNumber is 12
}
SmallNumber를 프라퍼티에 적용했듯이 myNumber의 값을 10으로 지정해주는 것 또한 유효하다. 프라퍼티 래퍼가 12 이상의 값을 허용하지 않기 때문에 이는 myNumber의 값을 24 대신 12로 지정한다.
6. 타입 프라퍼티
인스턴스 프라퍼티란 특정 타입의 인스턴스에 속하는 프라퍼티를 의미한다. 해당 타입에 대해 새로운 인스턴스를 생성할 때마다, 그 인스턴스는 다른 인스턴스들과 별개로 자신만의 프라퍼티 값들을 갖는다.
한편, 타입의 인스턴스가 아니라 타입 그 자체에 속하는 프라퍼티 또한 정의할 수 있다. 얼마나 많은 인스턴스를 생성하든지간에 이러한 프라퍼티들에는 단 하나의 카피만이 존재할 것이다. 이런 프라퍼티를 타입 프라퍼티라고 부른다.
타입 프라퍼티는 특정 타입의 모든 인스턴스에 보편적으로 적용될 수 있는 값들을 정의하는데 유용하다. 예를 들면 모든 인스턴스가 사용할 수 있는 상수 프라퍼티나, 해당 타입의 모든 인스턴스에서 전역적인 값을 저장하는 변수 프라퍼티 등이 있다.
저장된 타입 프라퍼티는 변수 또는 상수가 될 수 있다. 계산된 타입 프라퍼티는 계산된 인스턴스 프라퍼티처럼 늘 변수 프라퍼티로 선언된다.
저장된 인스턴스 프라퍼티와 다르게 저장된 타입 프라퍼티에는 늘 디폴트 값을 줘야만 한다. 이는 타입 자체는 초기화시에 저장된 타입 프라퍼티에 할당할 수 있는 이니셜라이저를 갖지 않기 때문이다.
저장된 프라퍼티는 첫 접근시 lazy하게 초기화된다. 여러 개의 스레드에 의해 동시다발적으로 접근된다 해도 단 한 번만 초기화되도록 보장되며, lazy 수식어로 표시해줄 필요도 없다.
(1) Type Property Syntax
스위프트에서는 C나 옵젝티브-C와는 다르게 타입 프라퍼티를 타입의 정의의 일부로 적는다. 타입의 바깥 중괄호 안에 적어주고 각각의 타입 프라퍼티는 자신이 뒷받침하는 타입의 범위에 명확하게 맞춰져있다.
타입 프라퍼티는 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
}
}
(2) 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"
아래의 예시는 여러 개의 오디오 채널에 대한 오디오 레벨 미터를 모델링하는 구조의 일부로써 두 저장된 타입 프라퍼티를 사용한다. 각각의 채널은 0부터 10까지, 양 끝을 포함한 정수값의 오디오 레벨을 갖는다.
아래의 표는 오디오 채널들이 스테레오 오디오 레벨 미터를 모델링하기 위해 어떻게 통합될 수 있는지를 보여준다. 채널의 오디오 레벨이 0일 경우 해당 채널에 대한 빛들 중 아무 것도 반짝이지 않는다. 오디오 레벨이 10일 때는 해당 채널의 모든 불이 켜진다. 이 표에서 왼쪽 채널은 현재 9 레벨이고 오른쪽 채널의 현재 레벨은 7이다.
위에 묘사된 오디오 채널들은 AudioChannel 구조체의 인스턴스로 표현된다.
struct AudioChannel {
static let thresholdLevel = 10
static var maxInputLevelForAllChannels = 0
var currentLevel: Int = 0 {
didSet {
if currentLevel > AudioChannel.thresholdLevel {
// cap the new audio level to the threshold level
currentLevel = AudioChannel.thresholdLevel
}
if currentLevel > AudioChannel.maxInputLevelForAllChannels {
// store this as the new overall maximum input level
AudioChannel.maxInputLevelForAllChannels = currentLevel
}
}
}
}
AudioChannel 구조체는 기능성을 뒷받침하기 위해 두 개의 저장된 타입 프라퍼티를 정의한다. 첫 번째인 thresholdLevel은 오디오 레벨이 취할 수 있는 최대의 분기점 값을 정의한다. 이는 모든 AudioChannel 인스턴스에 대해 상수값 10이다. 만일 오디오 시크널이 10보다 큰 값으로 들어온다면 아래애 묘사된 것처럼 이 분기점 값으로 capped 될 것이다.
두 번째 타입 프라퍼티는 maxInputLevelForAllChannels라고 하는 변수 저장된 프라퍼티이다. 이는 AudioChannel 인스턴스가 받은 최대 입력 값을 기록한다. 초깃값은 0이다.
AudioChannel 구조체는 currenLevel이라고 하는 저장된 인스턴스 프라퍼티 또한 정의하는데, 이는 채널의 현재 오디오 레벨을 0부터 10까지의 스케일로 나타낸다.
currentLevel 프라퍼티는 didSet 프라퍼티 옵저버를 가지고 있어 currentLevel이 설정될 때 그 값을 체크한다. 이 옵저버는 두가지 점검을 수행하는데
- currentValue의 새 값이 허용된 thresholdLevel보다 큰 경우 프라퍼티 옵저버는 currentLevel을 thresholdLevel로 cap한다.
- 만일 currentLevel의 캐핑 후 새 값이 AudioChannel 인스턴스가 받은 이전의 값들보다 크다면 그 프라퍼티 옵저버는 새 currentLevel 값을 maxInputLevelForAllChannels 타입 프라퍼티 안에 저장한다.
AudioChennel 구조체를 사용하여 leftChannel과 rightChannel이라는 새로운 오디오 채널 두 개를 생성해줄 수 있다. 이를 통해 스테레오 사운드 시스템의 오디오 레벨을 나타낼 수 있다.
var leftChannel = AudioChannel()
var rightChannel = AudioChannel()
왼쪽 채널의 currentLevel을 7로 설정하면 maxInputLevelForAllChannels 타입 프라퍼티가 동등한 7로 업데이트 되는 것을 확인할 수 있다.
leftChannel.currentLevel = 7
print(leftChannel.currentLevel)
// Prints "7"
print(AudioChannel.maxInputLevelForAllChannels)
// Prints "7"
오른쪽 채널의 currentLevel을 11로 맞추면 오른쪽 채널의 currentLevel 프라퍼티가 최댓값인 10으로 cap되는 것을 확인할 수 있으며, maxInputLevelForAllChannels 타입 프라퍼티 역시 동등하게 10으로 업데이트되는 것을 볼 수 있다.
rightChannel.currentLevel = 11
print(rightChannel.currentLevel)
// Prints "10"
print(AudioChannel.maxInputLevelForAllChannels)
// Prints "10"
'개발_iOS > 스위프트' 카테고리의 다른 글
[Swift 공식문서] Inheritance 정리 (0) | 2022.06.09 |
---|---|
[Swift 공식문서] Methods 정리 (0) | 2022.06.05 |
[Swift 공식문서]Properties 정리 (1) (2) | 2022.05.31 |
[Swift 공식문서] Structures and Classes 정리 (1) | 2022.05.24 |
[Swift 공식문서] Closures 정리 (2) (0) | 2022.05.23 |