티스토리 뷰

반응형

안녕하세요 Pingu입니다!

 

이번 글에서는 Swift 공식 문서의 10번째 단원인 Properties를 읽고 정리한 글을 쓰려고 합니다.

 

Apple Swift 공식 문서 10단원 - Properties

Properties (프로퍼티)

Properties(프로퍼티)는 클래스, 구조체, 열거형에서 사용되는 associate value입니다. Stored property(저장 프로퍼티)는 클래스, 구조체, 열거형의 인스턴스의 일부로 저장되고 Computed property(계산 프로퍼티)는 값을 계산하는데 쓰입니다. 저장 프로퍼티는 클래스와 구조체에만 제공되고 계산 프로퍼티는 클래스, 구조체, 열거형에 제공됩니다.

프로퍼티는 타입 그 자체와 연결되어 있을 수 있습니다. 이런 프로퍼티를 type property(타입 프로퍼티)라고 합니다. 또한 프로퍼티가 가진 값의 변화를 관찰하기 위해 property observer(프로퍼티 관찰자)를 만들 수 있습니다. 프로퍼티 옵저버는 정의한 저장 프로퍼티나 하위 클래스가 상속받는 상위 클래스의 프로퍼티에 추가할 수 있습니다.

Property wrapper를 사용하면 여러 프로퍼티의 getter, setter를 사용하여 코드를 재사용할 수도 있습니다.


Stored Properties (저장 프로퍼티)

저장 프로퍼티는 프로퍼티를 사용하는 가장 간단한 방법으로 변수나 상수로 선언된 클래스나 구조체의 인스턴스의 일부분으로 저장되는 것입니다. 저장 프로퍼티는 클래스나 구조체에서 변수, 상수 중 어떤 것으로 선언되었는지에 따라 자신의 타입도 결정됩니다. 이때 클래스나 구조체에서 선언과 동시에 값을 주면 이것을 Default Property Value라고 하며 기본값이 됩니다. 이러한 저장 프로퍼티는 initializer(생성자)로 초기화시킬 수도 있습니다. 생성자에 의해 초기화될 때는 해당 프로퍼티가 상수로 선언되었다고 해도 수정이 가능합니다.

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

위의 코드에서 firstValue, length가 저장 프로퍼티이다. 여기서 length는 상수로 선언되었지만 rangeOfThreeItems라는 변수가 구조체 인스턴스로 선언될 때 생성자로 값을 할당할 수 있습니다. 물론 생성자로 값을 할당한 이후엔 값을 변경할 수 없습니다.

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

위의 코드와 같이 firstValue라는 프로퍼티는 구조체를 선언할 때 변수로 선언되었지만 구조체 인스턴스인 rangeOfFourItems가 상수로 선언됐기 때문에 변경할 수 없게 됩니다. 이러한 결과가 나오는 이유는 구조체가 value(값) 타입이기 때문입니다. 구조체 인스턴스를 상수로 선언하게 되면 내부의 프로퍼티들도 모두 상수가 되어 수정할 수 없게 됩니다.

class FixedLengthRange {
    var firstValue: Int
    let length: Int
    
    init(firstValue: Int, length: Int) {
        self.firstValue = firstValue
        self.length = length
    }
}

let rangeOfFourItems = FixedLengthRange(firstValue: 0, length: 4)
// 상수로 선언된 클래스 인스턴스는 변수로 선언된 프로퍼티의 값을 바꿀 수 있습니다.
rangeOfFourItems.firstValue = 5

// 오류 발생!
rangeOfFourItems.length = 3

하지만 참조 타입인 클래스에서는 이것과 다른 결과가 나타납니다. 클래스 인스턴스를 상수로 선언해도 변수로 선언한 프로퍼티의 값을 바꿀 수 있습니다. 물론 상수로 선언한 프로퍼티 값은 바꿀 수 없습니다.

Lazy Stored Properties (Lazy 저장 프로퍼티)

Lazy Stroed Property는 처음 사용될 때까지 초기화되지 않는 프로퍼티입니다. 이러한 프로퍼티는 lazy 키워드를 사용해 선언할 수 있습니다. lazy 프로퍼티를 사용하기 위해서는 반드시 var로 변수를 선언해야 합니다. 변수가 아닌 상수로 선언하게 되면 처음 사용할 때까지 값이 없게 되기 때문에 오류를 발생하게 됩니다.

 

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 has not yet been created

위의 코드는 lazy프로퍼티를 사용한 예입니다. DataManager의 importer 프로퍼티가 lazy 프로퍼티로 선언되었습니다. 그런 뒤 manager라는 이름으로 새로운 DataManager 상수 인스턴스를 생성하게 되는데 위의 코드에서는 아직 importer가 사용되지 않았습니다. 이런 상태에서 importer 프로퍼티에 값이 할당되면 낭비이기 때문에 이럴 때를 위해 importer를 lazy 프로퍼티로 선언해서 낭비를 줄일 수 있습니다.

print(manager.importer.filename)
// the DataImporter instance for the importer property has now been created
// Prints "data.txt"

즉 위의 코드와 같이 처음 접근됐을 때 드디어 lazy 프로퍼티가 초기화되게 됩니다.

Stored Properties and Instance Variable (저장 프로퍼티와 인스턴스 변수)

Objective-C에서는 클래스 인스턴스의 일부로 값과 참조를 저장하는 방법이 있습니다. 또한 프로퍼티에 저장된 값에 대한 백업 저장소로 인스턴스 변수를 사용할 수 있습니다.

 

Swift는 이러한 개념을 single property declaration(단일 프로퍼티 선언)으로 통합했습니다. Swift의 프로퍼티는 인스턴스 변수가 없고 프로퍼티의 백업 저장소에 바로 접근하지 않습니다. 이렇게 됨으로써 값을 여러 곳에서 접근하려고 할 때 혼란을 피할 수 있고 프로퍼티 선언을 하나의 명확한 코드로 단순화할 수 있게 되었습니다. 즉 이름, 타입, 메모리 관리 특성 등 프로퍼티의 모든 정보는 타입을 선언할 때 단일 위치에서 정의됩니다.


Computed Properties (계산 프로퍼티)

계산 프로퍼티는 클래스, 구조체, 열거형에서 사용 가능합니다. 계산 프로퍼티에는 값을 직접적으로 저장하지 않는 대신 getter와 setter를 사용해서 다른 프로퍼티와 값들을 간접적으로 접근할 수 있습니다. 이때 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
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)"

위의 코드에서 Point, Size는 점의 위치와 사각형의 크기를 캡슐화해둔 구조체입니다. 두 구조체 인스턴스를 가지고 Rect 구조체에서는 사각형을 그리게 됩니다. 여기서 Rect 구조체의 center 프로퍼티를 잘 살펴보겠습니다. 이 center 프로퍼티가 계산 프로퍼티입니다. Rect 구조체의 origin, size 변수는 선언된 Point과 Size 인스턴스로 정해지지만 center변수에는 origin, size 변수의 값에 의해 계산되어야 값들이 정해지게 됩니다. 이때 getter와 setter를 사용하여 center라는 계산 프로퍼티 값을 저장 프로퍼티처럼 사용할 수 있는 것입니다.

shortand Setter Declaration (간단한 setter 선언)

만약 계산 프로퍼티에서 set을 사용할 때 새로운 값의 이름을 설정해 두지 않으면 default값인 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)
        }
    }
}

Shortand Getter Declaration (간단한 getter 선언)

계산 프로퍼티의 getter 사용에서 getter가 단일 표현식으로 되어있다면 자동적으로 그 표현식을 반환하게 됩니다.

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을 생략해도 해당 표현식의 결과가 반환됩니다.

Read-Only Computed Properties (읽기 전용 계산 프로퍼티)

계산 프로퍼티에서 getter만 있고 setter가 없는 경우가 read-only computed 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"

위의 코드에서 volume 프로퍼티와 같이 getter, 즉 return만 존재하는 경우에는 get을 생략할 수 있습니다.


Property Observers (프로퍼티 옵저버)

Property Observers(프로퍼티 옵저버)는 프로퍼티의 값의 변화를 관찰하고 반응합니다. 새로운 값이 기존 값과 같더라도 프로퍼티 옵저버는 호출됩니다. 즉 프로퍼티가 set 될 때 마다 호출된다고 볼 수 있습니다. 프로퍼티 옵저버는 다음과 같은 곳에 사용할 수 있습니다.

  1. 정의한 저장 프로퍼티
  2. 상속한 저장 프로퍼티
  3. 상속한 계산 프로퍼티

상속한 프로퍼티의 경우 자식 클래스에서 프로퍼티 옵저버를 추가하려면 overriding을 사용하면 됩니다. 계산 프로퍼티의 경우 프로퍼티 옵저버를 만드는 것 대신에 setter에서 값의 변화를 관찰 및 응답할 수 있습니다. 프로퍼티 overriding은 여기를 참고해주세요!

 

프로퍼티의 옵저버에는 두 가지가 있습니다.

  • willSet 옵저버 : 값이 저장되기 직전에 호출됩니다.
  • didSet 옵저버 : 값이 저장된 직후에 호출됩니다.

willSet 옵저버는 새로 저장될 프로퍼티 값을 상수 매개변수로 전달합니다. 이 매개변수의 이름은 willSet 구문안에서 사용할 수 있도록 이름을 지정할 수 있고 만약 지정하지 않으면 기본값인 newValue가 매개변수의 이름이 됩니다.


didSet 옵저버는 프로퍼티의 기존 값이 상수 매개변수로 전달됩니다. 이 매개변수의 이름을 didSet 구문안에서 사용할 수 있도록 이름을 지정할 수 있고 만약 지정하지 않으면 기본값인 oldValue가 매개변수의 이름이 됩니다.

 

부모 클래스의 생성자가 호출된 후 상속된 자식 클래스 생성자에서 프로퍼티가 초기화되면 부모 클래스의 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

위의 코드는 willSet, didSet의 사용 예입니다. 위와 같이 클래스의 인스턴스를 생성하여 인스턴스의 프로퍼티를 초기화해줄 때마다 willSet, didSet 옵저버가 호출되어 해당 구문이 실행되는 것을 볼 수 있습니다. 또한 따로 매개변수의 이름을 설정하지 않은 didSet의 경우 oldValue라는 이름이 매개변수의 이름으로 사용되는 것을 볼 수 있습니다.

 

옵저버가 있는 프로퍼티에서 in-out 매개변수로 함수에 전달하게 되면 willSet, didSet 옵저버가 항상 호출됩니다. 이는 in-out 매개변수에 대한 copy in copy out 메모리 모델 때문입니다. in-out 매개변수에 대한 자세한 설명은 여기를 참고해주세요!


Property Wrappers (프로퍼티 포장?)

Property wrapper는 프로퍼티 저장 방법을 관리하는 코드와 프로퍼티를 정의하는 코드를 구분하는 계층을 추가하는 것입니다. 예를 들어 스레드 안전성 검사나 데이터를 DB에 저장하는 프로퍼티가 있는 경우 모든 프로퍼티에 대해 코드를 작성해야 하는데 Property wrapper를 사용하면 이러한 작업을 줄일 수 있습니다. 관리 코드를 한 번만 작성한 다음 여러 프로퍼티에 적용하여 관리 코드를 재사용할 수 있기 때문입니다.

 

Property wrapper 사용을 위해서는 구조체, 클래스, 열거형에서 wrapperValue라고 하는 프로퍼티를 선언해야 한다. 예시를 보며 이해해 보겠습니다.

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

위의 코드에선 wrappedValue에 항상 12보다 작은 값이 저장되게 만든 코드입니다. 만약 12보다 큰 수라면 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를 여러 번 재사용하여 사용할 수 있습니다. Property wrapper를 사용하기 위해선 프로퍼티 앞에 property wrapper의 이름을 @와 함께 써주면 됩니다. SmallRectangle의 height, width 프로퍼티는 Property wrapper가 적용된 상태이기 때문에 12 이상의 값을 가질 수 없게 됩니다.

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 wrapper를 적용하는 방법입니다. 위와 같이 property wrapper를 사용하게 되면 property wrapper로 선언된 프로퍼티에 값을 저장해야 하기 때문에 getter, setter를 사용할 수 있게 됩니다.

Setting Initial Values for Wrapped Properties (래핑 된 프로퍼티에 초기화 값을 설정하기)

위에서 작성된 코드는 property wrapper에서 초기 값을 정의할 수 있었습니다. 그렇기 때문에 래핑 된 프로퍼티는 다른 초기 값을 지정할 수 없었는데요, 이러한 점을 보완하기 위해서는 property wrapper에 생성자를 추가해주면 됩니다.

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

위의 코드에서 property wrapper로 사용될 구조체에는 3개의 생성자가 있습니다. 이렇게 생성자가 추가된 property wrapper를 적용한 예를 살펴보겠습니다.

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

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

이젠 생성자를 통해서 초기 값을 선언할 수 있게 된 것을 볼 수 있습니다.

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

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

위와 같이 height, width에 1을 할당하게 되면 이는 property wrapper의 init(wrappedValue:) 생성자를 호출하게 됩니다.

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"

위의 코드는 init(wrappedValue:maximum:) 생성자를 사용하는 예입니다. 프로퍼티 래퍼에 인수를 포함하면 래퍼의 초기 상태를 설정하거나 래퍼가 생성될 때 다른 옵션을 래퍼에 전달할 수 있습니다.

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)을 호출하여 초기화했고 width 프로퍼티를 감싸는 SmallNumber 인스턴스는 SmallNumber(wrappedValue: 2, maximum: 9)를 호출하여 초기화됩니다.

Projecting a Value From a Property Wrapper (프로퍼티 래퍼에 의해 투영된 값)

래핑 된 값 말고도 property wrapper는 projected value(투영 값)을 정의하여 추가 기능을 확장할 수 있습니다. 투영 값은 $로 시작하는 점만 제외하면 래핑 된 값과 동일합니다.

@propertyWrapper
struct SmallNumber {
    private var number: Int
    var projectedValue: Bool
    init() {
        self.number = 0
        self.projectedValue = false
    }
    var wrappedValue: Int {
        get { return number }
        set {
            if newValue > 12 {
                number = 12
                projectedValue = true
            } else {
                number = newValue
                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"

위의 코드에서 Bool 타입의 projectedValue 프로퍼티를 추가하여 새 값을 저장하기 전에 property wrapper가 프로퍼티에 값을 초기화했는지 확인할 수 있습니다. 예를 들어 4를 저장할 경우엔 property wrapper가 새 값을 할당해 준 것이 아니기 때문에 false가 저장되어 있지만 55를 저장할 경우엔 property wrapper가 값을 조정하여 12를 저장하게 되기 때문에 이땐 true가 저장되게 됩니다. 물론 이렇게 Bool 타입 말고도 모든 타입의 값을 반환할 수 있습니다.

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

위의 코드와 같이 getter 또는 인스턴스 메서드와 같은 타입 선언의 일부인 곳에서 projected value값을 접근한다면 self를 제외해도 됩니다. 이런 경우엔 $를 붙여주어 값을 나타냅니다.

 

프로퍼티 래퍼 구문은 getter, setter가 있는 프로퍼티의 syntactic sugar(수월한 구문 표현)에 불과하기 때문에 위의 코드에서 height, width에 접근하는 동작은 다른 프로퍼티에 접근하는 것과 동일하게 동작합니다. 예를 들어 resize(to:) 메서드는 프로퍼티 래퍼를 사용하여 width, height에 접근합니다. resize(to:. large)를 호출하면 사각형의 width, height를 각각 100으로 설정합니다. 래퍼는 프로퍼티의 값이 12보다 크지 않도록 방지하기 때문에 투영 값을 true로 설정합니다. 이를 활용하여 $height || $width을 처리한 값을 return 하여 프로퍼티 래퍼가 width, height를 조정했는지 확인할 수 있습니다.


Global and Local Variables (전역 변수, 지역 변수)

프로퍼티를 계산하고 값이 변화하는지 관찰하는 옵저버 기능은 전역 변수, 지역 변수에서도 사용할 수 있습니다. 전역 변수는 함수, 메서드, 클로 저등의 외부에서 정의된 변수이고 지역 변수는 함수, 메서드, 클로저등의 내부에 정의된 변수입니다.


지난 글에서 본 모든 전역 변수와 지역 변수는 stored variable 였습니다. 이는 저장 프로퍼티와 비슷하게 값에게 저장 공간을 주고 해당 값을 설정하고 검색할 수 있게 해 줍니다.


물론 전역 범위와 지역 범위에서 computed variable을 정의하고 저장 변수에 대한 옵저버를 정의할 수도 있습니다. 계산 변수는 값을 저장하지 않고 계산 프로퍼티와 같은 방식으로 쓰입니다.

 

전역 변수, 상수는 Lazy 저장 프로퍼티와 동일한 방법으로 처리됩니다. 하지만 전역 변수, 상수에 Lazy 키워드를 표시할 필요는 없습니다. 이와 반대로 지역 변수, 상수에는 절대로 lazy를 사용할 수 없습니다.


Type Properties (타입 프로퍼티)

인스턴스의 프로퍼티는 특정 타입의 인스턴스에 속해있는 프로퍼티입니다. 특정 타입의 인스턴스를 만들 때마다 다른 인스턴스들과는 별도로 각자의 프로퍼티가 생성됩니다.

 

하지만 Type Property는 특정 타입 자체에 속하는 프로퍼티를 만들어 특정 타입의 인스턴스를 몇 개를 만들어도 같은 프로퍼티에 접근할 수 있도록 할 수 있습니다.

 

타입 프로퍼티는 어떠한 타입의 모든 인스턴스에 필요한 값들을 정의할 때 사용하면 유용합니다. 이를 사용할 때 Stored type property(저장 타입 프로퍼티)를 만들면 항상 값이 존재해야 합니다. 타입 자체에는 생성자가 없기 때문입니다. 또한 저장 타입 프로퍼티에 처음 접근될 때는 lazy 형태로 초기화됩니다. 여러 스레드가 동시에 접근해도 한 번만 초기화되도록 보장되며 lazy를 명시적으로 표시할 필요는 없습니다.

Type Property Syntax (타입 프로퍼티 사용법)

Swift에서는 타입 프로퍼티를 타입의 중괄호 안에 작성하며 명시적으로 지원됩니다. 타입 프로퍼티를 정의하기 위해선 static 키워드를 사용하면 됩니다.

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

위의 예는 저장, 계산 타입 프로퍼티를 사용하는 방법을 보여주는 예입니다. 구조체, 열거형, 클래스의 프로퍼티를 선언할 때 static 키워드를 앞에 써주면 타입 프로퍼티로 정의됩니다.

 

위의 예에서는 계산 타입 프로퍼티가 읽기 전용으로 선언되었지만 일반적인 계산 프로퍼티와 동일한 방법으로 읽기, 쓰기 가능한 계산 타입 프로퍼티를 정의할 수도 있습니다.

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

한 가지 예를 들어보며 타입 프로퍼티를 이해해보겠습니다.

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

위의 클래스에서 선언된 구조체는 오디오 채널에 관련된 구조체입니다. 구조체에는 thresholdLevel, maxInputLevelForAllChannels라는 두 개의 타입 프로퍼티가 선언되어 있습니다. 여기서 currentLevel 프로퍼티에는 수정할 때마다 currentLevel의 값을 확인하는 didSet 프로퍼티 옵저버가 있습니다. 여기서 확인하는 것은 currentLevel의 새 값이 허용된 thresholdLevel보다 큰 경우 currentLevel을 thresholdLevel로 수정합니다. 그런 뒤 currentLevel의 수정된 값이 기존의 maxInputLevelForAllChannels보다 큰 경우 maxInputLevelForAllChannels의 값을 수정된 currentLevel의 값으로 수정합니다.

var leftChannel = AudioChannel()
var rightChannel = AudioChannel()

leftChannel.currentLevel = 7
print(leftChannel.currentLevel)
// Prints "7"
print(AudioChannel.maxInputLevelForAllChannels)
// Prints "7"

두 개의 AudioChannel 인스턴스를 만들고 leftChannel의 currentLevel의 값을 7로 수정합니다. 인스턴스의 프로퍼티를 수정했지만 AudioChannel, 즉 타입의 프로퍼티 값도 변한 것을 알 수 있습니다. 이는 currentLevel 프로퍼티에 didSet 옵저버가 있기 때문입니다. currentLevel 프로퍼티의 값이 초기화된 뒤 currentLevel의 값이 maxInputLevelForAllChannels의 값보다 크다면 currentLevel의 값을 maxInputLevelForAllChannels의 값으로 저장합니다. 따라서 위와 같은 결과가 나오는 것을 이해할 수 있습니다.

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

위의 코드처럼 AudioChannel의 타입 프로퍼티인 thresholdLevel 값 보다 큰 값을 currentLevel의 값으로 주면 currentLevel의 값이 타입 프로퍼티인 thresholdLevel의 값과 동일하게 초기화됩니다.

 

감사합니다.

반응형
댓글
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
«   2024/04   »
1 2 3 4 5 6
7 8 9 10 11 12 13
14 15 16 17 18 19 20
21 22 23 24 25 26 27
28 29 30
글 보관함