Swift study - Properties

rbw·2022년 3월 15일
0

swift-study

목록 보기
13/17
post-thumbnail

Swift - Properties

프로퍼티는 값을 특정 클래스, 구조체 또는 열거형으로 연결한다. 저장 프로퍼티는 인스턴스의 한 부분으로서, 상수 또는 변수의 값을 저장하고 반면 계산된 프로퍼티는 값을 계산(저장하기 보다는)한다. 계산된 프로퍼티는 클래스, 구조체 그리고 열거형에 의해 제공된다. 저장 프로퍼티는 오직 클래스와 구조체에서만 제공 된다.

저장, 계산된 프로퍼티는 주로 특정 타입의 인스턴스로 연결한다. 하지만 프로퍼티는 타입 자체로 연결될 수도 있다. 이러한 속성을 타입 프로퍼티라고 한다.

추가로, 프로퍼티의 값이 변하는 것을 감시하기 위한 관측자로 프로퍼티를 정의할 수 있다. 프로퍼티 관찰자는 저장 프로퍼티에 추가될 수 있으며 또한 하위 클래스가 상위 클래스에서 상속하는 프로퍼티에도 추가할 수 있다.

프로퍼티 래퍼를 사용하여 여러 프로퍼티의 getter or setter에 코드를 재사용 가능하다.

Stored Properties (저장된 프로퍼티)

가장 간단한 형태로, 저장 프로퍼티는 특정 클래스 또는 구조체의 인스턴스의 일부로 저장되는 상수나 변수이다. 저장 프로퍼티는 변수나 상수를 저장 가능하다. (var,let 키워드를 사용하여)

선언의 일부로 저장 프로퍼티에 기본 값을 제공할 수 있다. 또한 초기화 동안 저장프로퍼티의 초기 값의 설정과 변경도 설정 가능하다. 상수 저장 프로퍼티에도 같이 동작한다.

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

위의 예에서 길이 값은 상수이므로 지정이된다면 변할 수 없다.

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

이 동작은 구조체가 값 타입이기 때문이다. 인스턴스의 값 타입이 상수라면, 모든 프로퍼티도 마찬가지이다.

클래스에서는 같지 않다, 클래스는 참조 타입이기 때문이다. 클래스의 인스턴스를 상수로 할당해도, 인스턴스의 변수 저장 프로퍼티는 변경이 가능하다.

Lazy Stored Properties (게으른 저장 프로퍼티)

게으른 저장 프로퍼티는 프로퍼티를 사용 전에는 초기값이 계산되지 않는 프로퍼티다. lazy 키워드로 사용이 가능하다.

NOTE

항상 게으른 저장 프로퍼티에는 변수로 선언해라, 왜냐하면 인스턴스 초기화가 완료될때 까지 초기값이 계산되지 않을 수 있기 때문이다. 상수 프로퍼티는 항상 초기화 전에 값을 갖기에 게으른 저장 프로퍼티로 선언 할 수 없다.

게으른 프로퍼티는 인스턴스 초기화 완료까지 값을 알 수 없는 외부요인에 의지하는 프로퍼티 초기값일 때 유용하다. 게으른 프로퍼티는 또한 필요할때까지 수행안해도 되는 복잡한 계산 설정인 프로퍼티나, 복잡한 프로퍼티의 초기값일때 유용하다.

class DataImporter {
    /*
    DataImporter는 외부 파일에서 다른 데이터를 받아오는 클래스이다.
    클래스는 초기화 하는데 상당한 시간이 걸린다고 가정한다.
    */
    var filename = "data.txt"
    // the DataImporter class는 데이터를 받아오는 기능을 제공한다.
}

class DataManager {
    lazy var importer = DataImporter()
    var data: [String] = []
    // the DataManager class는 데이터를 관리하는 기능을 제공한다.
}

let manager = DataManager()
manager.data.append("Some data")
manager.data.append("Some more data")
// 아직 importer는 만들어지지 않았다.

기능을 다 작성하지는 않았지만, DataManager클래스의 목적은 문자열 배열의 접근과 관리하는 기능을 제공한다.

DataImporter 클래스의 인스턴스를 가지고 있고, 이는 파일을 열 필요가 있을 때 실행이 되고 인스턴스 초기화 될 때 메모리로부터 요소들을 읽어온다.

파일로 부터 데이터를 받아오지 않아도 데이터의 조작은 가능하기 때문에, 데이터 매니저는 새 데이터임포터를 스스로 만들지는 않는다. 대신에, 데이터 임포터 인스턴스가 처음 사용될 때 만드는 것이 더 합리적이다.

print(manager.importer.filename)
// 이제 importer 프로퍼티에 클래스 인스턴스가 생성이 되었다.
// Prints "data.txt"

NOTE

만약 게으른 프로퍼티가 여러 스레드에서 동시에 접근이 되고, 프로퍼티가 아직 초기화 되지 않았다면, 그 프로퍼티가 단 한번만 초기화 된다는 보장은 없다.

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

Objective-C 경험이 있다면, 클래스 인스턴스의 일부로서 참조와 값을 저장하는 두가지 방법이 있다는 것을 알것이다. 프로퍼티외에도, 프로퍼티에 저장된 값의 백업으로 인스턴스 변수를 사용할 수 있다.

스위프트에서는 하나의 프로퍼티 선언으로 위 컨셉들을 통일하였다. 스위프트 프로퍼티는 동일한 인스턴스 변수를 가지지 않고, 백업 프로퍼티에 직접 접근을 할 수 없다. 이 방식은 다른 문맥에서 값의 접근하는 충돌을 피하고 단일 정의문으로 프로퍼티 선언을 간단히 해준다. 모든 프로퍼티의 정보(이름, 타입, 메모리 관리 특성)는 하나의 위치에서 타입의 정의 일부로 정의된다.

Computed Properties (계산된 프로퍼티)

저장 프로퍼티 외에도, 클래스, 구조체 그리고 열거형은 계산된 프로퍼티를 정의 가능하다. 실제로는 값이 저장되지 않는다. 대신, 다른 속성과 값을 검색하고 설정하기 위해 getter, optional 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)"

위 예는 기하학적 모양과 함께 작업하는 세가지 구조체를 정의했다.

  • Point는 가로축과 세로축을 캡슐화하였다.
  • Size는 너비와 높이를 캡슐화함.
  • Rect는 포인트와 사이즈로 사각형을 정의함.

사각형 구조체는 또한 center로 불리는 계산된 프로퍼티를 제공한다. 사각형의 현재 중앙점은 항상 origin, size로 부터 지정되며, 따라서 명시적으로 중앙 값을 저장할 필요는 없다. 대신에 계산된 센터 값을 위한 커스텀 게터와 세터를 정의하며, 작업할시에 값이 실제로 저장된것 같이 보이게끔 해준다.

위의 예에서는 square라 불리는 새로운 사각형 변수를 만든다. 기존 값을 리턴하기 보다, 게터는 실제로 계산하고 새로운 점으로 리턴한다.

센터 속성을 설정하면 센터의 세터가 호출되어, origin 프로퍼티에 저장된 좌표의 값을 변경하고, 새로운 위치로 움직인다.

Shorthand Setter Declaration (축약된 세터 선언)

만약 계산된 프로퍼티의 세터가 새 값을 설정하기 위한 이름이 지정되지 않았다면, 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)
        }
    }
}

Shorthand Getter Declaration (짧은 게터 선언)

게터의 전체 내부를 하나의 표현식으로 만들면, 게터는 암묵적으로 반환한다. 아래 에시 참조

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

return 키워드를 생략한 게터는 함수와 형태가 같다.

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

세터없이 게터로 작성한 계산된 프로퍼티는 읽기 전용 계산된 프로퍼티로 알려져있다. 읽기 전용 계산된 프로퍼티는 항상 값을 반환하고, 점을 사용해서 접근이 가능하지만, 다른 값으로 설정은 불가하다.

NOTE

읽기 전용 기능을 포함한 계산된 프로퍼티는 반드시 변수로 설정해라, 값이 고정 되어 있지 않기 때문이다. 인스턴스 초기화의 일부로 한 번 설정되면 값을 변경할 수 없음을 나타내기에 상수 프로퍼티에만 사용된다.

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"

위 예에서는 volume 으로 읽기전용 계산된 프로퍼티를 가지며, 현재 부피를 리턴한다. 부피를 변하게 할 수는 없다. 그렇게 된다면 모호해진다 특정 부피를 계산하는 값들중 어떤 것을 사용할지는 모호하기 때문에 부피 설정을 가능하게 하는 것은 의미가 없다. 그럼에도 불구하고, 외부 유저에게 현재의 계산된 부피를 드러낼때 유용하다.

Property Observers (프로퍼티 관찰자)

프로퍼티 관찰자는 프로퍼티의 값이 변경되면 관측하고 응답한다. 프로퍼티 관찰자는 값이 설정될때마다 호출되고, 새로운 값이 현재 값이랑 같아도 호출이된다.

다음과 같은 위치에 추가가 가능하다.

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

상속된 프로퍼티에서, 하위클래스의 프로퍼티를 오버라이딩하여 프로퍼티 관찰자를 추가 가능하다. 계산된 프로퍼티에서, 관찰자를 만들려고 하는것 대신에 프로퍼티 세터로 값의 변경을 관찰하고 응답으로 사용해라.

프로퍼티에서 다음과 같은 옵션으로 관찰자를 정의 가능하다.

  • willSet는 값을 저장하기 전에 호출된다.
  • didSet는 새로운 값이 저장된 즉시 호출된다.

willSet 관찰자로 구현한다면, 상수 파라미터로서 새로운 값을 전달한다. 구현의 일부로 파라미터의 이름을 지정 가능하다. 파라미터의 이름과 구현 내부에서 괄호를 작성하지 않는다면, 파라미터는 newValue 라는 이름으로 기본 파라미터가 사용가능하다.

비슷하게 didSet 관찰자로 구현한다면, 이전 값을 상수 파라미터에 포함하여 전달한다. 기본 파라미터로 설정한다면 oldValue로 설정된다. 이 관찰자 내부 프로퍼티에 값을 할당한다면, 할당한 새로운 값이 방금 설정했던 값을 대체된다.

NOTE

부모클래스 프로퍼티의 위 두가지 관찰자는 부모클래스 초기화가 호출된 후에, 프로퍼티가 자식클래스 초기화 내에서 설정시 호출됩니다. 슈퍼클래스 초기화가 호출되기 전에 클래스 자체 프로퍼티가 설정되는 동안은 호출되지 않는다.

아래의 걷는동안의 단계를 체크하는 예제로 살펴보겠다.

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

위 관찰자를 이용해서 총 걸음수와 추가된 걸음수를 관찰하여 리턴이 가능하다. didSet에는 기본 파라미터를 사용하였다.

NOTE

만약 관찰자가 있는 속성을 인-아웃 파라미터로서 험수에 전달한다면, 위 두가지 관찰자들은 항상 호출된다. 인-아웃 파라미터에 대한 카피-인, 카피-아웃 메모리 모델이기 때문이다. 값은 항상 함수의 종료시점에서 다시 작성된다.

Property Wrappers (프로퍼티 래퍼)

프로퍼티 래퍼는 프로퍼티가 저장되는 방식을 관리하는 코드와 프로퍼티를 정의하는 코드 사이에 분리 계층을 추가한다. 예를 들어, 스레드-안전성의 체크를 제공하는 프로퍼티와 데이터베이스에 데이터를 저장하는 프로퍼티를 가지고 있다면, 모든 코드내에 프로퍼티에 작성해야 한다. 프로퍼티 래퍼를 사용할 때, 래퍼를 정의할 때 관리코드를 작성하고 여러 프로퍼티에서 적용함으로써 관리코드를 재사용이 가능하다.

프로퍼티 래퍼를 정의하기위해, wrappedValue 프로퍼티가 정의된 구조체, 열거형 또는 클래스를 만들어야한다.

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

세터는 새로운 값이 12보다 작거나 같은 값을 보장하고, 게터는 저장된 값을 리턴한다.

NOTE

위의 예에서 private 로 변수에 붙였다. 이는 구현부 내부에서만 사용이 가능하게 보장해준다. 직접 변수에 접근이 불가하며, 값은 게터와 세터로만 접근이 가능하다.

프로퍼티로 래퍼의 이름을 프로퍼티 전에 작성을 함으로써 프로퍼티에 래퍼가 적용된다. 밑의 예제.

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"

값을 저장하기전에 유효한 값인지 프로퍼티 래퍼를 사용해서 검사하고 있다.

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

특별한 프로퍼티 구문을 포함하지 않고, 프로퍼티 래퍼의 동작을 사용하는 코드를 작성할 수 있다. 밑의 예제.

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 (래핑된 프로퍼티에 대한 초기값 설정)

위 코드에서는 number를 주어서 초기값으로 설정하였다. 이 프로퍼티 래퍼를 사용하는 코드는 래핑된 프로퍼티에 대한 다른 초기값을 지정할 수 없다. 예를들어, SmallRectangle의 정의에서 높이와 너비에 초기값을 줄 수 없다. 초기값을 설정하거나 다른 사용자화를 지원하기 위해, 프로퍼티 래퍼는 초기화가 필요로하다. 밑의 예제.

@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()`로 래퍼를 설정한다.
struct ZeroRectangle {
    @SmallNumber var height: Int
    @SmallNumber var width: Int
}

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

// 초기 값을 1개 받은 경우
struct UnitRectangle {
    @SmallNumber var height: Int = 1
    @SmallNumber var width: Int = 1
}

var unitRectangle = UnitRectangle()
print(unitRectangle.height, unitRectangle.width)
// Prints "1 1"

// 래퍼를 가진 프로퍼티에 값을 두개 넣는 경우
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"
// 맨 위에서 맥시멈 값이 5,4로 들어갔기 때문에 위의 출력결과가 나온다.

// 인자하나와 할당값을 같이 적어줄 수도 있다.
struct MixedRectangle {
    @SmallNumber var height: Int = 1

    // SmallNumber(wrappedValue: 2, maximum: 9)와 같다.
    @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 (프로퍼티 래퍼에서의 값의 투영)

래퍼값 외에도, 프로퍼티 래퍼는 projected value를 정의함으로써 추가 기능을 노출 할 수 있다. 예로, 데이터베이스에 접근을 관리하는 프로퍼티 래퍼는 투영된 값으로 flushDatabaseConnection() 메소드를 노출할 수 있다. 투영 값의 이름으로는 래퍼값과 같고, 시작부분에 $를 붙이는 차이가 있다. 달러기호로 시작하는 프로퍼티를 프로퍼티를 정의 할 수 없기 때문에, 투영왼 값은 정의한 프로퍼티를 방해하지 않는다.

아래 코드는 프로퍼티 래퍼가 새 값을 저장하기 전에 프로퍼티에 대해 값을 조정했는지 여부를 추적하기 위해 투영된 값 프로퍼티를 추가했다.

@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"

프로퍼티 래퍼는 투영된 값으로서 어떤 타입이든지 반환할 수 있다. 좀 더 많은 정보를 리턴할 필요가 있는 래퍼는 다른 데이터 타입의 인스턴스로 리턴이 가능하거나, 투영된 값으로서 래퍼의 인스턴스를 노출하기 위한 self를 리턴할 수 있다.

타입의 일부 코드로 부터 투영된 값에 접근할 경우, 프로퍼티의 게터와 인스턴스 메소드와 같이 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
    }
}

프로퍼티 래퍼 구문은 단지 게터와 세터가 있는 프로퍼티에 문법적 설탕(suntactic sugar)이기 때문에, 높이와 너비에 접근하는 행동은 다른 프로퍼티에 접근하는것과 동일하다. 예로, 코드의 resize(to:)는 프로퍼티 래퍼를 사용하여 높이와 너비에 접근한다. resize(to: .large)로 호출하면, 값에 100을 설정하려고 하지만, 프로퍼티 래퍼가 12보다 큰 값은 방지하기 때문에 높이 너비는 12로 설정되고, 투영된 값은 true로 설정된다. resize(to:)가 끝이나면 높이 또는 너비를 조정하였는지에 대한 여부를 반환한다.

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

계산된 프로퍼티와 프로퍼티 관찰자는 또한 전역변수와 지역변수에도 사용이 가능하다. 전역 변수는 어떠한 함수, 메소드, 클로져, 타입 문맥의 밖에서 정의된 변수이다. 지역변수는 함수, 메소드 또는 클로저 문맥 내부에 작성된 변수이다.

이전 챕터에서 마주친 전역 변수와 지역 변수는 모두 저장된 변수이다. 저장된 프로퍼티와 같은 저장된 변수는 특정 타입에 대한 저장소를 제공하고, 해당 값을 설정 및 검색되게 한다.

그러나, 계산된 변수와 저장된 변수의 관찰자 또한 전역, 지역 스코프에서 정의가 가능하다. 계산된 변수는 값을 저장하기 보다는 계산하고, 계산된 프로퍼티와 같은 방식으로 작성한다.

NOTE

전역 상수와 변수는 항상 게으르게 계산된다. 계으른 저장된 프로퍼티와 다르게, 전역 상수와 변수는 lazy키워드가 필요하지는 않다. 지역 상수와 변수는 게으르지않다.

지역 저장 변수에 프로퍼티 래퍼를 적용 가능하지만, 전역 변수나 계산된 변수에는 불가하다. 아래의 예시.

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

    myNumber = 10
    // now myNumber is 10

    myNumber = 24
    // now myNumber is 12
}

프로퍼티에 적용하는것과 같이 래퍼를 사용하는 것이 유효하다.

Type Properties (타입 프로퍼티)

인스턴스 프로퍼티는 특정 타입의 인스턴스에 속한 프로퍼티이다. 특정 타입의 새 인스턴스를 만들때 마다, 다른 인스턴스와 분리된 고유한 프로퍼티 값을 설정한다.

또한 특정 타입의 인스턴스가 아닌, 타입 자체에 속한 프로퍼티를 정의할 수도 있다. 이러한 프로퍼티의 복사본은 오직 하나이고, 해당 타입의 인스턴스 숫자는 상관이 없다. 이러한 프로퍼티의 종류를 타입 프로퍼티라고 부른다.

타입 프로퍼티는 해당 타입의 인스턴스 전부에 만능인 값을 정의하기에 유용하다. (모든 인스턴스에 사용 가능한 상수 프로퍼티와, 특정 타입의 모든 인스턴스로 전역인 값이 저장된 변수 프로퍼티와 같은 값)

저장된 타입 프로퍼티는 변수 또는 상수이다. 계산된 타입 프로퍼티는 항상 변수 프로퍼티로 선언되고, 계산된 인스턴스 프로퍼티와 같은 방식이다.

NOTE

저장된 인스턴스 프로퍼티와 다르게, 항상 저장된 타입 프로퍼티에 기본 값을 할당해야한다. 이는 타입 스스로 초기화를 할 때 저장된 타입 프로퍼티로 값을 할당하는 초기화를 가지지 않기 때문이다.

저장된 타입 프로퍼티는 처음 접근시에 게으르게 초기화한다. 이는 오직 한번에 초기화를 보장하고, 다중 스레드에서 동시적으로 접근을 할때도 마찬가지이며, lazy 키워드가 필요하지 않다.

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

C, Objective-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
    }
}

NOTE

위 예제에 계산된 타입 프로퍼티는 읽기 전용 타입 프로퍼티지만, 계산된 인스턴스 프로퍼티의 동일한 구문으로 읽기-쓰기로 만들 수 있다.

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의 정수 오디오 레벨을 가지고 있다.

아래의 삽화는 오디오 채널 2개를 결합하여 스테레오 오디오 레벨 미터를 모델링 하는 방법을 보여준다. 채널의 오디오 레벨이 0인 경우, 해당 채널의 표시등이 켜지지 않는다. 오디오 레벨이 10인 경우 그 채널의 모든 빛은 켜진다. 이 특징에서 왼쪽 채널은 현재 레벨이 9이고, 오른쪽 채널은 현재 7인 상태이다.

struct AudioChannel {
    static let thresholdLevel = 10
    static var maxInputLevelForAllChannels = 0
    var currentLevel: Int = 0 {
        didSet {
            if currentLevel > AudioChannel.thresholdLevel {
                // 새 오디오 레벨을 임계값으로 제한
                currentLevel = AudioChannel.thresholdLevel
            }
            if currentLevel > AudioChannel.maxInputLevelForAllChannels {
                // 새로운 맥시멈 입력 값으로서 저장
                AudioChannel.maxInputLevelForAllChannels = currentLevel
            }
        }
    }
}

오디오 채널 구조체는 위 기능을 지원하기 위한 두가지 저장된 타입 프로퍼티를 정의한다. 먼저, 임계값레벨을 지정하고, 이는 오디오레벨이 가질 수 있는 최대 값을 의미한다. 또, 모든 오디오 채널 인스턴스에서 10의 상수 값을 가진다. 만약 오디오 시그널이 10보다 높은 값이 온다면, 10으로 제한을 할 것이다.

두번째 타입 프로퍼티는 변수 저장된 프로퍼티인 maxInputLevelForAllChannels이다. 이는 최대 입력 어느 오디오 채널 인스턴스에 의해 받아온 값의 추적을 유지한다. 이는 초기값 0으로 시작한다.

currentLevel 프로퍼티는 현재 값이 변할때마다 체크하기 위해 didSet 프로퍼티 관찰자를 가진다. 이 옵저버는 두가지 체크를 한다.

  • currentLevel의 새로운 값이 임계값 레벨보다 높은 경우에, 이 프로퍼티 관찰자는 현재 값을 임계값으로 제한한다.
  • 어느 오디오 채널에 의해 이전에 받은 어떠한 값 보다 높은 currentLevel(제한된 후)에 새로운 값이 온다면, 프로퍼티 관찰자는 maxInputLevelForAllChannels 타입 프로퍼티 안의 값에 새로운 값을 저장한다.

NOTE

이 두가지 체크에서 처음에서, didSet 관찰자는 currentLevel에 다른 값을 저장한다. 이로인해 관찰자가 다시 호출되지는 않는다.

스테레오 소리 시스템의 오디오 레벨을 대표하는 두가지 구조체를 만들었다. currentLevel을 7로 설정한다면 maxInputLevelForAllChannels도 같이 7로 설정되는 것을 볼 수 있다.

11로 설정한다면, currentLevel 프로퍼티는 맥시멈 값인 10으로 제한되는것을 볼 수 있다.

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

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

rightChannel.currentLevel = 11
print(rightChannel.currentLevel)
// Prints "10"
print(AudioChannel.maxInputLevelForAllChannels)
// Prints "10"
profile
hi there 👋

0개의 댓글