GGURUPiOS

Swift 공식 문서 정리 - (10) Properties 본문

Swift/공식문서 정리 ( 문법 )

Swift 공식 문서 정리 - (10) Properties

꾸럽 2023. 4. 27. 20:32

Properties ( 프로퍼티 )

프로퍼티는 값을 특정 클래스, 구조체 또는 열거형과 연결함
저장 프로퍼티(Stored Properties)는 인스턴스의 일부로 저장하는 반면
계산된 프로퍼티(Computed Properties)는 저장하는 대신 계산함
저장 프로퍼티 클래스와 구조체에서만,계산된 프로퍼티는 클래스와 구조체 열거형 모두에서 제공 됨

또한 프로퍼티 옵저버를 정의해서 값이 변할 때마다 모니터링이 가능 함
프로퍼티 래퍼를 사용하여 여러 속성의 getter 밑 setter 에서 코드를 재사용할 수도 있음

저장 프로퍼티 ( Stored Properties )

저장 프로퍼티는 단순히 값을 저장하고있는 프로퍼티임
let, var 키워드를 이용해서 선언해서 사용 가능

초기 값을 설정하고 수정할 수도 있음

struct FixedLengthRange {
    var firstValue: Int
    let length: Int
}
var rangeOfThreeItems = FixedLengthRange(firstValue: 0, length: 3)
// 범위 값은 0, 1, 2 입니다.
rangeOfThreeItems.firstValue = 6
// 범위 값은 6, 7, 8 입니다.

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

구조체의 인스턴스를 만들고 해당 인스턴스를 상수에 할당하면 변수 속성으로 선언된 경우에도 인스턴스의 속성을 수정할 수 없음

let rangeOfFourItems = FixedLengthRange(firstValue: 0, length: 4)
// 범위 값은 0, 1, 2, 3 입니다.
rangeOfFourItems.firstValue = 6
// 에러 발생!

구조체가 아니라 클래스는 let 으로 선언하더라도 프로퍼티가 변경 가능함.
이유는 전 챕터에서 말했듯이 클래스 인스턴스는 참조 타입이기 때문.

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

지연 저장 프로퍼티는 값이 처음으로 사용 되기 전에는 계산되지 않는 프로퍼티 임
선언은 앞에 lazy 키워드를 붙이면 됨

지연 프로퍼티는 반드시 변수로 선언해야 함
→ 상수는 초기화가 되기전에 항상 값을 갖는 프로퍼티인데, 지연 프로퍼티는 처음 사용되기 전에는 값을 갖지 않는 프로퍼티 이기 때문에.

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

속성의 초기 값이 필요할 때 까지 또는 필요할 때 까지 수행하면 안되는 복잡하거나 계산이 많이 요구되는 경우에도 유용 함.

class DataImporter {
    /*
        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")
// DataImporter 인스턴스는 이 시점에 생성돼 있지 않습니다.

print(manager.importer.filename)
// the DataImporter 인스턴스가 생성되었습니다.
// "data.txt" 파일을 출력합니다.

즉 importer 가 사용되기 전까지는 계산되지 않는다.

그러나 지연 프로퍼티가 여러 스레드에서 사용되면 지연프로퍼티가 한번만 실행되는 걸 보장하지 않음. 만약 지연 프로퍼티가 단일 스레드에서 사용되면 초기화는 한번만 사용 됨

저장 프로퍼티와 인스턴스 변수

Objective-C 와 달리 프로퍼티의 선언과 사용의 혼란을 피했습니다.
프로퍼티의 이름, 타입, 메모리 관리 등의 모든 정보를 프로퍼티를 선언하는 한곳에서 정의하게 됩니다
( 이 부분은 옵젝씨를 안해봐서 이해가 잘 안가지만, 그냥 프로퍼티 선언하는 곳에서 모든 정보를 정의 한다고 이해하면 될 듯 )

계산된 프로퍼티 ( 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
square.center = Point(x: 15.0, y: 15.0)
print("square.origin is now at (\\(square.origin.x), \\(square.origin.y))")
// "square.origin is now at (10.0, 10.0)" 출력

위의 코드에서 사각형의 중점을 계산하는 center는 값을 따로 저장하는 것이 아니라
점과 크기에 따라 달라지므로 다른 프로퍼티들을 적절히 연산해서 구할 수 있음

Setter 의 간략한 표현 (속기)

계산된 프로퍼티의 setter가 설정할 새 값의 이름을 정의하지 않으면 기본 이름이 사용됨

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

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만 있는 계산된 프로퍼티
프로퍼티는 반드시 반환 값을 제공하고 다른 값을 지정할 수는 없는 프로퍼티
계산된 프로퍼티를 선언시에는 반드시 var 로 선언해야 함.
보통 읽기전용이라 함은 한번 값이 정해지면 변하지 않기 때문에 let 으로 선언하는 것이 맞으나
계산된 프로퍼티는 읽기 전용이라 하더라도 계산 값에 따라 값이 변할 수 있으므로 var로 선언

아래는 예시

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)")
// "the volume of fourByFiveByTwo is 40.0" 출력

프로퍼티 옵저버

프로퍼티에는 새 값이 설정 될 때마다 이벤트를 감지할 수 있는 옵저버 제공
새 값이 이전 값과 같더라도 항상 호출 됨
지연저장 프로퍼티에서는 사용 불가
서브 클래스의 프로퍼티에 옵저버를 정의 하는 것도 가능

다음 위치에서 옵저버 추가 가능

  • 사용자가 정의하는 저장 프로퍼티
  • 상속 받은 저장 프로퍼티
  • 상속 하는 계산된 프로퍼티

계산된 프로퍼티는 setter 에서 값의 변화를 감지 할 수 있기 때문에 따로 옵저버를 정의할 필요가 없다.

  • willSet : 값이 저장되기 바로 직전에 호출 됨
  • didSet : 새 값이 저장되고 난 직후에 호출 됨willSet에서는 새 값의 파라미터명을 지정할 수 있는데, 지정하지 않으면 기본 값으로 newValue를 사용합니다.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

프로퍼티 래퍼

프로퍼티 래퍼는 프로퍼티가 저장되는 방식을 관리하는 코드와

프로퍼티를 정의하는 코드 사이에 분리 계층 추가

래퍼를 사용하는 경우 정의할 때 관리 코드를 한 번 작성하고 여러 프로퍼티에 적용하여 해당 관리 코드를 재사용 함

프로퍼티 래퍼를 정의하려면 프로퍼티를 정의하는 구조체, 열거형 또는 클래스를 만듬

아래 코드에서 구조체는 래핑하는 값이 항상 12 보다 작거나 같은 숫자를 포함하도록 함

더 큰 숫자를 저장하도록 요청하면 12를 저장

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

래퍼를 적용하려면 프로퍼티 앞에 래퍼의 이름을 작성하여 적용한다.

아래는 그 예시임

struct SmallRectangle {
    @TwelveOrLess var height: Int
    @TwelveOrLess var width: Int
}
// 여기서 height, width 는 항상 12이하가 된다.

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

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

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

래핑된 프로퍼티의 초기값 설정

나중에 초기화 챕터에서 다루겠지만 위의 예제에서는 number에 초기값을 제공하여 래핑된 프로퍼티의 초기값을 설정함.

초기 값 또는 기타 사용자 지정 설정을 하려면 프로퍼티 래퍼에서 이니셜라이저를 추가해야함

아래는 그 예시임

@propertyWrapper
struct SmallNumber {
    private var maximum: Int
    private var number: Int

// maximum과 number는 아직 초기화 안된 상태.
    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 UnitRectangle {
    @SmallNumber var height: Int = 1
    @SmallNumber var width: Int = 1
}

// 위와 같이 사용하면 Swift는 init(wrappedValue:) 에 대한 호출로 변환 됨

사용자 지정 프로퍼티 뒤에 괄호 안에 인수를 작성할 때, 스위프트는 해당 인수를 허용하는 초기화 프로그램을 사용하여 래퍼를 설정 함.

위의 예제에서는 .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"

그럼 아래의 예제에서는 어떤 생성자가 사용될까 ?

struct MixedRectangle {
    @SmallNumber var height: Int = 1
    @SmallNumber(maximum: 9) var width: Int = 2
}

// height 는 init(wrappedValue: 1)
// width 는 init(wrappedValue: 2, maximum: 9) 를 이용해 초기화 된다.

프로퍼티 래퍼에서 값 프로젝션 (Projecting a Value From a Property Wrapper)

래핑 값 뿐만 아니라, 추가적인 예상 값을 노출할수 있다.

예상 값의 이름은 달러 기호($)로 시작한다.

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

// 위 코드는 너무 큰 숫자를 저장하려고 시도한 후에는 projectedValue 가 true
// 그 반대 상황은 false로 설정되는 코드임

어떻게 $someNumber로 접근했는데 저 값이 나올까 하고 이해가 안가서 찾아봤다.

→ 알고보니 프로퍼티 래퍼에서 projectedValue를 사용할 때는 무조건 변수명이 저거여야 했다.
 한마디로 예약어 였다는 점..

또 하나의 의문은, 그럼 결국 projectedValue를 설정하지 말고, 그냥 다른 변수명으로 설정해서 접근하면 되는거 아닐까?

→ 프로퍼티 래퍼에서 프로젝션된 값을 표현하는 표준화된 방법이기 때문에 가독성과 일관성을 유지하는데 도움이 된다고 한다.
권장되는 방법이라고 함

프로퍼티 래퍼는 모든 유형의 값을 프로젝션된 값으로 반환 할 수 있음
위의 예제에서는 Bool 값 하나만을 노출하므로 더 많은 정보를 노출해야 하는 래퍼는 다른 유형의

인스턴스를 반환하거나 래퍼 인스턴스 자체 ( self ) 를 projectedValue로 반환 할 수 있다.

글로벌(전역) 및 로컬(지역) 변수

프로퍼티 계산 및 관찰에 대해 위에서 설명한 기능은 전역 변수 및 지역 변수 에도 사용 가능
전역 변수는 함수, 메서드, 클로저, 또는 유형 문맥 외부에서 정의되는 변수임
지역 변수는 함수, 메서드, 클로저 문맥 내에서 정의되는 변수 임
저장 프로퍼티와 같은 저장된 변수는 특정 유형의 값에 대한 저장소를 제공하고 해당 값을 설정, 검색할 수 있도록 함

그러나 글로벌 또는 로컬 범위에서 계산된 변수를 정의하고 저장된 변수에 대한 관찰자를 정의할 수도 있음. 계산된 변수는 값을 저장하는 대신 계산, 계산된 프로퍼티와 동일한 방식으로 작성 됨

전역 상수 및 변수는 Lazy Stored Properties (지연 저장 프로퍼티) 와 비슷한 방식으로 항상 느리게 계산됨
그러나 lazy를 표시할 필요는 없음

로컬 상수와 변수는 절대 느리게 계산되지 않음

프로퍼티 래퍼를 로컬 저장 변수에 저장할 수 있지만 전역 변수나 계산된 변수에는 적용 불가.
아래 코드에서는 프로퍼티 래퍼로 사용 됨.

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

    myNumber = 10
    // now myNumber is 10

    myNumber = 24
    // now myNumber is 12
}

타입 프로퍼티

인스턴스 프로퍼티는 특정 타입 인스턴스에 속하는 프로퍼티임.
해당 타입의 인스턴스가 아니라 타입 자체에 속하는 프로퍼티를 정의할 수 도 있음.
이러한 프로퍼티의 복사본은 하나만 있다.
이러한 종류의 프로퍼티를 타입 프로퍼티라고 함

타입 프로퍼티는 모든 인스턴스가 사용할 수 있는 상수 프로퍼티 또는 모든 인스턴스에 전역적인 값을 저장하는 변수 속성과 같이 특정 타입의 모든 인스턴스에 보편적인 값을 정의하는 데 유용함

→ 어디서나 쓰일 수 있는 값이라고 이해하자.

저장된 타입 프로퍼티는 변수 또는 상수 일수 있음.

계산된 타입 프로퍼티는 항상 변수 속성으로 선언됨

저장 인스턴스 프로퍼티와 달리 항상 저장 타입 프로퍼티에는 기본값이 있어야 함. ( 기본 값으로 초기화 해야함 ) → 초기화 과정이 없기 때문에!

저장 타입 프로퍼티는 처음 접근할 때 지연 초기화 됨.

여러 스레드에서 동시 액세스 할 경우 한 번만 초기화 되도록 보장됨 ( lazy 키워드 불필요 )

타입 프로퍼티 구문

스위프트에서 타입 프로퍼티는 타입 바깥쪽 중괄호 내에서

타입 선언의 일부로 작성되며 각 타입 프로퍼티는 지원하는 타입으로 명시적으로 범위가 지정됨

키워드는 static과 class 두가지로 되어있다.

두 키워드의 가장 큰 차이점은 서브클래스에서 오버라이딩 가능 여부다. → 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"
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
            }
        }
    }
}
// didSet안에서 currentLevel값을 할당하는 것은 didSet을 반복호출하지 않음