Skip to content

프로퍼티 (Properties) #3

@jryoun1

Description

@jryoun1

프로퍼티

프로퍼티는 값을 특정 클래스, 구조체, 열거형과 연결해준다.

  • 저장 프로퍼티(Stored Property)
    • 클래스, 구조체에서만 제공
  • 연산 프로퍼티(Computed Property)
    • 클래스, 구조체, 열거형에서 제공
  • 타입 프로퍼티 (Type Property)
    • 타입 자체와 연결된 프로퍼티
  • 프로퍼티 관찰자 (Property Observer)
  • 프로퍼티 래퍼 (Property Wrapper)

저장 프로퍼티 (Stored Properties)

특정 클래스, 구조체의 인스턴스에 저장된 상수, 변수

  • 저장된 프로퍼티 변수(var) = variable stored properties
  • 저장된 프로퍼티 상수(let) = constant stored properties

기본 프로퍼티 값(Default Property Values)에서 말했던 것처럼 저장된 프로퍼티는 기본값을 제공할 수 있다.

초기화중에 저장 프로퍼티에 초기값을 설정하고 수정할 수도 있다.

struct FixedLengthRange {
  var firstValue: Int
  let length: Int
}
var rangeOfThreeItems = FixedLengthRange(firstValue: 0, length: 3)
// 그러면 세 개의 값은 0, 1, 2 가 된다
rangeOfThreeItems.firstValue = 6
// 위와 같이 수정하면 값은 6, 7, 8 이 된다

FixedLengthRange의 인스턴스는 초기화할 때 값이 설정된다.
이때 firstValue 는 저장 프로퍼티 변수이므로 값을 변경가능하고, length 는 저장 프로퍼티 상수이기 때문에 값 변경이 불가능하다.


상수 구조체 인스턴스의 저장 프로퍼티 (Stored Properties Of Constant Structure Instances)

위에서는 FixedLengthRange의 인스턴스가 변수로 생성되지만,
만약 상수로 인스턴스를 생성하게되면 firstValue 가 변수 저장 프로퍼티여도 값을 수정할 수 없다.

let rangeOfFourItems = FixedLengthRange(firstValue: 0, length: 4)
// 그러면 세 개의 값은 0, 1, 2, 3 이 된다
rangeOfFourItems.firstValue = 6
// 🚨 변수 저장 프로퍼티지만 값을 수정할 수 없음 -> 상수 인스턴스이기 때문!!

그 이유는 구조체가 값 타입이기 때문이다.

값 타입의 인스턴스가 상수로 생성되면, 인스턴스의 모든 프로퍼티는 수정이 불가능하다.

반면 참조 타입인 클래스인스턴스는 상수로 생성되어도 인스턴스의 프로퍼티 변수는 수정이 가능하다.


지연 저장 프로퍼티 (Lazy Stored Property)

지연 저장 프로퍼티(lazy)는 처음 사용되기 전까지 초기값이 계속되지 않는 프로퍼티이다.

인스턴스 초기화가 완료된 후에도 초기값이 없을 수 있기 때문에 지연프로퍼티는 var 변수로 선언해야한다.

프로퍼티 상수의 경우에는 초기화가 되기 전에는 항상 값을 가지고 있어야하므로 lazy로 선언할 수 없다.

지연 프로퍼티는 인스턴스의 초기화가 완료될 때까지 값을 알 수 없는 외부 요인에 의해 초기값이 달라질 때 유용하다.

또한 프로퍼티의 초기값을 실제 사용전까지 수행하면 안되는 복잡하거나 계산 비용이 많은 드는 경우에도 유용하다.

class DataImporter {
  var filename = "data.txt"
}

class DataManager {
  lazy var importer = DataImporter()
  var data = [String]()
}

let manager = DataManager()
manager.data.append("Some data")
manager.data.append("Some more data")

DataManager 클래스는 String 값의 빈 배열인 data 저장 프로퍼티를 가지고 있다. DataManager 클래스의 목적은 String 배열을 관리하고 접근하는 기능을 제공하기 위함이다.

이때 importer를 통해서 파일로부터 데이터를 가져올 수 있다고 하고, 이 기능은 DataImporter 클래스를 통해서 제공되며, 인스터스가 초기화될 때 파일을 열고 메모리로 내용을 읽어야하기 때문에 초기화에 시간이 많이 든다고 가정하자.

DataManager 인스턴스는 파일로부터 데이터를 가져올 필요없이 데이터를 관리할 수 있으므로, 초기화되는 시점에 꼭 DataImporter 인스턴스를 생성할 필요는 없다. 대신 처음 사용하는 경우에 생성하는 것이 더 효율적일 것이다.

print(manager.importer.filename)
// 위와 같이 실제로 filename 프로퍼티를 조회할 때 처음 생성되게 된다. 

DataImporter 클래스의 인스턴스인 importer는 lazy 가 붙어있으므로 위 예시와 같이 실제로 처음 접근 될 때 생성되게 된다.

lazy 가 붙은 프로퍼티는 여러 쓰레드에서 동시에 접근되고 프로퍼티가 아직 초기화되지 않은 경우,

프로퍼티가 한 번만 초기화 된다는 보장이 없으니 주의해야한다. 🚨


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

Objective-C에서는 클래스 인스턴스의 값과 참조를 저장하는 2가지 방법을 제공하는 것을 알 것이다.

추가로 프로퍼티에 저장된 값의 백업 저장소로서 인스턴스 변수를 사용할 수 있다. ?!

반면 Swift에서는 이러한 컨셉을 단일 프로퍼티 선언으로 통합했다.

Swift 프로퍼티는 일치하는 인스턴스 변수가 없으며, 프로퍼티의 백업 저장소에 직접적으로 접근할 수 없다.

이러한 방식은 다른 컨텍스트에서 값에 접근하는 것에 대한 혼동을 피할 수 있으며 프로퍼티 선언을 하나의 명확한 구문으로 단순화 한다.

이름, 타입, 그리고 메모리 관리 특성을 포함한 프로퍼티에 대한 모든 정보는 타입의 정의에 부분으로 한 곳에서 정의된다.


연산 프로퍼티 (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 // -> 연산프로퍼티의 getter를 호출해서 값을 조회 (5.0, 5.0)
square.center = Point(x: 15.0, y: 15.0) // -> 연산프로퍼티의 setter를 호출하고 값을 변경
print("\(square.origin.x), \(square.origin.y)") // (10.0, 10.0)

Rect 의 중앙 위치는 항상 origin, size를 통해서 계산될 수 있기에 상수로 저장할 필요없이 연산프로퍼티를 활용할 수 있다.

center 프로퍼티 설정은 center 의 setter를 호출하고 origin 에 저장된 x ,y 값을 변경하고 사각형을 새로운 위치로 이동시킨다.


짧은 Setter 선언 (Shorthand Setter Declaration)

연산 프로퍼티의 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 { // 👈🏻 이름이 설정되지 않았고, 그래서 기본 네이밍인 newValue가 사용되게 된다
            origin.x = newValue.x - (size.width / 2)
            origin.y = newValue.y - (size.height / 2)
        }
    }
}

짧은 Getter 선언 (Shorthand Getter Declaration)

getter의 바디가 단일 표현식이라면 return을 생략해도 된다.

struct CompactRect {
    var origin = Point()
    var size = Size()
    var center: Point {
        get { // 👈🏻 단일로 표현될 수 있기 때문에 이때는 return 없이 작성 가능
            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)

setter가 없고 getter만 있는 연산 프로퍼티를 의미한다.

읽기 전용 연산 프로퍼티는 항상 값을 반환하고, 점(dot) 구문으로 접근할 수 있지만 다른 값을 설정할 수는 없다.

읽기 전용 연산프로퍼티와 연산프로퍼티는 값이 고정되지 않기 때문에 var 프로퍼티 변수로 선언 되어야한다.

let 은 인스터스 초기화로 한 번 설정되면 값을 변경할 수 없는 오직 상수 프로퍼티에서만 사용된다.

읽기 전용 연산 프로퍼티는 get 키워드와 중괄호를 제거하고 간편하게 선언이 가능하다.

struct Cuboid {
    var width = 0.0, height = 0.0, depth = 0.0
    var volume: Double { // 👈🏻 get, 중괄호 제거 가능
        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 값에 width, height, depth 의 어떤 값을 사용해야 하는지 모호하므로 volume 을 설정하는 것은 의미가 없다.

그럼에도 불구하고 현재 계산된 부피를 알 수 있도록 외부에 읽기전용 계산된 프로퍼티로 제공되는 Cuboid 구조체는 유용하다.


프로퍼티 관찰자 (Property Observers)

프로퍼티 값이 변경되는지 관찰하고 응답한다.

프로퍼티의 현재 값이 새로운 값과 같더라도 프로퍼티의 값이 설정될 때 호출된다.

  • 정의한 저장 프로퍼티
  • 상속한 저장 프로퍼티
  • 상속한 연산 프로퍼티

위의 경우에 프로퍼티 관찰자를 붙일 수 있다.

이때 상속된 프로퍼티들의 경우에는 하위 클래스의 프로퍼티를 오버라이드(재정의)하면서 프로퍼티 관찰자를 추가한다.

정의한 연산 프로퍼티의 경우에는 프로퍼티 관찰자를 추가하는 것이 아니라 setter를 사용해 값이 변하는 것을 관찰하고 응답할 수 있다.

프로퍼티 관찰자를 정의하는 방법은 아래 2가지 이며, 둘 다 정의할 수 있다.

  • willSet : 값이 저장되기 직전에 호출
  • didSet: 새로운 값이 저장되자마자 호출

상위 클래스 프로퍼티의 willSetdidSet 옵져버는 상위 클래스의 initializer가 호출된 이후, 하위 클래스의 initializer에서 프로퍼티 값을 설정할 때 호출된다.

willSet, didSet 은 클래스가 자체 속성을 설정하는 동안, 즉 상위 클래스의 initializer가 호출되기 전에는 호출되지 않는다.

initializer 전파에 대해서는 [Initializer Delegation for Value Types](https://docs.swift.org/swift-book/LanguageGuide/Initialization.html#ID215) 와 [Initializer Delegation for Class Types](https://docs.swift.org/swift-book/LanguageGuide/Initialization.html#ID219)를 참고해라.

class StepCounter {
  var totalSteps: Int = 0 {
    willSet(newTotalSteps) { // 👈🏻 새로운 값이 상수 파라미터로 전달되고, 네이밍을 할 수 있고 default는 newValue
      print("곧 총 걸음수는 \(newTotalSteps)이 될 것이다.")
    }
    didSet { // 👈🏻 이전 값이 상수 파라미터로 전달되고, 네이밍을 할 수 있고 default는 oldValue
      if totalSteps > oldValue {
        print("\(totalSteps - oldValue)걸음이 추가되었습니다.")
        // 만약 여기 내부에서 totalSteps에 값을 할당한다고 했을 때, willSet, didSet이 불리지는 않고 값은 할당됨
      }
    }
  }
}

let stepCounter = StepCounter()
stepCounter.totalSteps = 200 
// 곧 총 걸음수는 200이 될 것이다.
// 200 걸음이 추가되었습니다.
stepCounter.totalSteps = 360
// 곧 총 걸음수는 360이 될 것이다.
// 160 걸음이 추가되었습니다.
stepCounter.totalSteps = 896
// 곧 총 걸음수는 896이 될 것이다.
// 536 걸음이 추가되었습니다.

프로퍼티 옵져버 프로퍼티를 in-out 파라미터로 함수에 전달하면 willSet, didSet 은 항상 호출된다.

이는 in-out 파라미터에 대한 copy-in-copy-out 메모리 모델 때문이고, 이때 값은 항상 함수의 마지막에 프로퍼티에 써지므로 항상 호출되게 된다.

자세한 내용은 [In-Out Parameters](https://docs.swift.org/swift-book/ReferenceManual/Declarations.html#ID545) 보자


프로퍼티 래퍼 (Property Wrapper)

프로퍼티 래퍼는 프로퍼티가 저장되는 방법을 관리하는 코드와 프로퍼티를 정의하는 코드 사이에 분리 계층을 추가한다.

예를 들어 스레드 안전성 검사를 제공하거나, 데이터를 db에 저장해야하는 프로퍼티가 있는 경우에 모든 프로퍼티에 해당 코드를 작성해야한다.

이때 프로퍼티 래퍼를 사용하면 이러한 관리 코드를 랩퍼를 정의할 때 한 번만 작성하면되고, 이를 여러 프로퍼티들에 적용하여 재사용할 수 있다.

프로퍼티 래퍼를 정의하기 위해서는 wrappedValue 프로퍼티를 정의하는 구조체, 열거형, 클래스를 생성한다.

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

TwelveOrLess 구조체는 래핑하는 값이 항상 12와 같거나 더 작은 숫자를 가지게 된다.

numberTwelveOrLess 의 구현에서만 사용될 수 있도록 private 접근제한을 가진다.

즉, 다른 곳에서 작성된 코드는 number 값에 wrappedValue 의 getter와 setter를 통해서만 접근이 가능하고 직접적으로는 사용할 수 없게 한다.

이에 관해서는 [Access Control](https://docs.swift.org/swift-book/LanguageGuide/AccessControl.html)를 더 살펴보자.

struct SmallRectangle {
  @TwelveOrLess var height: Int // 👈🏻 프로퍼티 랩퍼는 변수 선언할 때 앞쪽에 래퍼의 이름을 작성
  @TwelveOrLess var width: Int
}

var rectangle = SmallRectangle()
print(rectangle.height) // 0

rectangle.height = 10
print(rectangle.height) // 10

rectangle.height = 24
print(rectangle.height) // 12

프로퍼티 래퍼를 적용할 때 컴파일러는 래퍼를 위한 저장소를 제공하는 코드와 레퍼를 통해 프로퍼티에 접근하는 코드를 합성한다.

프로퍼티 래퍼는 래핑된 값을 저장하는 역할을 하기 때문에 이에 대한 합성 코드는 없다.

이때 special attribute syntax의 이점을 사용하지 않고도 프로퍼티 래퍼의 기능을 사용하는 코드를 작성할 수 있다.

(여기서 special attribute syntax가 @TwelveOrLess 이거 말하는 것 같음)

struct SmallRectangle {
  private var _height = TwelveOrLess() // 👈🏻 프로퍼티 랩퍼를 사용하지 않고 TwelveOrLess 구조체에 명시적으로 프로퍼티를 래핑하는 방법
  private var _width = TwelveOrLess()
  var height: Int {
    get { _height.wrappedValue }
    set { _height.wrappedValue = newValue }
  }
  var width: Int {
    get { _width.wrappedValue }
    set { _width.wrappedValue = newValue }
  }
}

이때 _height, _width 프로퍼티는 프로퍼티 래퍼의 인스턴스인 TwelveOrLess 를 저장한다.

그리고 height, width에 대한 getter, setter는 wrappedValue 프로퍼티에 접근한다.


래핑된 프로퍼티를 위한 초기값 설정 (Setting Initial Values for Wrapped Properties)

프로퍼티 래퍼를 사용한 코드는 TwelveOrLess 로 래핑된 프로퍼티에 다른 초기값을 지정할 수 없다.

예를 들어 SmallRectangle에 정의한 height, width의 초기값을 설정할 수가 없다는 것이다.

따라서 초기값 설정을 하려면 프로퍼티 래퍼 코드에 초기값 설정 코드를 추가해야한다.

@propertyWrapper
struct SmallNumber {
  private var maximum: Int
  private var number: Int
  
  var wrappedValue: Int {
    get { return number }
    set { number = min(newValue, maximum) }
  }
  
  init() { // 👈🏻 case1: 프로퍼티 래퍼를 적용할 때 초기값을 주지 않으면 요거 호출
    maximum = 12
    number = 0
  }
  
  init(wrappedValue: Int) { // 👈🏻 case2: wrappedValue만 넘겨주는 경우 호출
    maximum = 12
    number = min(wrappedValue, maximum)
  }
  
  init(wrappedValue: Int, maximum: Int) { // 👈🏻 case3: wrappedValue, maximum 둘 다 넘겨주는 경우 호출
    self.maximum = maximum
    number = min(wrappedValue, maximum)
  }
}

// case1
struct ZeroRectangle {
  @SmallNumber var height: Int
  @SmallNumber var width: Int 
}
var zeroRectangle = ZeroRectangle()
print(zeroRectangle.height, zeroRectangle.width) // "0 0"

// case2
struct UnitRectangle {
  @SmallNumber var height: Int = 1
  @SmallNumber var width: Int = 1
}
var unitRectangle = UnitRectangle()
print(unitRectangle.height, unitRectangle.width) // "1 1"

// case3
struct NarrowRectangle {
  @SmallNumber(wrappedValue: 2, maximum: 5) var height: Int
  @SmallNumber(wrappedValue: 3, maximum: 4) var width: Int
}
var narrowRectangle = UnitRectangle()
print(narrowRectangle.height, narrowRectangle.width) // "2 3"

narrowRectangle.height = 100
narrowRectangle.width = 100
print(narrowRectangle.height, narrowRectangle.width) // "5 4"

프로퍼티 래퍼 인자를 포함하면, 할당을 통해서 초기값을 선언할 수 있다.

Swift는 할당을 마치 wrappedValue 인자로 취급하고, 이 인자를 받을 수 있는 초기화를 사용하게 된다.

struct MixedRectangle {
  @SmallNumber var height: Int = 1 // 👈🏻 매개변수로 넘겨주는 것이 아니라 초기값 설정으로 초기화
  // SmallNumber(wrappedValue: 1)를 호출한 것과 같음
  
  @SmallNumber(maximum: 9) var width: Int = 2 // 👈🏻 매개변수도 넘기고, 초기값도 설정으로 초기화
  // SmallNumber(wrappedValue: 2, maximum: 9)를 호출한 것과 같음
}

var mixedRectangle = MixedRectangle()
print(mixedRectangle.height) // "1"

mixedRectangle.height = 20
print(mixedRectangle.height) // "12"

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

프로퍼티 래퍼는 래핑된 값 외에도 **투영된 값(projected value)**를 정의해서 추가적인 기능을 노출할 수 있다.

아래의 db 접근을 관리하는 프로퍼티 래퍼는 투영 값으로 flushDatabaseConnect() 메서드를 노출한다.

@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) // "false" 👈🏻 래퍼의 투영된 값에 접근 `$` 표시로 이를 표현!!

someStructure.someNumber = 55
print(someStructure.$someNumber) // "true" 👈🏻 래퍼의 투영된 값에 접근

프로퍼티 래퍼는 투영된 값으로 어떤 타입의 값도 반환할 수 있다.

위 예시에서는 프로퍼티 래퍼가 단지 숫자의 변경에 대한 정보만 노출하기에 투영된 값도 부울 값을 노출하게 된다.

그러나 더 많은 정보의 노출이 필요하다면 다른 데이터 타입의 인스턴스 반환이나 투영된 값으로 래퍼의 인스턴스를 노출하기 위해 self 를 반환할 수 있다.

투영된 값에 접근할 때 프로퍼티 getter나 인스턴스 메서드처럼 다른 프로퍼티에 접근하듯 self. 은 생략이 가능하다.

enum Size {
  case small, large
}

struct SizedRectangle {
  @SmallSize var height: Int
  @SmallSize 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
  }
}

프로퍼티 래퍼 syntax는 getter, setter가 있는 프로퍼티 syntax로 width, height에 접근은 다른 프로퍼티 접근과 동일하다.

따라서 resize(to: .large) 는 width, height에 접근해 100, 100으로 바꾸지만 이때 둘 다 12보다 큰 값으로 설정하는 것을 막고 변경 사실을 기록하기 위해 투영값이 true로 변경되게 된다.

resize()의 반환 구문은 프로퍼티 래퍼가 height, width를 변경했는지 판단하기 위해서 각각의 투영값을 체크하게 된다.


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

  • 지역변수: 함수, 메서드, 클로저 컨텍스트 내에 정의된 변수
  • 전역변수 : 함수, 메서드, 클로저, 타입 컨텍스트의 외부에 정의된 변수

전역, 지역변수는 모두 저장된 변수(stored variables)이다.

즉, 저장 프로퍼티처럼 저장된 변수는 타입의 값을 위한 저장소를 제공하고 값을 설정하고 조회하는 것이 가능하다.

또한 전역, 지역변수도 연산된 변수(computed variables)와 저장된 변수를 위한 관찰자 정의가 가능하다.

이때 연산된 변수는 값을 저장하기보다는 연산 프로퍼티처와 같은 방법으로 작성된다.

전역 상수와 전역 변수는 [Lazy Stored Properties](https://docs.swift.org/swift-book/LanguageGuide/Properties.html#ID257) 와 유사하게 항상 lazy하게 계산된다.

그러나 lazy stored property와 다르게 lazy 키워드를 작성하지는 않아도 된다.

🚨 지역 상수와 지역 변수는 절대 lazy하지 않음

저장된 지역 변수(local stored variable)에는 프로퍼티 래퍼를 적용할 수 있지만

전역 변수(global variable), 연산된 변수(computed variable)에는 프로퍼티 래퍼를 적용할 수 없다.

func someFunction() {
  @SmallNumber var myNumber: Int = 0 // 👈🏻 저장된 지역변수는 프로퍼티 래퍼 사용가능
  
  myNumber = 10 // myNumber는 10
  myNumber = 24 // myNumber는 12
}

타입 프로퍼티 (Type Property)

  • 인스턴스 프로퍼티 : 특정 타입의 인스턴스에 속하는 프로퍼티
    • 새로운 인스턴스가 생성될 때 마다 다른 인스턴스와 별도의 고유한 프로퍼티 값을 설정하게 됨
  • 타입 프로퍼티 : 타입 자체에 속하는 프로퍼티
    • 인스턴스 수에 관계없이 이 프로퍼티의 복사본은 하나만 존재

타입프로퍼티는 C의 static 상수처럼 모든 인스턴스에서 사용할 수 있는 프로퍼티 상수나,

static 변수처럼 타입에 모든 인스턴스에 전역인 값을 저장하는 프로퍼티 변수와 같이 특정 타입의 모든 인스턴스에 보편적인 값을 정의하는데 유용하다.

저장된 인스턴스 프로퍼티(stored instance properties)와 다르게 저장된 타입 프로퍼티(stored type properties)는 항상 기본값이 있어야한다. 왜냐하면 초기화 시 저장된 타입 프로퍼티에 값을 할당할 수 있는 초기화를 가지고 있지 않기 때문이다.

저장된 타입 프로퍼티는 처음 접근될 때 lazy하게 초기화된다.

여러 스레드가 동시에 접근할 때도 한 번만 초기화 되도록 보장하며, lazy 키워드가 필요하지는 않다.


타입 프로퍼티 구문 (Type Property Syntax)

C, Objective-C에서는 전역 static 변수로 타입과 연관된 static 상수와 변수를 정의한다.

Swift에서 타입 프로퍼티는 타입 내부의 정의부분에 작성되며, 각 타입 프로퍼티는 지원되는 타입으로 명시적으로 범위가 지정된다.

struct SomeStructure {
  static var storedTypeProperty = "Some value." // 👈🏻 static을 사용해 타입프로퍼티 정의
  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 { // 👈🏻 class 키워드 사용해 하위 클래스에서 상위 클래스의 구현을 재정의 가능
    return 107
  }
}

위 예시에서 연산 타입 프로퍼티(computed type property)는 read-only 타입프로퍼티이지만, 실제로는 연산 인스턴스 프로퍼티(computed instance property)처럼 read-write computed type properties로 작성할 수 있다.


타입 프로퍼티 조회와 설정 (Querying and Setting Type Property)

. 구문을 통해서 접근하고 설정한다.

해당 타입의 인스턴스가 아닌 타입자체에 대해 접근하고 설정한다.

print(SomeStructure.storedTypeProperty) // "Some value."
SomeStructure.storedTypeProperty = "Another value."
print(SomeStructure.storedTypeProperty) // "Another value."

print(SomeEnumeration.computedTypeProperty) // "6"
print(SomeClass.computedTypeProperty) // "27"

여러 오디오 채널에 대해 오디오 레벨 미터를 모델링하는 구조체에 일부분으로 2개의 저장된 타입 프로퍼티를 사용한다.

위 그림은 오디오 채널 중 2개를 결합해 스테레오 오디오 레벨 미터를 모델링하는 방법을 나타낸다.

레벨이 0이라면 해당 채널의 조명은 켜지지 않고, 레벨이 10이되면 모든 채널의 조명이 켜진다.

그림에서 왼쪽 채널은 현재 9의 레벨을, 오른쪽 채널은 7의 레벨을 가진다.

struct AudioChannel {
  static let thresholdLevel = 10 // 👈🏻 오디오 레벨이 가질 수 있는 최대값 정의
  static var maxInputLevelForAllChannels = 0 // 👈🏻 AudioChannel인스턴스로부터 받은 최대 입력값을 추적
  var currentLevel: Int = 0 { // 👈🏻 현재 오디오 레벨을 0~10으로 표현하는 저장된 인스턴스 프로퍼티
    didSet {
      if currentLevel > AudioChannel.thresholdLevel { // 10보다 큰 값이 오면 10으로 설정
        currentLevel = AudioChannel.thresholdLevel
      } 
      if currentLevel > AudioChannel.maxInputLevelForAllChannels { // 이전 값보다 큰 currentLevel 값이 들어오면 새로운 currentLevel 값을 maxInputLevelForAllChannels 타입프로퍼티에 저장
         AudioChannel.maxInputLevelForAllChannels = currentLevel
      }
    }
  }
}

위의 didSet에서 첫번째 if 문에서 currentLevel에 다른 값을 설정해주는데, 이때 관찰자가 다시 호출되지는 않는다.

var letfChannel = AudioChannel()
var rightChannel = AudioChannel()

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

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

Metadata

Metadata

Assignees

Labels

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions