공식 문서로 공부하는 Swift (9) - 프로퍼티

ci·2020년 5월 29일
1

Properties

프로퍼티(Property)는 클래스, 구조체, 열거형과 연관된 값이다. 저장 프로퍼티(stored property)는 인스턴스의 일부로써 상수나 변수를 저장한다. 계산 프로퍼티(computed property)는 값을 계산하는 기능을 한다. 저장 프로퍼티는 클래스와 구조체에서만 사용 가능하며, 계산 프로퍼티는 클래스와 구조체, 열거형 모두에서 사용할 수 있다.

프로퍼티는 보통 특정 타입의 인스턴스와 연관되어 있다. 타입 프로퍼티처럼 그 자신의 타입과 관련될 수도 있다.

프로퍼티 옵저버를 정의해서 프로퍼티의 값의 변화를 관찰할 수도 있다. 프로퍼티 옵저버는 저장된 프로퍼티, 또는 서브 클래스가 부모 클래스로부터 상속받은 프로퍼티에 더해질 수 있다.

여러 프로퍼티의 게터와 세터 코드를 재사용하기 위해 프로퍼티 래퍼를 사용할 수도 있다.



저장 프로퍼티 (Stored Properties)

저장 프로퍼티(stored property)는 특정 클래스나 구조체 인스턴스의 일부분으로써 저장된 상수 또는 변수이다. 저장 프로퍼티는 다른 변수/상수 저장 프로퍼티가 될 수도 있다.

저장 프로퍼티를 정의할 때 기본 값을 지정할 수도 있다. 초기화 과정에서 저장 프로퍼티에 초기 값을 설정하거나 수정하는 것도 가능하다. 이는 상수 저장 프로퍼티에도 허용된다.

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는 새로운 인스턴스가 만들어질 때 초기화되고 이후에는 수정할 수 없다.


상수 구조체 인스턴스의 저장 프로퍼티

구조체 인스턴스를 상수로 할당할 경우, 프로퍼티를 수정할 수 없다. 변수 프로퍼티도 불가능하다.

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 프로퍼티가 변수로 선언되었음에도 바꾸는 것이 불가능하다. 왜냐면 구조체는 값 타입(value type)이기 때문이다. 값 타입의 인스턴스가 상수로 지정되면, 그것의 모든 프로퍼티도 상수가 된다.

클래스는 참조 타입이기 때문에 다르다. 참조 타입을 상수로 선언해도 인스턴스의 변수 프로퍼티는 변경 가능하다.


지연된 저장 프로퍼티

지연된 저장 프로퍼티(lazy stored property)는 그것이 처음으로 사용되기 전까지는 초기 값이 계산되지 않는 프로퍼티다. 선언부 앞에 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

DataManager 클래스는 빈 문자열 배열로 초기화되는 data 저장 프로퍼티를 갖고 있다. DataManager의 목적은 문자열 데이터 배열에 대한 접근 또는 관리를 제공하는 것이다.

DataManager 클래스 기능의 일부는 파일로부터 데이터를 불러오는 것이다. 이 기능은 초기화하는 데 어느정도의 시간이 필요한 DataImporter 클래스로부터 제공된다. DataImporter 인스턴스는 파일을 열고, 그것의 내용을 메모리에 읽기 때문에 인스턴스를 초기화할 때 시간이 소요된다.

DataManager 클래스는 파일로부터 데이터를 불러오지 않아도 데이터를 다루는 것이 가능하다. DataManager 인스턴스를 만들 때 DataImporter 인스턴스까지 만들 필요가 없다는 뜻이다. 대신, DataImporter가 처음으로 사용될 때 그것을 만들게 하면 된다.

lazy 키워드가 표기됐기 때문에 importer 프로퍼티의 DataImporter 인스턴스는 프로퍼티에 처음접근할 때만 만들어진다.

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

만악 초기화되지 않은 lazy 프로퍼티가 여러 스레드에서 동시에 접근할 경우, 프로퍼티가 하나만 생성된다고 보장할 수 없다.


저장된 프로피티와 인스턴스 변수

Objective-C는 값을 저장하거나 클래스 인스턴스를 참조하기 위한 두 가지 방법을 제공한다. 점 연산자(instance.property = value)나 set 연산(instance.setProperty(value))을 사용한다. 뿐만 아니라 뿐만 아니라 메모리 관리와 관련한 개념도 프로퍼티에 함께 명시한다. @property (nonatomic, retain) NSString *propertyName와 같은 형태이다.

Swift는 이러한 개념을 하나의 프로퍼티 선언에 통합한다. Swift 프로퍼티는 해당 인스턴스 변수를 갖지 않고, 프로퍼티의 저장 공간에 직접적으로 접근하지 않는다. 이러한 접근법은 서로 다른 문맥에서 값에 접근하는 방식에 따른 혼란을 피하고, 프로퍼티의 선은을 하나의 일정한 구문으로 간소화 한다. 프로퍼티에 대한 모든 정보(이름, 타입, 메모리 관리 특성을 포함한)는 타입 정의의 일부분으로써 하나의 장소에 정의된다.



계산 프로퍼티 (Computed Properties)

클래스, 구조체, 열거형은 계산 프로퍼티(computed property)를 정의할 수 있다. 여기에는 실제 값이 저장되지 않는다. 대신 다른 프로퍼티와 값을 간접적으로 검색하고 설정할 수 있는 게터와 옵셔널 세터를 제공한다.

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는 x와 y좌표를 캡슐화한다.
  • Size는 너비와 높이를 캡슐화한다.
  • Rectorigin pointsize를 통해 사각형을 정의한다.

Rect 구조체는 계산 프로퍼티인 center를 제공한다. 처음 가운데 위치는 originsize에 의해 결정된다. 때문에 가운데 위치를 명시적인 Point 값으로 저장할 필요가 없다.대신 Rectcenter 계산 프로퍼티를 위한 커스텀 게터와 세터를 정의한다.

예시에서는 새로운 Rect 변수인 square를 만들었다. 이 변수는 (0, 0)origin point10widthheight로 초기화된다. 이 정사각형은 아래 그래프의 파란색 사각형과 같다.

center 프로퍼티는 새로운 값인 (15, 15)로 정해진다. 아래 그래프의 오렌지색 사각형과 같다. center 프로퍼티를 세팅하는 것은 center의 세터를 호출하고, origin 프로퍼티에 저장된 x, y 값을 수정한다. 정사각형은 새로운 위치로 옮겨진다.


computedProperties_2x

세터 선언 축약

계산 프로퍼티의 세터가 새로운 값의 이름을 정의하지 않으면, 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)
        }
    }
}

게터 선언 축약

게터의 전체 바디가 한 줄로 표현된다면 그 표현을 암시적으로 반환할 수 있다.

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

읽기 전용 계산 프로퍼티

게터는 있지만 세터는 없는 계산 프로퍼티를 읽기 전용 계산 프로퍼티(read-only computed property)라고 부른다. 읽기 전용 프로퍼티는 점 문법으로 접근할 수 있고 항상 값을 반환하지만, 새로운 값을 설정할 수는 없다.

읽기 전용 계산 프로퍼티를 포함한 모든 계산 프로퍼티는 반드시 변수로 선언되어야 한다. 이러한 값은 고정되어 있지 않기 때문이다. 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)

프로퍼티 옵저버(property observer)는 프로퍼티 값의 변화를 관찰하고 거기에 반응한다. 프로퍼티 옵저버는 프로퍼티 값이 설정될 때마다 호출된다. 현재 값과 똑같은 값이 들어오더라도 마찬가지다.

지연된 저장 프로퍼티를 제외한 모든 저장 프로퍼티에 프로퍼티 옵저버를 더할 수 있다. 부모클래스로부터 오버라이딩한 모든 프로퍼티에도 가능하다. 오버라이드되지 않은 계산 프로퍼티를 위한 프로퍼티 옵저버를 정의할 필요는 없다. 계산 프로퍼티의 세터가 값의 변화를 이미 관찰하고 있기 때문이다.


프로퍼티 옵저버엔 두 가지 옵션이 있다.

  • willSet은 값이 저장되기 이전에 호출된다.
  • didSet은 새로운 값이 저장된 이후에 즉시 호출된다.

willSet 옵저버를 구현했다면 새 값의 매개 변수 이름을 지정할 수 있는데, 지정하지 않으면 기본 값으로 newValue를 사용한다. didSet 옵저버를 구현한다면 바뀌기 전의 매개 변수 이름을 정할 수 있다. 지정하지 않으면 oldValue가 기본값으로 사용된다.

자식 클래스에서 특정 프로퍼티의 값을 설정했을 때, 부모 클래스의 initializer가 호출된 후 willSet, didSet 프로퍼티 옵저버가 실행된다. 부모 클래스에서 프로퍼티를 변경하는 것도 마찬가지로 부모 클래스의 initializer가 호출된 후 옵저버가 실행된다.


아래는 willSetdidSet을 사용하는 예시를 보여 준다. 새로운 클래스 StepCounter를 정의한다. 보수계 또는 다른 걸음 계산기로부터 들어오는 데이터를 사용하여 하루 루틴에서의 운동을 추적한다.

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

함수의 만약 인아웃 매개 변수에 프로퍼티를 넘기면 willSetdidSet은 항상 호출된다. 인아웃 매개 변수에서는 프로퍼티가 항상 복사되기 때문이다. 함수가 끝날 때 프로퍼티는 원래 값에 새 값을 덮어 쓰게 된다.



프로퍼티 래퍼 (Property Wrapper)

프로퍼티 래퍼(property wrapper)는 프로퍼티가 저장되는 방식을 다루는 코드와 프로퍼티를 정의하는 코드를 분리하는 단계를 더한다. 예를 들어, 스레드 안전 체크를 하거나 데이터베이스에 있는 데이터를 저장하는 프로퍼티가 있다면, 모든 프로퍼티에 그러한 코드를 작성해야 한다. 프로퍼티 래퍼를 사용한다면, 래퍼를 정의할 때 관리 기능을 하는 코드는 한 번만 작성하면 된다. 그리고 여러 프로퍼티에 그 코드를 적용함으로써 재사용할 수 있다.

wrappedValue 프로퍼티를 정의하여 프로퍼티 래퍼를 만든다. 아래의 코드에서, TwelveOrLess 구조체는 그것이 감싸는 값이 항상 12 이하의 숫자를 포함한다는 것을 보장한다.

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

세터는 새로운 값이 항상 12 이하임을 보장하고, 게터는 저장된 값을 반환한다.

위 예에서 number의 선언은 private으로 표시되었다. number 프로퍼티는 오직 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"

heightwidth 프로퍼티는 TwelveOrLess로부터 초기 값을 갖게 된다. 숫자 10을 rectangle.height에 저장하는 것은 성공하지만, 24를 저장하려 하면 세터의 규칙에 의해 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 }
    }
}

_height_width 프로퍼티는 TwelveOrLess 프로퍼티 래퍼 인스턴스를 저장한다. heightwidth에 대한 게터와 세터로 wrappedValue 프로퍼티에 접근할 수 있다.


포장된 프로퍼티 (Wrapped Properties)에 초기 값 설정하기

위의 예시는 TwelveOrLess의 정의에서 number에 초기 값을 설정했다. 이런 프로퍼티 래퍼를 사용하는 코드는 TwelveOrLess에 의해 포장된 프로퍼티에 서로 다른 초기 값을 특정할 수 없다. 예를 들어, SmallRectangleheightwidth에 초기 값을 전달하지 못한다. 초기 값과 다른 커스터마이징을 위해, 프로퍼티 래퍼는 initializer를 추가할 필요가 있다.

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

초기 값 없이 프로퍼티에 래퍼를 적용할 때, Swift는 init() initializer를 사용한다.

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

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

프로퍼티에 초기 값을 지정한다면 Swift는 init(wrappedValue:) initializer를 사용해 래퍼를 설정한다.

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

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

커스텀 속성 소괄호 안에 인자를 전달하면, Swift는 이 인자를 받아들이는 initializer를 사용해 래퍼를 설정한다. 예를 들어 초기 값과 maximam 값을 준다면 Swift는 init(wrappedValue:maximum:) initializer를 사용한다.

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"

프로퍼티 래퍼에 인자를 포함하여 래퍼의 초기 상태를 설정하고 그것이 생성될 때 래퍼에 다른 옵션을 넣을 수 있다. 이 문법은 프로퍼티 래퍼를 사용하는 가장 일반적인 문법이다.

프로퍼티 래퍼 인자를 포함할 때 할당 연산자를 사용하여 초기 값을 지정할 수 있다. Swift는 할당 연산을 wrappedValue 인자와 같이 다루며, 포함한 인자를 수용하는 initializer를 사용한다.

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의 인스턴스는 최댓값이 12인 SmallNumber(wrappedValue: 1)을 호출하며, width를 감싸는 인스턴스는 SmallNumber(wrappedValue: 2, maximum: 9)를 호출한다.


프로퍼티 래퍼로부터 값을 예상하기

프로퍼티 래퍼는 projected value를 정의함으로써 새로운 기능을 드러낼 수 있다. 예를 들어, 데이터베이스에 대한 접근을 관리하는 프로퍼티 래퍼는 그것의 projected value에 flushDatabaseConnection() 메소드를 노출할 수 있다. projected value의 이름은 달러 사인($)으로 시작한다는 점만 빼면 wrapped value와 같다. 개발자가 정의하는 코드에서는 $로 시작하는 프로퍼티를 정의할 수 없기 때문에, 이 값은 다른 프로퍼티에 간섭하지 않는다.

아래의 코드는 새로운 값을 프로퍼티에 저장하기 전에 프로퍼티 래퍼가 조정을 했는지 여부를 추적하기 위해 projectedValue 프로퍼티를 SmallNumber 구조체에 더한다.

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

프로퍼티 래퍼는 어떤 타입의 projected value든 반환할 수 있다. 예제에서는 프로퍼티 래퍼가 정보 하나만을 노출하면 되기 때문에 Bool 타입을 projectedValue로 사용했다. 더 많은 정보를 드러내야 할 필요가 있는 래퍼는 다른 데이터 타입의 인스턴스를 반환할 수 있다. 또는 자기 자신의 projected value로써 래퍼의 인스턴스를 self를 사용해 노출할 수 있다.

프로퍼티 게터 또는 인스턴스 메소드와 같이 타입의 일부인 코드로부터 projected value에 접근할 때는 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
    }
}

프로퍼티 래퍼는 게터와 세터를 위한 쉽고 편리한 문법이기 때문에, 다른 프로퍼티와 동일한 방법으로 heightwidth에 접근할 수 있다.



전역 변수와 지역 변수

계산 프로퍼티와 프로퍼티 옵저버의 기능은 전역 변수와 지역 변수 모두에 사용 가능하다. 전역 변수는 함수, 메소드, 클로저, 타입 컨텍스트 밖에서 정의된 변수이고, 지역 변수는 그 안에서 정의된 변수이다.

지난 챕터들에서 만났던 전역/지역 변수는 모두 저장된 변수(stored variable)였다. 저장된 변수는 저장 프로퍼티와 마찬가지로 특정 타입의 값에 대한 저장 공간을 제공하고, 값을 설정하거나 검색할 수 있도록 한다.

계산된 변수(computed variable)나 저장된 변수를 위한 옵저버(observer)도 전역/지역 변수로 정의할 수 있다. 계산된 변수는 값을 저장하기 보다는 계산한다. 계산 프로퍼티와 같은 방식으로 작성된다.

전역 상수, 변수는 지연된 저장 프로퍼티와 비슷하게 지연 계산된다. 하지만 지연된 저장 프로퍼티와 다르게 lazy 키워드를 붙일 필요가 없다.

지역 상수, 변수는 지연 계산되지 않는다.



타입 프로퍼티 (Type Property)

인스턴스 프로퍼티는 특정한 타입의 인스턴스에 속한 프로퍼티를 말한다. 이러한 타입의 인스턴스를 생성할 때마다, 다른 인스턴스로부터 분리된 그 자신의 프로퍼티 값을 갖게 된다.

타입의 인스턴스 하나가 아니라 타입 자체에 속한 프로퍼티를 정의할 수 있다. 그 타입에 해당되는 단 하나의 프로퍼티만 생성된다. 이러한 프로퍼티를 타입 프로퍼티(type property)라 부른다.

타입 프로퍼티는 모든 인스턴스가 사용하는 상수 프로퍼티처럼 특정 타입의 인스턴스에 공통되는 값을 정의할 때 유용하다. 또는 그 타입의 모든 인스턴스에 공통되는 값을 저장하는 변수 프로퍼티를 정의할 때도 좋다.

저장 타입 프로퍼티는 변수 또는 상수가 가능하다. 계산 프로퍼티는 변수 프로퍼티로만 선언할 수 있다.

저장 인스턴스 프로퍼티와 달리, 저장 타입 프로퍼티에는 반드시 기본 값을 주어야 한다. 타입 자신은 초기화 시간에 저장 타입 프로퍼티에 값을 할당할 수 있는 initializer를 가질 수 없기 때문이다.

저장 타입 프로퍼티는 최초 접근시에 지연 초기화된다. 여러 스레드가 동시에 접근해도 한 번만 초기화되는 것을 보장한다. 또한 lazy 키워드를 필요로 하지 않는다.


타입 프로퍼티 문법

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

위의 계산 타입 프로퍼티 예시는 읽기 전용 계산 타입프로퍼티지만, 읽기와 쓰기가 모두 가능한 계산 타입 프로퍼티 역시 계산 인스턴스 프로퍼티와 같은 문법을 사용하여 정의할 수 있다.


타입 프로퍼티의 질의와 설정

점 문법을 통해 질의 받고 설정될 수 있다는 점은 타입 프로퍼티와 인스턴스 프로퍼티가 같다. 그러나 타입 프로퍼티는 타입에 대해서만 질의 받고, 설정된다. 타입의 인스턴스가 아니다.

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 사이의 오디오 레벨을 갖는다.

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보다 더 큰 값이 들어온다면, 제한 값으로 수정된다.

두 번째 타입 프로퍼티는 maxInputLevelForAllChannels라는 뱐수 저장 프로퍼티이다. 이것은 모든 AudioChannel 인스턴스에 의해 받는 최대 입력 값을 추적한다. 초기화 값은 0이다.

AudioChannel 구조체는 채널의 현재 오디오 레벨을 보여주는 currentLevel 저장 프로퍼티 역시 정의한다.

currentLevel 프로퍼티는 값의 변화를 확인하는 didSet 프로퍼티 옵저버를 가진다. 이 옵저버는 두 가지를 체크한다.

  • 만약 새로운 currentLevel 값이 허용된 thresholdLevel보다 크다면, 프로퍼티 옵저버는 currentLevelthresholdLevel로 맞춘다.
  • 만약 새로운 currentLevel 값이 아무 AudioChannel 인스턴스로부터 받은 값보다 크다면, 프로퍼티 옵저버는 새로운 currentLevel 값을 maxInputLevelForAllChannels 타입 프로퍼티에 저장한다.

처음 두 타입 프로퍼티를 체크할 때, didSet 옵저버는 currentLevel에 기본 값을 설정한다.


AudioChannel 구조체를 새로운 두 오디오 채널을 만드는 데 사용할 수 있다.

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

만약 leftChannelcurrentLevel을 7로 설정한다면, maxInputLevelForAllChannels 타입 프로퍼티는 7로 수정된다.

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

만약 currentLevel에 11을 넣으려 한다면, rightChannelcurrentLevel 프로퍼티는 최댓값인 10으로 정해진다. maxInputLevelForAllChannels 프로퍼티 역시 10으로 수정된다.

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

0개의 댓글