Swift/Swift 가이드

[Swift] 공식 문서 - Properties (속성)

유정주 2022. 3. 22. 22:47
반응형

안녕하세요. 개발하는 정주입니다.

 

오늘은 "Properties (속성)"를 정리해보겠습니다.

 

* 완벽한 번역이 아닌 내용을 한 줄 한 줄 읽는 것에 의의를 두었습니다.

* 파파고의 힘을 빌려 번역했으며 잘못된 번역이 있다면 댓글로 알려주세요.

 


새로 배운 점

  • lazy 속성은 동시에 여러 개의 쓰레드가 접근할 경우 중복으로 초기화가 될 가능성이 있다는 점
  • Objective-C와 Swift의 클래스 인스턴스 값과 참조를 저장하는 방법이 다르다는 점 => Swift가 훨씬 단순화 되었다는 점
  • computed propety가 실제로는 저장이 안 된고 계산만 한다는 점
  • Observers
    • willSet, didSet이 Property Observer라는 카테고리(?)라는 점
    • 옵저버를 가진 속성을 in-out 매개 변수로 함수에 전달하면 willSet, didSet을 항상 호출한다는 점
  • property wrapper
    • 중복되는 속성을 재사용 할 수 있다는 점
    • 선언 방법, 활용 방법
    • 인자를 전달할 수 있다는 점
  • projected value에 대한 것
  • 전역 상수와 변수는 항상 lazily하게 계산된다는 점
  • stored type properties는 항상 lazily하게 초기화되며 여러 쓰레드가 접근해도 단 한 번만 초기화되는 것을 보증한다는 점

서론

properties는 값을 특정 클래스, 구조체 또는 열거형을 연결합니다. Stored properties는 인스턴스의 일부분으로서 상수와 변수 값을 저장하는 반면, Computed properties는 값을 계산합니다.

Computed properties는 클래스, 구조체 및 열거체에서 제공합니다.

Stored properties는 클래스와 구조체에서만 제공합니다.

 

Stored properties와 Computed properties는 보통 특정 타입의 객체와 결합됩니다.

하지만, properties는 그 자체와도 결합할 수 있습니다.

이런 properties를 type properties라고 합니다.

 

추가로, properties 값이 바뀌는 것을 감시하는 속성 옵저버(property observers)를 정의할 수 있어, 커스텀 액션으로 응답할 수도 있습니다. 속성 옵저버는 자신이 직접 정의한 stored properties 및 상위 클래스로부터 상속한 하위 클래스 속성에 추가할 수도 있습니다.

 

property wrapper를 이용하면 여러 속성에서 getter와 setter를 재사용할 수 있습니다.

 


Stored Properties (저장 속성)

가장 단순한 형식의 저장 속성은 특정 클래스나 구조체 인스턴스의 일부분으로서 저장하는 상수나 변수입니다.

stored properties는 var 키워드를 쓰는 변수 저장 속성이거나 let 키워드를 쓰는 상수 저장 속성일 수 있습니다.

 

저장 속성을 정의하면서 기본 값(default value)을 정의할 수도 있습니다. 상수에서도 마찬가지 입니다.

 

아래 예제는 생성 후엔 길이를 바꿀 수 없는 정수 범위를 설명한 FixedLengthRange라는 구조체입니다.

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

FixedLengthRange 인스턴스에는 firstValue라는 변수 저장 속성과 length라는 상수 저장 속성이 있습니다.

length는 상수 속성이기 때문에 초기화 후에는 변경할 수 없습니다.

 

Stored Properties of Constant Structure Instances (상수 구조체 인스턴스의 저장 속성)

구조체 인스턴스를 생성하고 그 인스턴스를 상수에 할당하면, 변수 속성으로 선언한 경우에도 인스턴스의 속성을 수정할 수 없습니다.

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

rangeOfFourItems를 상수로 선언했기 때문에 firstValue가 변수 속성일지라도 자신의 firstValue 속성을 바꾸는 게 불가능합니다.

 

이런 동작은 구조체가 값 타입(value types)이기 때문입니다.

값 타입의 인스턴스를 상수로 표시할 땐 자신의 모든 속성도 상수가 됩니다.

 

참조 타입(reference types)인 클래스는 다릅니다.

참조 타입의 인스턴스를 상수에 할당하면 그 인스턴스의 변수 속성은 값을 바꿀 수 있습니다.

 

Lazy Stored Properties (느긋한 저장 속성)

lazy stored properties는 최초 사용 전까지는 초기 값을 계산하지 않는 속성입니다.

선언 앞에 lazy modifire를 작성하여 씁니다.

lazy property는 항상 변수로 선언해야 하는데, 인스턴스 초기화를 완료하기 전까진 자신의 초기 값을 못 가져올 수도 있기 때문입니다.
상수 속성은 반드시 하상 초기화 완료 전에 값을 가져야 하므로 lazy로 선언할 수 없습니다.

Lazy properties는 속성의 초기 값이 외부 요인에 의존해서 인스턴스 초기화를 완료하기 전까지 값을 알 수 없을 때 유용합니다.

lazy property는 초기 값이 복잡하거나 비싼 계산 비용을 요구해서 필요한 게 아닌 한 그 전까지 수행하지 않는게 좋을 때도 유용합니다.

 

아래 예제는 lazy stored property를 사용하여 불필요한 복잡한 클래스의 초기화를 피합니다.

이 예제는 DataImporter와 DataManager라는 두 개의 클래스의 일부분을 보여줍니다.

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

 

DataManager 클래스에는 data 라는 저장 속성이 있는데 이는 빈 String 배열로 초기화됩니다.

나머지 기능을 보여주진 않았지만 DataManager 클래스는 이 String 배열에 접근하고 관리하는 역할입니다.

DataImporter는 외부 파일에서 자료를 불러오는 클래스입니다.

 

DataManager는 외부 파일을 불러오지 않을 수도 있기 때문에 DataImporter 인스턴스는 최초로 사용할 때 생성하는 것이 좋습니다.

이럴 때 lazy를 쓰면 유용합니다.

print(manager.importer.filename)
// the DataImporter instance for the importer property has now been created
// Prints "data.txt"
lazy 수정자로 표시한 속성이 아직 초기화가 안 됐는데 동시에 여러 개의 쓰레드가 접근할 경우 속성이 한 번만 초기화될 거라는 보증은 없습니다.

 

Stored Properties and Instance Variables (저장 속성과 인스턴스 변수)

Swift는 Objective-C와 달리 해당 인스턴스 변수가 없으며 속성의 백업 저장소에 직접 액세스하지 않습니다.

이런 접근법은 서로 다른 상황에서의 값 접근 방법에 대한 혼동을 피하게 하며 속성 선서을 단일, 정의문으로 단순화합니다.

속성의 이름, 타입 및 메모리 관리 성질을 포함한 모든 정보는 타입 정의의 일부분으로써 단일 위치에 정의합니다.

 


Computed Properties (계산 속성)

저장 속성에 추가로, 클래스와 구조체, 열거체는 실제론 값을 저장하지 않는 computed properties(계산 속성)도 정의할 수 있습니다.

이는 간접적으로 다른 속성과 값을 가져오고 설정하는 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 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)
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)"

Rect 구조체는 center라는 계산 속성도 제공합니다. Rect의 중심은 origin과 size로 항상 결정할 수 있으므로 Point 값으로 저장할 필요가 없습니다. 그 대신, Rect는 center라는 계산 변수를 위한 사용자 정의 획득자 및 설정자를 정의하여 직사각형의 center가 마치 실제 저장 속성인 것처럼 작업할 수 있게 합니다.

 

Shorthand Setter Declaration (짧게 줄인 설정자 선언)

새 값의 이름을 setter가 정의하지 않으면 newValue라는 기본 이름을 사용합니다. 

이 축약 표기법의 이점을 최대한 취한 Rect 구조체입니다.

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가 단일 표현식이라면 그 표현식을 암시적으로 반환합니다.

이 축약을 최대한 취한 Rect 구조체입니다.

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)
        }
    }
}

getter에서 return을 생략하는 건 함수에서 return을 생략하는 것과 동일한 규칙을 따릅니다.

 

Read-Only Computed Properties (읽기-전용 계산 속성)

getter는 있지만 setter는 없는 계산 속성은 읽기 전용 계산 속성(read-only computed property)이라고 합니다.

읽기 전용 계산 속성은 항상 값을 반환하며 접근할 수는 있지만 다른 값을 설정할 수는 없습니다.

읽기 전용 계산 속성을 포함한 계산 속성은 값이 고정된 것이 아니기 때문에 반드시 var 키워드를 써서 변수 속성으로 선언해야 합니다. 인스턴스 초기화의 일부분으로 한 번 설정하고 나면 값을 바꾸지 않는 상수 속성에만 let 키워드를 사용합니다.

 

get 키워드와 중괄호를 제거함으로써 읽기 전용 계산 속성의 선언을 단순화할 수 있습니다.

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 (속성 관찰자)

속성 관찰자는 속성 값이 바뀌는 걸 관찰하여 응답합니다. 속성 관찰자는 속성 값을 설정할 때마다(새 값이 현재 속성 값과 같아도) 호출됩니다.

 

다음 장소에 속성 관찰자를 추가할 수 있습니다.

  • 자신이 정의한 stored properties
  • 자신이 상속한 stored properties
  • 자신이 상속한 computed properties

상속한 속성이면 하위 클래스에서 그 속성을 재정의(overriding)함으로써 속성 관찰자를 추가합니다.

자신이 정의한 계산 속성이면 관찰자를 생성하는 대신에 속성의 setter로 값의 바뀜을 관찰하여 응답합니다.

 

속성에 대해 다음 관찰자 중 하나만 정의할지 둘 다 정의할지 선택할 수 있습니다.

  • willSet은 값을 저장하기 직전에 호출합니다.
  • didSet은 새 값을 저장한 바로 뒤에 호출합니다.

willSet 관찰자를 구현하면 새로운 속성 값을 상수 매개 변수로 전달합니다. willSet 구현부에서 이 매개 변수에 이름을 지정할 수 있습니다. 구현부에서 매개 변수 이름과 괄호를 작성하지 않으면 newValue라는 기본 매개 변수 이름으로 매개 변수를 사용할 수 있습니다.

 

didSet 관찰자를 구현하면 예전 속성 값을 담은 상수 매개 변수를 전달합니다. 매개 변수에 이름을 붙이거나 oldValue라는 기본 매개 변수 이름을 사용할 수도 있습니다. 자신의 didSet 관찰자 안에서 속성에 값을 할당하면 새로운 할당 값이 그전에 설정한 것을 대체합니다.

상위 클래스 속성의 willSet, didSet 관찰자는 상위 클래스 초기자를 호출한 후 하위 클래스 초기자에서 속성을 설정할 때 호출됩니다.
상위 클래스 초기자를 호출하기 전, 클래스가 자신만의 속성을 설정하는 동안엔 이를 호출하지 않습니다.

 

다음은 willSet과 didSet의 예시입니다.

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
관찰자를 가진 속성을 'in-out 매개 변수'로 함수에 전달하면 willSet과 didSet 관찰자를 항상 호출합니다.
이는 함수 끝에서 값을 속성으로 다시 작성하는 'in-out 매개 변수의 copy-in copy-out 메모리 모델'이기 때문입니다.

 

 


Property Wrappers

property wrapper는 속성 저장 방법을 관리하는 코드와 속성을 정의하는 코드 사이에 구분 계층을 추가합니다.

예를 들어, 쓰레드 안전성 검사를 제공하거나 자신의 실제 자료를 데이터베이스에 저장하는 속성이 있다면, 모든 속성에 대해 그 코드를 작성해야 합니다.

property wrapper를 사용할 땐, wrapper를 한 번 작성하면 여러 속성에 적용함으로써 그 관리 코드를 재사용합니다.

 

property wrapper를 정의하려면 wrappedValue 속성을 정의한 구조체나 열거체 또는 클래스를 만듭니다.

아래 코드의 TwelveOrLess 구조체는 자기의 wrap 값이 항상 12보다 작거나 같은 수를 담는다는 것을 보장합니다.

더 큰 수를 저장하도록 요청하면 12를 대신 저장합니다.

@propertyWrapper
struct TwelveOrLess {
    private var number = 0
    var wrappedValue: Int {
        get { return number }
        set { number = min(newValue, 12) }
    }
}

setter는 새 값이 12보다 작다는 것을 보장하며 getter는 저장한 값을 반환합니다.

 

속성 앞에 wrapper 이름을 특성(attribute)으로 작성함으로써 속성에 wrapper를 적용합니다.

아래 예시는 직사각형의 차원이 항상 12 이하가 되도록 하기 위해 TwelveOrLess 속성 포장을 사용하는 구조체입니다.

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를 적용할 때, 컴파일러는 wrapper의 저장 공간을 제공하는 코드와 포장을 통해 속성의 접근을 제공하는 코드를 통합합니다. (property wrapper는 wrapper 값의 저장을 책임지므로, 그에 대한 통합 코드는 없습니다)

 

@TwelveOrLess를 attribute로 작성하는 대신 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의 getter와 setter는 wrappedValue 속성에 대한 접근을 wrap합니다.

 

Setting Initial Values for Wrapped Properties (포장 속성에 초기 값 설정하기)

위 예제 코드는 TwelveOrLess 정의 안에서 number에 초기 값을 주는 것으로 property wrapper의 초기 값을 설정합니다.

이 property wrapper를 사용하는 코드는, TwelveOrLess가 wrap한 속성에 다른 초기 값을 지정할 수 없습니다.

초기 값 설정 및 다른 사용자 정의를 지원하려면 속성 포장에 초기자를 추가할 필요가 있습니다.

다음은 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:)이라는 세 개의 초기자를 포함하며 아래 예제에서 포장 값과 최대 값을 설정할 때 이를 사용합니다. 

 

속성에 wrap을 적용하면서 초기 값을 지정하지 않을 땐, Switf가 init() 초기자로 포장을 설정합니다.

struct ZeroRectangle {
    @SmallNumber var height: Int
    @SmallNumber var width: Int
}

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

height와 width를 wrap한 SmallNumber 인스턴스는 SmallNumber() 호출로 생성합니다.

그 초기자 안의 코드는 0과 12라는 기본 값을 사용하여 초기 포장 값과 초기 최대 값을 설정합니다.

property wrapper는 모든 초기 값을 제공합니다. 그 예제와는 달리 SmallNumber는 속성을 선언하는 부분에서 그 초기 값을 작성하는 것도 지원합니다.

 

속성의 초기 값을 지정할 땐, Swift가 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"

wrap 속성에 = 1를 작성할 땐, init(wrappedValue:) 초기자의 호출이라고 번역합니다. height와 width를 포장한 SmallNumber 인스턴스는 SmallNumber(wrappedValue: 1) 호출로 생성합니다. 초기자는 여기서 지정한 포장 값을 사용하며 12라는 기본 최대 값을 사용합니다.

 

사용자 정의 attribute 뒤의 괄호 안에 인자를 작성할 땐, Swift가 그 인자를 받는 초기자로 wrapper를 설정합니다.

예를 들어, 초기 값과 최대 값을 제공하는 경우, Swift가 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) 호출로 생성합니다.

 

property wrapper에 인자를 포함시켜서, 포장의 초기 상태를 설정하거나 포장을 생성할 때 다른 옵션을 전달할 수 있습니다.

이 구문이 property wrapper를 사용하는 가장 일반적인 방식입니다. 무슨 인자든 필요하면 attribute에 제공할 수 있으며 이를 초기자로 전달합니다.

 

propery wrapper 인자를 포함할 땐, 할당을 사용하여 초기 값을 지정할 수도 있습니다. Swift는 할당을 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라는 기본 최대 값을 사용합니다. 

 

Projecting a Value From a Property Wrapper (속성 포장에 있는 값 꺼내기)

wrapper 값에 더하여 property wrapper는 projected value를 정의함으로써 추가 기능을 나타낼 수 있습니다.

예를 들어, 데이터베이스 접근을 관리하는 property wrapper는 자신의 projected value에 대하여 flushDataConnection()을 나타냅니다. projected value의 이름은 달러 기호($)로 시작한다는 것만 제외하면 wrapper 값과 똑같습니다. 코드에서 $로 시작하는 속성을 정의할 순 없기 때문에 projected value가 자신이 정의한 속성을 간섭할 일은 절대로 없습니다.

 

위의 SmallNumber 예제에서 너무 큰 수를 속성에 설정하려고 하면 property wrapper는 저장 전에 수치 값을 적당히 조정합니다.

아래 코드는 Small Number 구조체에 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라고 작성하면 wrapper가 projected value에 접근합니다. 4처럼 작은 수를 저장한 후엔 someStruecture.$someNumber 값이 false입니다. 하지만 55처럼 너무 큰 수를 저장하려고 하면 true입니다.

 

properties wrapper는 어떤 타입의 값이든 자신이 내민 값으로 반환할 수 있습니다. 이 예제에선 properties wrapper가 수치 값을 적당히 조정했는지에 대한 정보만을 드러내므로 Boolean 값을 자신의 projected value로 드러냅니다.

더 많은 정보를 드러내야 하면 wrapper는 다른 자료 타입 인스턴스를 반환하거나, wrapper의 인스턴스를 자신의 projected Value로 드러내기 위해 self를 반환할 수도 있습니다.

 

setter나 인스턴스 메서드 같이, 타입의 일부분인 코드에서 projected value 값에 접근할 땐 다른 속성에 적근할 때처럼 속성 이름 앞의 self. 를 생략할 수 있습니다. 다음 예제는 height와 width wrapper의 projected value를 $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
    }
}

properties wrapper 구문은 getter와 setter가 있는 속성을 위한 수월한 구문일 뿐이기 때문에, height와 width 로의 접근은 다른 어떤 속성으로의 접근과 똑같이 동작합니다. 예를 들어, resize(to:) 코드는 자신의 properties wrapper를 써서 height와 width에 접근합니다. resize(to: .large)를 호출하면, .large라는 switch문 case 절이 직사각형의 높이와 폭을 100으로 설정합니다. wrapper는 그 속성 값이 12보다 커지는 것을 막고, 자신이 값을 조정한 사실을 기록하기 위해 projected value를 true로 설정합니다.

resize(to:) 끝에서, 반환문이 $height와 $width를 검사하여 properties wrapper가 height나 width 중 어느 하나를 적당히 조정했는지 결정합니다.

 


Global and Local Variables

위에서 설명한 속성을 계산하고 관찰하는 능력은 전역 변수와 지역 변수에서도 사용 가능합니다.

전역 변수는 어떤 함수나 메소드, 클로저, 타입 밖에서 정의한 변수입니다. 지역 변수는 함수나 메소드, 클로저 안에서 정의한 변수입니다.

 

이전 장에서 다룬 전역 변수와 지역 변수는 모두 stored variables입니다. 저장 변수는 저장 속성 같이 정해진 타입의 값에 저장 공간을 ㅈ공하며 그 값을 설정하고 가져오는 걸 허용합니다.

 

하지만 전역이나 지역 중 어디서든 computed variables를 정의할 수도, 저장 변수와 관찰자(observers)를 정의할 수도 있습니다.

계산 변수는 자신의 값을 저장하기 보단, 계산하며 계산 속성과 똑같은 식으로 작성합니다.

전역 상수와 변수는 lazy stored properties와 비슷한 특성에 따라 항상 lazily하게 계산합니다. lazy stored properties와 달리 전역 상수와 변수를 lazy 수정자로 표시할 필요는 없습니다.

지역 상수와 변수는 절대로 lazily하게 계산하지 않습니다.

 

전역 변수나 계산 변수가 아닌 지역 저장 변수에 properites wrapper를 적용할 수 있습니다.

예를 들어, 아래 코드의 myNumber는 SmallNumber를 properties wrapper로 사용합니다.

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

    myNumber = 10
    // now myNumber is 10

    myNumber = 24
    // now myNumber is 12
}

SmallNumber를 속성에 적용할 때처럼, myNumber 값에 10을 설정하는 것은 유효합니다. 12보다 높은 값은 속성 포장이 호용하지 않기 때문에 24 대신 12를 myNumber에 대입합니다.

 


Type Properties

인스턴스 속성은 한 특정 타입의 인스턴스에 속하는 속성입니다. 그 타입의 인스턴스를 새로 생성할 때마다 다른 어떤 인스턴스와도 구분된 자신만의 속성 값 집합을 가지게 됩니다.

 

그 타입의 어떤 하나의 인스턴스가 아니라 타입 그 자체에 속하는 속성을 정의할 수도 있습니다. 그 타입의 인스턴스를 많이 만들어도 이 속성의 복사본은 늘 단 하나만 있을 것입니다. 이러한 종류의 속성을 type properties라고 합니다.

 

타입 속성은 모든 인스턴스가 사용할 수 있는 상수 속성 또는 그 타입의 모든 인스턴스에 값을 전역으로 저장하는 변수 속성 같이 한 특정 타입의 모든 인스턴스에 보편적인 값을 정의하기에 유용합니다.

 

stored type properties는 변수 또는 상수일 수 있습니다. computed type properties는 항상 변수로 선언합니다.

저장 인스턴스 속성과 달리, 저장 타입 속성엔 반드시 기본 값을 설정해야 합니다. 이는 타입 그 자체에는 초기화 시간에 저장 타입 속성에 값을 할당할 수 있는 초기자가 없기 때문입니다.

저장 타입 속성은 최초로 접근할 때에 lazily하게 초기화됩니다. 동시에 여러 쓰레드가 접근할 때도 단 한 번만 초기화되는 것을 보증하며 lazy 수정자로 표시할 필요도 없습니다.

 

Type Property Syntax

C와 Objective-C에선, 타입과 결합한 정적 상수와 변수를 전역 정적 변수로 정의합니다.

하지만 Swift에서는, 타입 속성을 타입 정의의 일부분인 타입의 바깥 중괄호 안에서 작성하며, 각각의 타입 속성 영역은 자신이 지원하는 타입 영역으로 명시됩니다.

 

타입 속성은 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
    }
}
위의 계산 타입 속성 예제는 read-only 계산 타입 속성을 위한 것이지만, 계산 인스턴스 속성을 위한 것과 동일한 구문으로 read-write 계산 타입 속성을 정의할 수도 있습니다.

 

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"

 

아래 예제는 구조체의 일부분으로 두 개의 저장 타입을 사용하여 '다수의 음향 채널을 위한 음량 측정기(audio level meter)를 모델링합니다. 각각의 채널은 0부터 10까지를 포함한 정수 음량(audio level)'을 가집니다.

이 음향 채널을 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 구조체를 사용하여 스테레오 음향 시스템의 음량을 나타내는 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이라는 최대 값으로 제한하고 maxInputLevelForAllChannels 타입 속성을 10으로 갱신합니다.

rightChannel.currentLevel = 11
print(rightChannel.currentLevel)
// Prints "10"
print(AudioChannel.maxInputLevelForAllChannels)
// Prints "10"

 


참조

https://docs.swift.org/swift-book/

https://bbiguduk.gitbook.io/swift/

 

 

반응형