[Swift] 10. Properties

도윤·2021년 7월 21일
0

Swift

목록 보기
10/21

Properties

stored properties는 인스턴스의 일부로 상수나 변수를 저장하며, class와 structure에만 제공
Computred Properties는 저장하기보다는 계산하는데 쓰이고 class,structure,enum에 제공

프로퍼티는 타입 그자체와 연결되어 있다. 이러한 프로퍼티를 type property라고 한다.
또한 프로퍼티의 값의 변화를 관찰하기 위해 property observer를 만들 수 있다.
프로퍼티 옵저버는 정의한 저장 프로퍼팉나 하위 클래스가 상속받는 상위 클래스의 프로퍼티에 추가할 수 있다.

Property wrapper를 사용하면 여러 프로퍼티의 getter,setter를 사용하고 코드를 재사용할 수 있다.


Stored Properties

저장 프로퍼티는 특정 class나 structure의 인스턴스의 부분으로써 저장된 상수나 변수를 말한다.
프로퍼티를 정의할 때 default property를 선언하여 기본값을 사용할 수 있다.
마찬가지로 초기화할 때 값을 수정하거나 새로운 value으로 선언할 수 있다.

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

firstValue,length가 저장 프로퍼티이다. length는 상수로 선언되었지만 rangeOfThreeItems라는 변수로 구조체 인스턴스를 선언할 때 생성자의 값으로 할당할 수 있다. 선언된 이후에는 값을 변경할 수 없다.

Stored Properties of Constant Structure Instances

만약 구조체 인스턴스를 상수로 정의한다면 생성과 동시에 어떤 프로퍼티의 value도 변경할 수 없다.

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

위의 코드와 같이 firstValue라는 프로퍼티에 새로운 값을 할당했지만 구조체가 상수이므로 에러가 발생한다.구조체가 value type이기 때문에 이러한 현상이 발생한다.
value type의 인스턴스가 상수로 선언된다면, 그 인스턴스의 모든 프로퍼티 또한 상수로 선언이 된다.

하지만 reference type인 class에는 동일하게 적용되지 않는다. class를 상수로 선언을 해도, 인스턴스의 프로퍼티는 수정이 가능하다.

Lazy Stored Properties

Lazy Stored Properties는 처음 사용될 때까지 초기화되지 않는 프로퍼티이다.
lazy키워드를 이용하여 선언할 수 있으며 반드시 변수(var)로 선언해야 한다. 상수로 선언하게 되면 처음 사용할 때 가지 값이 없기 때문에 오류가 발생할 수 있다.

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

DataManager의 importer 프로퍼티가 lazy 프로퍼티로 선언되었는데 새로운 DataImporter 상수 인스턴스를 생성한다. 위 코드에서는 아직 importer가 사용되지 않는 상태여서 만약 할당한다면 메모리 낭비가 발생하기 때문에 lazy로 선언하여 낭비를 줄일 수 있다.

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

위 코드와 같이 접근을 한다면 lazy프로퍼티가 초기화된다.

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

Object-C에서는 클래스 인스턴스의 일부로 값과 참조를 저장하는 방법이 있다. 또한 프로퍼티에 저장된 값에 대한 백업 저장소로 인스턴스 변수를 사용할 수 있다.

swift는 이러한 개념을 single property declaration(단일 프로퍼티 선언)으로 통합했다. 스위프트 프로퍼티는 인스턴스 변수가 없고 프로퍼티의 백업 저장소에 바로 접근하지 않습니다.


Computed Properties

계산 프로퍼티는 클래스,구조체,열거형에서 사용 가능하다. 값을 직접 저장하는 것 대신 getter,setter를 사용하여 다른 프로퍼티와 값을 간접적으로 접근.
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)

위의 코드는 point,size는 점의 위치와 사각형의 크기를 캡슐화해둔 구조체 이다.
여기서 Rect 구조체의 center 프로퍼티가 계산 프로퍼티이다. Rect 구조체의 origin,size변수는 선언된 Point와 Size인스턴스로 정해지지만, center변수에는 origin,size 변수의 값에 의해 계산되어야 값들이 정해지게 된다. 이때 getter,setter를 이용하여 center 계산 프로퍼팉 값을 저장 프로퍼티값처럼 사용할 수 있다.

Shorthand Setter Declaration

만약 계산 프로퍼티에서 set을 사용할 때 새로운 값의 이름을 설정해 두지 않으면, default값인 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

계산 프로퍼티의 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의 코드가 단일 표현식이라면 Return을 생략해도 해당 표현식의 결과가 반환된다.

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

계산 프로퍼티에서 getter만 있고 setter가 없는 경우 read-only computed property(읽기 전용 프로퍼티)라고 한다. 이름에서 알 수 있듯 이러한 프로퍼티는 값을 반환만 할 수 있으며 새로운 값을 할당할 수 없다.

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 프로퍼티와 같이 getter,즉 return만 존재하는 경우에는 get을 생략할 수 있다.


Property Observers

프로퍼티의 값의 변화를 관찰하고 반응하는 것으로, 새로운 값이 기존과 같더라도 프로퍼티 옵저버는 호출된다.
즉 프로퍼티가 set 될 때 마다 호출된다고 볼 수 있다. 프로퍼티 옵저버는 다음과 같이 사용할 수 있다.
1. 정의한 저장 프로퍼티
2. 상속한 저장 프로퍼티
3. 상속한 계산 프로퍼티

상속한 프로퍼티의 경우 자식 클래스에서 프로펕티 옵저버를 추가하려면 overriding을 사용하면 된다.
계산 프로퍼티의 경우 옵저버를 만드는 것 대신 setter에서 값의 변화를 관찰 및 응답할 수 있다.

프로퍼티의 옵저버에는 두가지 옵션이 있다.

  • willSet : 값이 저장되기 직전에 호출된다.
    : 새로운 값이 저장될 프로퍼티 값을 상수 매개변수로 전달한다. 이 매개변수 이름은 willSet구문안에서 사용할 수 있도록 이름을 선언할 수 있고, 선언하지 않는다면 기본값인 newValue가 매개변수 이름이 된다.

  • didSet : 값이 저장된 직후에 호출된다.
    : 프로퍼티의 기존 값이 상수 매개변수로 전달된다 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

위와 같이 클래스의 인스턴스를 생성하여 프로퍼티의 값을 새로 할당할 때 마다 willSet,didSet 옵저버가 호출되어 해당 구문이 실행되는 것이다. 또한 매개변수의 이름을 설정하지 않은 didSet의 경우 oldValue라는 이름이 매개변수의 이름으로 사용되는 것을 볼 수 있다.


Property Wrappers

Property wrapper는 프로퍼티 저장 방법을 관리하는 코드와 프로퍼티를 정의하는 코드를 구분하는 계층을 추가하는 것이다. 예를들어 만약 스레드 안정성 검사나 데이터를 DB에 저장하는 프로퍼티가 있는 경우 모든 프로퍼티에 대한 코드를 작성해야 하는데 Property wrapper를 사용하면 이러한 작업을 줄일 수 있다. 관리 코드를 한 번만 작성한 다음 여러 프로퍼팉에 적용하여 관리 코드를 재사용할 수 있기 때문이다.

Property wrapper 사용을 위해서는 구조체, 클래스, 열거형에서 wrappeValue라고 하는 프로퍼티를 선언해야 한다.

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

위의 코드는 wrappedValue에 항상 12보다 작은 값이 저장되게 만드는 코드이다. 만약 12보다 큰 수가 저장된다면 12가 저장되게 하는 코드이다. 위에서 선언한 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"

다른 구조체의 프로퍼티에 Property wrapper를 여러 번 재사용하여 사용할 수 있다
Property wrapper를 사용하기 위해선 프로퍼티 앞에 property wrapper의 이름을 @와 함께 써주면 된다.

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

이전의 코드와는 다른 방식으로 property wrapper를 적용하는 방법이다. 위와 같이 property wrapper를 사용하게 되면 property wrapper로 선언된 프로퍼티에 값을 저장해야 하기 때문에 getter,setter를 사용할 수 있게 된다.

Setting Initial Values for Wrapped Properties

위에서 작성된 코드는 property wrapper에서 초기 값을 정의할 수 있다. 그렇기 때문에 래핑된 프로퍼티는 다른 초기 값을 지정할 수 없다. 이러한 점을 보안하기 위해서는 property wrapper에 생성자를 추가해주면 된다.

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

3개의 생성자를 선언했다.

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

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

생성자를 통해 초기값을 선언

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

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

위와 같이 Height,width에 1을 할당하게 되면 이는 Property wrapper의 init(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"

위의 코드는 init(wrapperValue:maximum:) 생성자를 사용하는 예이다.
프로퍼티 래퍼에 인수를 포함하면 초기 상태를 설정하거나 래퍼가 생성될 때 다른 옵션을 래퍼에 전달할 수 있다.

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 인스턴스는 wrappedValue:1를 호출하여 초기화했고
width 프로퍼티를 감싸는 SmallNumber 인스턴스는 wrappedValue:2, maximum:9를 호출하여 초기화한다.

Projecting a Value From a Property Wrapper

래핑된 값 말고도 property wrapper는 projected value을 정의하여 추가 기능을 확장할 수 있다.
투영값은 $로 시작한다는 점만 제외하면 래핑된 값과 동일하다.

@propertyWrapper
struct SmallNumber {
    private var number = 0
    var 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"

위의 코드에서 Bool 타입의 projectedValue 프로퍼티를 추가하여 새 값을 저장하기 전에 property wrapper가 프로퍼티에 값을 초기화했는지 확인할 수 있습니다. 예를 들어 4를 저장할 경우엔 property wrapper가 새 값을 할당해 준 것이 아니기 때문에 false가 저장되어 있지만 55를 저장할 경우엔 property wrapper가 값을 조정하여 12를 저장하게 되기 때문에 이땐 true가 저장되게 됩니다. 물론 이렇게 Bool 타입 말고도 모든 타입의 값을 반환할 수 있습니다.

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

getter 또는 인스턴스 메서드와 같은 타입 선언의 일부인 곳에서 projected value값을 접근한다면 self를 제외해도 된다. 이러한 경우 $를 붙여주면 된다.


Global and Local Variables

프로퍼티를 계산하고 값이 변화하는 지 관찰하는 옵저버 기능은 전역변수, 지역변수에서도 활용할 수 있다.
전역변수는 함수,메서드,크롤로저 등의 외부에서 정의된 변수이고 지역변수는 함수,메서드,클로저 등의 내부에서 정의된 변수이다.

모든 전역 변수와 지역 변수는 stored variable이었는데 이는 저장 프로퍼티와 비슷하게 값에게 저장 공간을 주고 해당 값을 설정하고 검색할 수 있게 해준다.

물론 전역 범위와 지역 범위에서 computed variable를 정의하고 저장 변수에 대한 옵저버를 정의할 수도 있다. 계산 변수는 값을 저장하지 않고 계산 프로퍼티와 같은 방식으로 쓰인다.

전역 변수,상수는 Lazy 저장 프로퍼티와 동일한 방법으로 처리됩니다. 하지만 전역 변수,상수에 Lazy키워드를 표시할 필요가 없다. 이와 반대로 지역변수,상수에는 절대로 lazy를 사용할 수 없다.


Type Properties

특정 타입의 인스턴스를 만들때마다 다른 인스턴스들과는 별도로 각자의 프로퍼티가 생성됩니다.

하지만 type Properties는 특정 타입 자체에 속하는 프로퍼티를 만들어 특정 타입의 인스턴스를 몇 개 만들어도 같은 프로퍼티에 접근할 수 있도록 할 수 있다.

타입 프로퍼티는 어떠한 타입은 모든 인스턴스에 필요한 값들을 정의할 때 사용하면 유용하다. 이를 사용할 때 Stored type property를 만들면 항상 값이 존재해야 한다. 타입 자체에는 생성자가 없기 때문이다. 또한 저장 프로퍼티에 처음 접근될 때는 lazy형태로 초기화됩니다. 여러 스레스가 동시에 접근해도 한 번만 초기화되도록 보장되며 lazy를 명시적으로 표시할 필요는 없다.

Type Property Syntax

Swift에서는 타입 프로퍼티를 타입의 중괄호 안에 작성하여 명시적으로 지원됩니다. 타입 프로퍼티를 정의하기 위해선 static 키워드를 사용하면 된다.

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

위의 예는 저장,계산,타입프로퍼티를 사용하는 방법을 보여주는 예이다. 구조체,열거형,클래스의 프로퍼티를 선언할 때 static 키워드를 앞에 써주면 타입 프로퍼티로 정의된다.

위의 예에선 계산 타입 프로퍼티가 읽기 전용으로 선언되었지만 일반적인 계산 프로퍼티와 동일한 방법으로 읽기,쓰기 가능한 타입 프로퍼티로 정의할 수 있다.

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

두개의 타입 프로퍼티(threshlodLevel,maxInputLevelForAllChannels)가 선언되어 있다.
여기서 currentLevel 프로퍼티에는 수정할 때 마다 currentLevel의 값을 확인하는 didSet 프로퍼티 옵저버가 있다. currentLevel의 새 값이 허용된 thresholdLevel보다 큰 경우 currentLevel을 thresholdLevel로 수정합니다. 그런 뒤 currentLevel의 수정된 값이 기존의 maxInputLevelForChannels보다 큰 경우 maxInputLevelForChannels로 수정된 currentLvel의 값으로 수정한다.

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

위의 코드처럼 AudioChannel의 타입 프로퍼티인 thresHoldLevel값 보다 큰 값을 CurrentLevel의 값으로 주면 currentLevel의 값이 타입 프로퍼티인 thresholdLevel의 값과 동일하게 초기화된다.

0개의 댓글