[Swift] Properties (프로퍼티)

LEEHAKJIN-VV·2022년 5월 10일
0

Study-Swift 5.6

목록 보기
9/22

참고사이트:
The Swift Programming Language


Properites (프로퍼티)

Properties는 클래스, 구조체, 열거형과 관련한 값이다. 프로퍼티의 종류에는 Stored properties(저장 프로퍼티), Computed properties(계산된 프로퍼티)가 있다. Stored properites는 값을 저장하고 있는 프로퍼티고, computed properties는 값을 저장하지 않고 특정하게 계산한 값을 반환해 주는 프로퍼티다. 계산된 프로퍼티는 클래스, 구조체, 열거형 모두에서 사용 가능하지만, 저장 프로퍼티는 클래스와 구조체에서만 사용 가능하다.

게다가 property observers를 정의하여 프로퍼티의 값의 변화를 모니터링하고, 사용자가 원하는 행동을 취할 수 있다.


Stored Properties (저장 프로퍼티)

Stored property의 가장 간단한 형태는 class나 structure의 인스턴스의 상수나 변수이다. 이 프로퍼티는 let키워드를 이용하여 상수 혹은 var키워드를 이용하여 변수로 선언해 사용할 수 있다.

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라는 변수 stored property와 length상수 stored property를 가지고 있다. lengh는 상수이기 때문에 새로운 범위가 만들어지고 나서 값이 변경되지 않는다.

Stored Properties of Constant Structure Instances (상수 구조체 인스턴스의 저장 프로퍼티)

structure의 인스턴스를 상수로 정의하면 인스턴스의 프로퍼티가 변수로 선언되더라도 그 값을 변경할 수 없다.

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

이는 structure가 value type(값 타입)이기 때문이다. value type의 인스턴스가 상수로 선언되면 그 인스턴스의 모든 프로퍼티도 상수가 된다.

그러나 reference type(참조 타입)인 class는 다르다. reference type의 인스턴스를 상수로 선언해도 인스턴스의 변수 프로퍼티는 값을 바꿀 수 있다.


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

Lazy stored property(지연 저장 프로퍼티)는 처음 사용되기 전까지 초깃 값이 계산되지 않는 프로퍼티다. 선언을 하기 전 제일 앞에 lazy키워드를 작성하여 사용한다.

NOTE
lazy property는 반드시 변수로 선언해야 한다. 왜냐하면 상수는 초기화가 되기 전에 항상 값을 갖는 프로퍼티로, lazy property는 처음 사용되기 전에는 값을 갖지 않는 프로퍼티이기 때문이다.

Lazy propery는 속성의 초기 값이 인스턴스 초기화가 완료될 떄까지 특정한 외부 요인에 의존될때 유용하다. 또 복잡한 계산이나 부하가 많이 걸리는 작업을 lazy propery 선언해 사용하면 실제 사용되기 전에는 실행되지 않아서 인스턴스의 초기화 시점에 복잡한 계산을 피할 수 있다.

다음 예제는 lazy stored prooery를 사용하여 class의 복잡한 초기화를 피한다.

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의 기능 중 일부는 파일로부터 중요한 데이터를 가져오는 것이다. 이 기능은 DataImporter class에서 가져오며 상당한 시간이 걸린다고 가정한다.

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"

NOTE
만약 멀티 스레드가 동시에 lazy 식별자를 접근하면 그 프로퍼티는 한 번만 실행되는 것을 보장하지 않는다. 단일 스레드에서 사용 시 초기화는 한 번만 수행한다.


Computed Properties (계산된 프로퍼티)

클래스, 구조체, 열거형에서 값을 저장하지 않는 Computed properties를 정의할 수 있다. 그 대신 다른 프로퍼티와 그리고 값을 간접적으로 검색하고 변경할 수 있는 getteroptional 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
// 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)"

위 예제는 기하학적 모형을 위해 3개의 structure를 정의한다.

  • Point: 점의 x, y좌표 캡슐화
  • Size: width와 height 캡슐화
  • Rect: 원점과 크기로 사각형을 정의

Rect structure는 center라는 computed property를 제공한다.Rect의 현재 중심 위치는 항상 원점과 크기로부터 결정되기 때문에 중심점을 명시적으로 값(으로 저장하고 있을 필요 없다.(중심 위치를 계속 저장하고 있을 필요가 없기 때문에 stored property 대신 computed property 사용) 대신 Rectcenter라고 불리는 computed variable에 대한 custom getter와 setter를 정의하여 실제로 저장된 프로퍼티처럼 사각형의 중심을 저장할 수 있다.

위의 예에서 square라는 불리는 새로운 변수 Rect를 생성한다. square변수는 원점이 (0,0) width와 height가 10으로 초기화 된다. 이 사각형은 아래 그림의 밝은 초록색이다.

(square.centr)와 같이 점 문법을 통해 square의 변수의 center 프로퍼티에 접근하고, center가 호출될 때 getter를 이용하여 현재 프로퍼티의 값을 검색한다. 그 다음 getter는 새로운 정사각형의 중심을 나타내는 Point (5,5)를 반환한다.

다음 코드는 새로운 값인 (15,15)는 사각형을 이동시켜 오른쪽 위로 이동하여 짙은 녹색으로 표시된다. center 프로퍼티를 수정하면 center의 setter가 호출되고 이것은 원점, x와 y 값을 수정하여 새로운 위치로 사각형을 이동시킨다.


Shorthand Setter Declaration (Setter 선언의 간락한 표현)

만약 computed property의 setter가 새로운 값이 저장될 인자 이름을 지정하지 않으면 default value인 newValue가 사용된다. 아래는 default value를 적용한 예시이다.

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의 body가 single expression(한줄)이면 getter는 암시적으로 반환할 수 있다. 다음 예제는 Shorthand Getter와 Shorthand Setter를 사용하였다.

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키워드를 생략하는 것은 함수에서 return키워드를 생략하는 규칙과 같다.[swift] function


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

setter가 없고 오직 getter만 있는 computed property를 read-only computed property라고 한다. 이는 항상 값을 반환하고 점(.) 문법을 통해 접근할 수 있다. 그러나 다른 값을 지정할 수는 없다.

NOTE
read-only computed properties를 포함하여 computed properties는 값이 고정되어 있지 않다. 그래서 변수(var)로 선언되어야 한다. 보통 read-only라고 해서 읽기 전용이기 때문에 let으로 선언한다고 생각할 수 있다. 그러나 computed properties는 계산 값에 따라 값이 변할 수 있기 떄문에 var로 선언한다.

get키워드와 중괄호 {} 를 생략하여 read-only computed property를 선언할 수 있다.

struct Cuboid {
    var width = 0.0, height = 0.0, depth = 0.0
    var volume: Double { // read-only computed property
        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(프로퍼티 옵저버)는 프로퍼티의 값의 변화를 관찰하고 반응한다. Property observers는 프로퍼티의 현재 값과 똑같은 값이 지정되더라도 항상 값의 변화가 생길 때마다 호출된다.

다음 위치에 property observer를 추가할 수 있다.

  • Stored properties that you define (저장 프로퍼티)
  • Stored properites that you inherit (상속받은 저장 프로퍼티)
  • Computed properties that you inherit (상속받은 계산 프로퍼티)

상속받은 프로퍼티의 경우 subclass에서 프로퍼티를 overriding(오버라이딩) 함으로써 property observer를 추가할 수 있다. computed property의 경우 프로퍼티의 setter로 값의 변화를 관찰하고 반응할 수 있으므로 oberver가 필요 없다.

Property observer는 정의할 수 있는 2가지 옵션이 있다.(1개만 or 모두 구현 가능)

  • willSet: 값이 저장되기 직전에 호출
  • didSet 새로운 값이 저장된 직후에 호출

만약 willSet observer를 구현하면 상수 파라미터로 새로운 값이 전달된다. 이 파라미터의 이름을 지정할 수 있으나 이름과 괄호를 작성하지 않으면 default인 newValue를 사용해야 한다.

유사하게 didSet observer를 구현하면, 이전 프로퍼티의 값이 상수 파라미터로 전달된다. 파라미터의 이름을 작성하거나 default 이름인 oldValue를 사용한다. 만약 didSet observer context 내에서 프로퍼티에 값을 할당하면 새로운 값이 방금 설정된 값(didset으로 넘어온 값)을 대체한다.

NOTE
superclass 프로퍼티의 willSet, didSet observer는 superclass의 initializer가 호출된 후 sub class의 initializer에서 프로퍼티가 설정될 떄 호출된다.

property observer관련한 예제를 살펴 보자.

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 class는 totalSteps프로퍼티를 선언했다. 이는 willSet,didSet observers를 가지는 stored property다.

NOTE
observers가 있는 속성을 in-out parameter로 선언된 함수의 인자에 넘기면 willSet,didSet는 계속 호출된다. 이는 in-out parameter가 copy-in copy-out model이기 때문이다.


Global and Local Variables

앞에서 설명한 계산된 프로퍼티와 옵저버 프로퍼티는 전역변수와 지역변수에 이용이 가능하다. 전역변수는 함수, 메소드, 클로저, 타입 컨텍스트 외부에 선언된 변수이다. 지역 변수는 함수, 메소드 클로저 컨텍스트 내부에 선언된다.

이전 챕터에서 접했던 전역변수 및 지역변수는 모두 stored variables이다. stored properties와 같은 stored variables는 특정한 타입의 값을 위한 저장소를 제공하고 값을 지정하고 검색할 수 있도록 한다.

그러나 전역 또는 지역 범위 내에서 computed variables을 정의하고 stored variables의 observers를 정의할 수 있다. Computed variables는 값을 저장하지 않고 값을 계산하며 computed properties와 같은 방식으로 작성된다.

NOTE
전역 상수와 변수들은 항상 lazily(늦게) 계산되고, 이는 Lazy Stored Properties(지연 저장 프로퍼티)와 비슷하다. lazy stored properties와는 다르게 전역 상수 및 변수들은 lazy식별자를 작성할 필요가 없다.
지역 변수및 상수들은 lazy 식별자 없이는 lazily computed되지 않는다.


Type Properties

Instance 프로퍼티는 인스턴스의 특정 타입에 속하는 프로퍼티다. 해당 타입의 인스턴스를 새로 만들 때마다, 이는 다른 인스턴스와 분리된 고유의 프로퍼티 값을 가진다.

Type properties는 특정 타입에 속한 프로퍼티로 그 타입에 해당하는 단 하나의 프로퍼티만 생성된다.

Type properties는 특정 타입의 모든 인스턴스에 공통으로 사용되는 값을 정의하는데 유용하다. (C의 static constant, static variable)

NOTE
Type properties는 stored instance properties와 다르게 항상 default value를 설정해야 한다. 이는 초기화 시, 타입 자체에 stored type property에 값을 할당할 수 있는 initializer(초기자)가 없기 때문이다.

Stored type properties는 처음 접근에서 lazily 하게 initialized 된다. 이들은 멀티 스레드 환경에서도 한번 initialized(초기화) 되는것을 보장하고 lazy 식별자를 작성할 필요가 없다.


Type Property Syntax

C와 Objective-C에서는 static constants와 variables를 global static variables(전역 정적 변수)로 선언한다. 그러나 Swift에서 Type properties는 선언의 일부분으로 타입의 외부 중괄호 안에 작성된다.

static키워드를 이용하여 type property를 선언한다. class 타입의 computed type 프로퍼티는 class키워드를 사용하고 superclass(상위 클래스)의 구현을 subclass(하위 클래스)가 override (재정의) 할 수 있도록 한다. 아래 예제는 stored type property와 computed type property를 보여준다.

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

NOTE
위의 computed type property는 read-only이지만 computed instance properties에서는 같은 문법으로 read-write 할 수 있다.


Querying and Setting Type Properties ( 타입 프로퍼티의 접근과 설정)

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"

Type propertiy를 점 구문으로 접근한 것을 확인할 수 있다. 이때 type propertiy의 타입은 인스턴스 타입일까? 확인을 해 보자.

print(type(of: SomeStructure.storedTypeProperty))
// Prints "String"

예제에서 확인할 수 있듯이 structure의 인스턴스인 SomeStructure 타입이 아니라 type property 자체의 타입인 String 타입인 것을 확인할 수 있다.

다음 예제는 오디오 채널의 볼륨을 조절하고 관리하는 구조체 AudioChannel을 보여준다.

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

현재 오디오의 최대 볼륨 크기(왼쪽, 오른쪽 채널 중 최댓값 의미)는 type property인 maxInputLevelForAllChannels로 관리하고 채널별 볼륨의 최댓값은 thresholdLevel 상수로 정의하였다. 현재 볼륨(currentLevel)이 설정될 때마다 didSet(프로퍼티 옵저버)가 호출된다.

NOTE
didSet은 프로퍼티 옵저버로 관찰하고 있는 프로퍼티의 값이 수정되면 수정된 직후에 호출된다. 그런데 didSet context를 보면 관찰하고 있는 프로퍼티(currentLevel)의 값을 조건에 따라 변경하고 있는 것을 볼 수 있다. 이때는 값을 변경하더라도 didSet이 반복 호출되지 않는다.

이렇게 만든 오디오 채널 구조체를 이용하여 좌우 2개의 오디오 채널을 생성한다.

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

왼쪽 채널에 볼륨을 7로 설정하고 type property의 값을 확인하자.

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

오른쪽 체널에는 채널의 최대볼륨이 넘는 11을 할당한다.

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

currentLevel이 최댓값이 10으로 설정되고 채널의 최대 볼륨도 10으로 설정됨을 확인할 수 있다.
이 예제들을 통해서 leftChannel 인스턴스와 rightChannel 인스턴스가 서로 다른 인스턴스임에도 구조체 내의 type property인 thresholdLevel과 maxInputLevelForAllChannels을 공유하고 있는 것을 확인할 수 있다.

0개의 댓글