티스토리 뷰

Swift Language Guide

8. 프로퍼티

지우개. 2020. 11. 3. 16:58

프로퍼티는 특정 클래스, 구조체 또는 열거형의 값과 연관되어 있습니다. 저장 프로퍼티는 인스턴스 값을 상수나 변수에 저장하고 반면에 연산 프로퍼티는 값을 저장하기 보단 값을 연산한다. 연산 프로퍼티는 클래스, 구조체 그리고 열거형에 사용할 수 있고, 저장 프로퍼티는 열거형에서는 사용할 수 없다.

 

저장 및 연산 프로퍼티는 각 타입의 인스턴스와 일반적으로 연관되어 있습니다. 그러나 프로퍼티는 타입 그자체와도 연관될 수 있습니다. 예를 들어 타입 프로퍼티로 알려진 프로퍼티 등등..

 

게다가 프로퍼티 옵서버를 정의하여 프로퍼티의 값이 바뀌는 것을 모니터링 할 수 있습니다. 프로퍼티 옵서버는 저장 프로퍼티에 추가 가능합니다, 또한 부모 클래스로 부터 상속받은 프로퍼티에게도 가능합니다. 

 

저장 프로퍼티

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

프로퍼티는 일반적인 변수, 상수 선언과 비슷하고 인스턴스를 생성할 때 및 그 이후에도 값을 변경해줄 수 있음.

위 예제의 경우 변수로 인스턴스 값을 받았기 때문에 내부 프로퍼티 또한 값 변경 가능. 만약 인스턴스를 let으로 받았다면 내부 프로퍼티 또한 전부 상수 취급이라 값을 바꿀 수 없음. 그러나 이건 값 타입인 구조체의 예시이고 클래스의 경우 인스턴스 타입을 let으로 받았다고 할 지라도 인스턴스 내부의 변수 프로퍼티는 변수로 유지되어 값 변경 가능함.

 

Lazy Stored Properties

프로퍼티가 사용되는 첫번째 시간까지 초기값이 계산되지 않는 프로퍼티. 선언 앞에 lazy 키워드를 적음으로서 사용 가능. lazy property는 항상 변수로써 선언되야 하는데 이유는 초기값이 인스턴스 생성이 완료될 때까지도 값이 주어지지 않을 수 도 있기 때문. 상수 프로퍼티는 무조건 생성자가 일을 마무리 하기 전까지 값이 주어저야 하는데 lazy는 그러지 못할 수 있기 때문에 무조건 변수로 선언해야 함.

 

아래 예제는 lazy stored property를 사용한 예시

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

DataImporter 클래스는 초기화할 때 오랜 시간이 필요하다고 가정한다면, 굳이 importer를 사용하지 않을 수도 있는 인스턴스도 DataImporter클래스를 초기화하느라 시간을 낭비할 이유가 없으므로 lazy 키워드를 통해 처음 사용 될 때 인스턴스가 생성되도록 함.

 

추가로 여러 스레드가 프로퍼티가 아직 초기화 되지 않았을 때 동시에 접근할 경우 한 번만 초기화된다는 보장이 없다고 한다. 오히려 시간낭비가 더 생길. 수 있으므로 이런 경우는 사용하는 것이 좋지 않은 듯

 

연산 프로퍼티

클래스, 구조체, 열거형 모두에서 사용가능한 연산 프로퍼티는 사실 값을 저장하는 일을 하는 것이 아니라 대신 다른 프로퍼티의 값을 받기 위해 간접적으로 접근할 수 있는 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))")
// Prints "square.origin is now at (10.0, 10.0)"

Rect의 center 프로퍼티는 연산 프로퍼티인데 다른 저장 프로퍼티인 size와 origin 값을 가져와서 내부에서 계산을 해 중심점을 반환한다. 또한 set으로 새 값을 받아와 기준점 origin값을 바꿔준다. setter의 set(새값) 대신 간단하게 set{}으로 선언하고 내부에서 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)
        }
    }

getter도 단축어가 있는데 내부 코드가 한줄 표현이라면  return 생략이 가능하다.

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

setter를 생략해서 읽기 전용 연산 프로퍼티를 만들 수도 있다.

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"

마지막으로 당연하게도 연산프로퍼티는 전부 변수로 선언해야 한다. 값이 고정되어 있지 않기 때문에 상수로는 선언할 수 가 없다.

 

프로퍼티 옵서버

프로퍼티 옵서버는 프로퍼티의 값이 바뀌었을 때 관찰 및 반응한다. 프로퍼티 옵서버는 프토퍼티 값이 바뀌었을 때 매번 불려지는데 심지어 새로운 값이 기존 값과 같다하더라도 불려진다.

 

프로퍼티 옵서버는 이런 장소에 추가 가능하다.

 

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

정의한 연산 프로퍼티가 빠져있는데, 이경우는 프로퍼티의 setter를 사용해서 값의 변화에 반응할 수 있기 때문에 굳이 옵서버가 필요치는 않다. 

 

프로퍼티 옵서버의 경우 두 가지 케이스를 고를 수가 있다.

  • willSet :  값이 저장되기 바로 직전에 호출
  • didSet : 값이 변경된 후 즉시 호출

만약 willSet 옵서버를 구현했다면, 새로운 프로퍼티 값이 상수 파라미터로 주어진다. willSet 구현부에서 이 파라미터의 이름을 수정가능하다.만약 특별한 이름을 정해주지 않으면 디폴트 값으로 newValue라는 이름으로 접근가능하다.

비슷하게 didSet 옵서버를 구현했다면, 예전 프로퍼티 값이 상수 파라미터로 주어지고 이 또한 이름을 주지 않으면 oldValue라는 파라미터 값으로 주어진다. 

 

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

 

프로퍼티 래퍼

swiftUI에 참 많이 쓰이는 친구. 프로퍼티 래퍼는 어떻게 프로퍼티를 관리할 지에 관한 코드와 프로퍼티를 정의하는 코드 사이 별도의 레이어를 추가시켜준다. (정의 자체는 참 이해하기 어렵다;)  프로퍼티 래퍼를 정의하기 위해선 wrappedValue 프로퍼티를 정의한 클래스, 열거형, 구조체를 만들고 @propertyWrapper 어노테이션을 달아주면 된다.

 

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

코드를 보면 실제 값이 저장되는 number 변수는 private로 선언되어 외부에서 접근이 불가능하고 대신 wrappedValue 값에 get, set 값으로 접근한다. 이렇게 만든 프로퍼티 래퍼를 다른 프로퍼티에 적용시킬 수 있다.

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"

프로퍼티 래퍼의 초기값은 앞서 만든 TwelveOrLess 구조체의 number 값에서 지정하기 때문에  프로퍼티 래퍼를 적용받은 프로퍼티는 초기값을 지정할 수가 없다. 초기값이 필요한 경우를 위해 프로퍼티 래퍼 내부에서 생성자를 만들어 초기값을 지정할 수 있도록 만들 수 있다.

@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)
    }
}
struct UnitRectangle {
    @SmallNumber var height: Int = 1
    @SmallNumber var width: Int = 1
}

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

프로퍼티 래퍼가 적용된 프로퍼티 값에 = 1 을 작성했는데 이것은 init(wrappedValue:) 생성자를 호출하는 형태로 바뀐다. wrappedValue 말고도 다른 값들을 생성자에서 지정해주고 싶다면 이렇게 작성하면 된다.

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
}

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

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

wrappedValue 말고도 projectedValue를 정의하여 추가적인 기능을 노출할 수 있다. 역시 정의만 보면 도통 뭔소린지 이해가 안가기에 예제를 보자.

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

아까 봤던 예제와 비슷하다. 다른점은 projectedValue이 추가되었고 새로 주어지는 값이 12 이상이면 true를 아니면 false 값을 저장한다.

예제 아래 부분에 볼 수 있듯이 someStructure.$someNumber, 즉 달러표시로 projectedValue 값의 참거짓을 확인할 수 있다.

 

타입 프로퍼티

앞서 본 인스턴스 프로퍼티들은 새로운 인스턴스가 생성되면 새로운 프로퍼티도 같이 생성되나 타입 프로퍼티는 특정타입에 속한 프로퍼티로 그 타입에 해당하는 단 하나의 프로퍼티만 생성된다. 타입프로퍼티는 모든 인스턴스에 공통으로 사용되는 값을 정의할 때 유용하다.

타입 프로퍼티는 항상 초기값이 주어져야 하는데 이유는 생성자가 없어 초기화 할 곳이 없기 때문이다.

 

swift에서도 익숙하게 static 키워드를 사용하나. 클래스에서는 static 과 class 두가지로 타입 프로퍼티를 선언할 수 있는데 두 가지의 차이는 서브클래스에서 오버라이드가 가능한지 여부이다. class로 선언된 프로퍼티는 자식 클래스에서도 오버라이드가 가능하다.

'Swift Language Guide' 카테고리의 다른 글

10. 서브스크립트  (0) 2020.11.08
9. 메소드  (0) 2020.11.06
7. Structures and Classes  (0) 2020.11.02
6. Enumerations  (0) 2020.11.01
5. Closures  (0) 2020.10.31
댓글
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
TAG
more
«   2025/02   »
1
2 3 4 5 6 7 8
9 10 11 12 13 14 15
16 17 18 19 20 21 22
23 24 25 26 27 28
글 보관함