소피it블로그

[Swift 공식문서]Properties 정리 (1) 본문

개발_iOS/스위프트

[Swift 공식문서]Properties 정리 (1)

sophie_l 2022. 5. 31. 22:24

https://docs.swift.org/swift-book/LanguageGuide/Properties.html

 

Properties — The Swift Programming Language (Swift 5.6)

Properties Properties associate values with a particular class, structure, or enumeration. Stored properties store constant and variable values as part of an instance, whereas computed properties calculate (rather than store) a value. Computed properties a

docs.swift.org

프라퍼티는 값을 특정 클래스, 구조체 또는 enumeration과 연관짓는다. 저장된 프라퍼티(stored properties)는 상수와 변수 값을 인스턴스의 일부로 저장하는 반면, 계산된 프라퍼티(computed properties)는 값을 저장하기보다는 계산한다. 계산된 프라퍼티는 클래스, 구조체 또는 enumeration 등이 전부 제공하는 반면, 저장된 프라퍼티는 클래스와 구조체에서만 제공된다.

저장된 프라퍼티와 계산된 프라퍼티는 보통 특정 타입의 인스턴스와 연관된다. 그러나 어떤 프라퍼티는 타입 그 자체와도 연관될 수 있다. 그런 프라퍼티는 타입 프라퍼티(type properties)라고 부른다.

게다가 프라퍼티의 값의 변화를 관찰하기 위해 프라퍼티 옵저버(property observers)를 정의할 수도 있는데, 커스텀 액션을 통해 이에 반응할 수 있다. 프라퍼티 옵저버는 저장된 프라퍼티에 추가될 수도 있고, 자식클래스가 부모클래스로부터 물려받는 프라퍼티에도 추가될 수 있다.

다양한 프라퍼티에 대해 코드를 재사용하기 위해 프라퍼티 래퍼(property wrapper)도 써줄 수 있다.

 

1. 저장된 프라퍼티(Stored Properties)

 

저장된 프라퍼티는 특정 클래스나 구조체의 인스턴스의 일부로 저장된 상수나 변수를 의미한다. 저장된 프라퍼티는 변수인 저장된 프라퍼티일 수도 있고(var 키워드 사용) 상수인 저장된 프라퍼티가 될 수도 있다(let 키워드를 사용).

저장된 프라퍼티의 정의의 일부로써 디폴트 값을 제시해줄 수도 있다. 또한 초기화 과정에서 저장된 변수의 초깃값을 설정하거나 변경해줄 수 있다. 이는 상수인 저장된 프라퍼티에서도 적용된다.

아래의 예시는 FixedLengthRange라는 구조체를 정의하는데, 이는 생성된 이후에는 변경될 수 없는 정수의 범위를 묘사한다.

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는 새 범위가 생성될 때 초기화되며 상수 프라퍼티이기 때문에 그 이후에는 변경될 수 없다.

 

(1) 상수 구조체의 인스턴스의 저장된 프라퍼티

 

만약 구조체의 인스턴스를 생성한 후 해당 인스턴스를 상수에 할당하면, 그 인스턴스의 프라퍼티들이 변수로 선언되었다고 해도 변경할 수 없다.

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가 let 키워드와 함께 상수로 선언되었기 때문에 firstValue가 변수 프라퍼티임에도 불구하고 해당 프라퍼티를 바꿀 수 없다. 이러한 상황은 구조체가 밸류 타입이기 때문에 발생하는 일이다. 밸류 타입의 인스턴스가 상수로 선언되면 그 프라퍼티들도 상수로 여겨진다.

반면 레퍼런스 타입인 클래스에서는 그렇지 않다. 레퍼런스 타입의 인스턴스를 상수에 할당하는 경우에는 그 인스턴스의 변수 프라퍼티를 변경해줄 수 있다.

 

(2) Lazy 저장된 프라퍼티

 

게으른 저장된 프라퍼티는 처음 사용될 때까지는 초깃값이 계산되지 않는 프라퍼티이다. 선언 전에 lazy 수식어를 써줌으로써 게으른 저장된 프라퍼티 나타낼 수 있다. 주의할 점은, 게으른 프라퍼티는 항상 var 키워드와 함께 변수로 선언해야 한다는 것이다. 상수 프라퍼티는 항상 초기화 완료 이전에 값을 가지고 있어야 하기 때문에 게으른 프라퍼티가 될 수 없다.

게으른 프라퍼티는 인스턴스의 초기화가 완료되기 전까지는 값을 알 수 없는 외부 요인에 영향을 받는 프라퍼티의 초깃값으로 사용할 때 특히 유용하다. 게으른 프라퍼티는 또한 프라퍼티의 초깃값이 복잡하거나 계산 비용이 비싼 설정을 필요로 하는 경우에도 사용하기 적절하다. 아래의 예시는 복잡한 클래스의 불필요한 초기화를 피하기 위해 게으른 저장된 프라퍼티가 사용되는 상황을 보여준다. 이 예시는 DataImporter와 DataManager라는 두 클래스를 정의하는데, 둘 다 완전히 보여지지 않는다.

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 hasn't yet been created

DataManager 클래스는 data라는 저장된 프라퍼티를 가지는데, 이는 빈 문자열의 배열로 초기화된다. 뒤의 기능들을 볼 수 없지만 이 DataManager 클래스의 목적은 이 문자열 자료의 배열을 관리하고 접근할 수 있게 해주는 것이다.

DataManager 클래스의 기능의 일부는 파일에서 데이터를 불러오는 능력이다. 이 기능은 DataImporter 클래스가 제공하는데, 이를 초기화하기 위해서는 적지 않은 시간이 들 것이다. DataImporter 인스턴스가 파일을 열고 그 내용을 메모리로 읽어와야 초기화를 할 수 있기 때문이다.

DataManager 인스턴스는 파일에서 데이터를 가져오지 않고서도 데이터를 다룰 수 있기 때문에 DataManager는 자신이 생성될 때 새로운 DataImporter를 생성하지 않는다. 그보다는 처음 사용될 때 DataImporter를 생성하는 게 더 말이 된다.

lazy 수식어와 함께 쓰였기 때문에 importer 프라퍼티를 위한 DataImporter 인스턴스는 importer 프라퍼티가 처음 접근될 때만 생성된다. 예를 들면 filename 프라퍼티가 처음 쿼리될 때 등.

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

2. 계산된 프라퍼티(Computed Properties)

 

저장된 프라퍼티에 더불어 클래스, 구조체, enumeration은 계산된 프라퍼티를 정의할 수 있는데, 이는 값을 실제로 저장하지 않는 대신 다른 프라퍼티와 값을 간접적으로 가져와서 세팅할 수 있는 게터와 옵셔널 세터를 제공한다.

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는 x좌표와 y좌표를 포함한다.
  • Size는 넓이와 높이를 포함한다.
  • Rect는 원점과 size를 이용해 직사각형을 정의한다.

Rect 구조체는 center라는 계산된 프라퍼티를 제공한다. Rect의 현재의 center 위치는 늘 origin과 size를 통해 결정되기 때문에 명확한 Point 값을 center point에 저장할 필요가 없다.

대신 Rect는 center라고 불리는 계산된 프라퍼티를 위한 커스텀 게터와 세터를 정의하는데, 이를 통해 직사각형의 center에 실제 값이 들어있는 것처럼 사용할 수 있게 된다.

위의 예시는 square라고 불리는 Rect의 변수를 또한 생성한다. square 변수는 (0, 0)의 원점과 10의 width, height로 초기화된다. 이 square는 아래 다이어그램에서 연한 녹색의 정사각형이다.

square 변수의 center 프라퍼티는 그 후 dot 문법(square.center)을 통해 접근되는데, 이는 center를 위한 게터를 불러서 현재 프라퍼티 값을 가져온다. 이미 존재하는 값을 리턴하는 대신, 게터는 정사각형의 center를 대변하는 새로운 Point를 계산한 후 리턴한다. 위에서 볼 수 있는 것처럼 게터는 (5, 5)라는 center point를 리턴한다.

center 프라퍼티는 그 후 새 값인 (15, 15)로 세팅되는데, 이는 정사각형을 오른쪽 위의 새 위치로 옮긴다(다이어그램에서 진 녹색). center 프라퍼티를 세팅하면 center의 세터가 호출되며, 이는 저장된 origin 프라퍼티의 x와 y의 값을 수정하며 정사각형을 새로운 위치로 옮긴다.

 

(1) 짧은 Setter 선언

 

계산된 프라퍼티의 세터가 세팅되는 새 값의 이름을 정의하지 않는다면 newValue라는 디폴트 이름이 사용된다. 이하는 이러한 짧은 기록을 사용하는 Rect 구조체의 다른 버전이다.

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

(2) 짧은 Getter 선언

 

게터의 전체 바디가 하나의 표현으로 되어있다면 게터는 그 표현을 암시적으로 리턴한다. 이하는 Rect 구조체의 또 다른 버전으로, 이러한 짧은 기록과 짧은 세터 기록을 활용한다.

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

(3) 읽기 전용 계산된 프라퍼티

 

게터는 있지만 세터는 없는 계산된 프라퍼티는 읽기 전용 계산된 프라퍼티라고 한다. 읽기 전용 계산된 프라퍼티는 항상 값을 리턴하며 도트 문법을 통해 접근될 수 있지만 다른 값으로 지정될 수는 없다.

여기서 주의할 점은, 읽기 전용 계산된 프라퍼티를 포함한 모든 계산된 프라퍼티는 값이 고정되지 않았기 때문에 var 키워드를 사용하여 변수로 선언해야 한다는 것이다. 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"

이 예시는 Cuboid라는 새로운 구조체를 정의하는데, 이는 3차원의 직사각형 상자를 넓이, 높이, 그리고 깊이 프라퍼티를 통해 나타낸다. 이 구조체는 또한 volume 부피라는 읽기 전용 계산된 프라퍼티를 갖는데, 이는 현재 cuboid의 부피를 계산하고 리턴한다. 넓이, 높이, 깊이 중 어떤 값으로 인해 특정한 부피 값이 결정되는지가 확실하지 않기 때문에 부피는 settable할 수 없다. 그럼에도 불구하고 Cuboid가 읽기 전용 계산된 프라퍼티를 제공하도록 하면 사용자들이 현재의 부피를 알 수 있게 해준다는 점에서 쓸모가 있다.

 

3. 프라퍼티 옵저버(Property Observers)

 

프라퍼티 옵저버는 프라퍼티 값의 변화를 관찰하고 이에 반응한다. 프라퍼티 옵저버는 프라퍼티의 값이 설정될 때마다 호출되는데, 새 값이 프라퍼티의 현재 값과 같은 상황에서도 마찬가지이다.

프라퍼티 옵저버는 다음과 같은 위치에 더해줄 수 있다:

 

  • 직접 정의하는 저장된 프라퍼티
  • 상속받은 저장된 프라퍼티
  • 상속받은 계산된 프라퍼티

상속받은 프라퍼티의 경우 해당 프라퍼티를 하위 클래스에서 오버라이딩해줌으로써 프라퍼티 옵저버를 더해줄 수 있다. 직접 정의하는 계산된 프라퍼티의 경우 옵저버를 만드는 대신 값의 변화를 관찰하고 이에 반응하는 프라퍼티 세터를 사용하면 된다.

프라퍼티에 다음과 같은 옵저버를 사용할 수 있다:

 

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

willSet 옵저버를 사용할 경우, 새로운 프라퍼티 값이 상수 매개변수로 전달된다. 이 매개변수의 이름을 willSet 수행시 따로 정해줄 수 있다. 수행할 때 매개변수 이름을 적지 않고 중괄호를 사용하지 않을 경우 newValue라는 디폴트 매개변수 이름이 붙여진다.

비슷하게 didSet 옵저버의 수행의 경우 이전의 값이 상수 매개변수로 전달된다. 해당 매개변수에 이름을 붙이거나 oldValue라는 디폴트 매개변수명을 사용할 수 있다. didSet 옵저버 안에서 프라퍼티에 값을 할당한다면 할당된 새 값은 세팅되었던 값을 대신한다.

이하는 willSet과 didSet의 예시이다. 아래의 예시는 StepCounter라는 새로운 클래스를 정의하는데, 이는 한 사람이 걷는 동안 취하는 걸음의 개수를 추적한다. 이 클래스는 pedometer나 다른 걸음 계산기에서 입력 데이터를 가져와서 사용할 수 있다.

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

StepCounter 클래스는 정수 타입의 totalSteps 프라퍼티를 선언한다. 이는 willSet과 didSet 옵저버를 가진 저장된 프라퍼티이다.

totalSteps에 대한 willSet과 didSet 옵저버는 프라퍼티에 새 값이 할당될 때마다 호출된다. 새 값이 현재 값과 같을 때도 마찬가지이다.

이 예시의 willSet 옵저버는 새 값을 위해 newTotalSteps라는 이름의 커스텀 매개변수를 사용한다. 이 예시에서 이는 단순히 곧 세팅될 값을 출력한다.

didSet 옵저버는 totalSteps의 값이 업데이트가 된 후 호출된다. 이는 totalSteps의 오래된 값과 새 값을 비교한다. 걸음수의 총 개수가 증가하면 얼마나 많은 새 걸음이 측정되었는지를 알려주는 문구가 출력된다. didSet 옵저버는 이전 값에 대한 커스텀 매개변수 이름을 제공하지 않으며, 대신 디폴트 이름인 oldValue가 사용된다.