프로퍼티(Properties)

린다·2022년 2월 20일
0

The Swift Language Guide

목록 보기
4/7
post-thumbnail

아래의 글은 The swift programming languagebbiguduk님, jusung님의 번역본을 보며 공부한 내용입니다.

정의

  • 값을 특정 클래스, 구조체, 열거형과 연결시킨 것
    • 주로 인스턴스와 연결되지만 타입 자체와도 연결이 가능함 → 타입 프로퍼티
    • 프로퍼티 관찰자(property observer)를 정의하여 프로퍼티의 값이 변경되는 것을 모니터링, 사용자 지정동작에 응답 가능
    • 프로퍼티 래퍼(property wrapper)를 사용하여 getter, setter를 통해 코드 재사용이 가능
  • Stored Property: 인스턴스의 일부, 상수와 변수 값을 저장(클래스, 구조체)
  • Computed Property: 값을 계산(클래스, 구조체, 열거형)

Stored Property

특정 클래스 또는 구조체의 인스턴스의 부분으로 저장된 상수 혹은 변수

  • var 혹은 let으로 사용 가능
  • 정의를 함과 동시에 default 값 제공이 가능(초기화와 함께)
struct FixedLengthRange {
	var firstValue: Int
	let length: Int // 길이는 변경되지 않는다는 것을 표현하기 위해
}

상수로 선언된 구조체 인스턴스의 Stored Property

  • 구조체 인스턴스를 상수로 선언해버리면 stored property가 var(변수)여도 수정이 불가능함
    • 구조체가 값 타입이기 때문에
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
  • 이와 반대로 클래스는 참조 타입이기 때문에 클래스 인스턴스를 상수로 선언해도 stored property가 변수이면 변경이 가능함

Lazy Stored Property

  • 사용될 때까지 초기화가 되지않는 프로퍼티
  • 키워드 lazy

let property는 초기화 전에 반드시 값을 가지고 있어야 하기 때문에 lazy로 선언이 불가능함
따라서 지연 프로퍼티는 반드시 var(변수)로 선언해야함

언제 사용될까?

  • 주로 인스턴스의 초기화가 완료될때까지 값을 알 수 없는 외부 요인에 의해 초기값이 달라질 때 유용함
  • 복잡하거나 계산 비용이 많이 드는 초기값인 경우에도 유용함

예시

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
  • 위와 같은 경우는 DataManager가 DataImporter() 인스턴스가 지금 당장 필요한 것이 아니기 때문에 처음에 초기화할때부터 생성하지 않고 lazy var로 선언하여 추후에 import를 사용해야할 때 접근, 생성하는 것
  • importer 인스턴스, 혹은 그의 프로퍼티에 접근하면 그때 프로퍼티가 생성됨

주의해야할 점

  • lazy 프로퍼티는 1) 여러 쓰레드에서 동시에 접근이 가능하고, 2) 프로퍼티가 초기화되기 전, 한 번만 초기화 된다는 보장이 없음

Computed Property

  • 값을 실질적으로 저장하지않고 계산을 하는 프로퍼티를 정의할 수 있음
  • 다른 프로퍼티의 값을 간접적으로 조회, 설정하는 getter, setter 제공

예시

// x, y 좌표의 위치
struct Point {
    var x = 0.0, y = 0.0
}

// width 와 height
struct Size {
    var width = 0.0, height = 0.0
}

// 원점과 크기로 사각형을 정의
struct Rect {
    var origin = Point()
    var size = Size()
		// center는 항상 origin과 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)"

짧은 Setter 선언 (Shorthand Setter Declaration)

  • setter를 사용할 때 새로운 값을 받게 되는데 이 새로운 값에 대한 이름을 설정해주지 않았다면 newValue라는 이름을 사용할 수 있음
set(newCenter) {
	origin.x = newCenter.x - (size.width / 2)
	origin.y = newCenter.y - (size.height / 2)
}
set {
	origin.x = newValue.x - (size.width / 2)
	origin.y = newValue.y - (size.height / 2)
}

짧은 Getter 선언 (Shorthand Getter Declaration)

  • getter의 바디가 단일 표현식(한줄로 구성)이라면 return 키워드를 생략해줄 수 있음
get {
	let centerX = origin.x + (size.width / 2)
	let centerY = origin.y + (size.height / 2)
	return Point(x: centerX, y: centerY)
}
get {
	Point(x: origin.x + (size.width / 2),y: origin.y + (size.height / 2))
}

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

  • setter가 없고 getter만 있는 computed property를 읽기 전용이라고 부름
    • 값을 읽을(get)수만 있고 설정(set)할 수 없기 때문에
  • 이런 경우, 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"

프로퍼티 관찰자 (Property Observers)

정의

  • 프로퍼티 값이 변경되는지 관찰하고 응답함
  • 프로퍼티의 현재값과 새로 설정된 값이 동일해도 설정만 된다면 호출됨

적용가능한 경우

stored property, computed property 모두 가능한데 내가 정의한 computed property만 안된다고 보면 됨

  • Stored properties that you define(내가 정의한 stored property)

  • Stored properties that you inherit(상속한 stored property)

  • Computed properties that you inherit(상속한 computed property)

  • 상속된 프로퍼티에 관찰자를 적용하고자 하는 경우에는 하위 클래스(상속한 클래스)의 프로퍼티를 재정의하여 프로퍼티 관찰자를 추가하면 됨

  • 여기에 없는 정의한 계산된 프로퍼티의 경우, 관찰자를 적용하지 않고 setter를 이용해서 값 변경을 관찰 및 응답할 수 있음

종류

  • willSet
    • 값이 저장/변경되기 직전에 호출
    • 앞으로 바뀔 new value를 상수 파라미터로 전달받을 수 있음
  • didSet
    • 값이 저장/변경된 직후에 호출
    • 변경되기 전의 old value를 상수 파라미터로 전달받을 수 있음

참고 1

수퍼 클래스(상속된 클래스) 프로퍼티의 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

참고 2

관찰자를 가진 프로퍼티를 in-out 파라미터로 함수에 전달하면 willSet 과 didSet 관찰자는 항상 호출됩니다. 이것은 in-out 파라미터에 대한 copy-in-copy-out 메모리 모델 때문에 그렇습니다. 값은 함수 끝에서 프로퍼티에 항상 다시 작성됩니다.

프로퍼티 래퍼 (Property Wrappers)

정의

프로퍼티가 저장되는 방법을 관리하는 코드와 프로퍼티를 정의하는 코드 사이에 분리 계층을 추가함

  • 예를 들어 1) Thread 안정성 검사를 제공하거나 2) 기본 데이터를 데이터베이스에 저장하는 프로퍼티가 있는 경우, 모든 프로퍼티에 해당 코드를 작성해야함
  • property wrapper를 사용하고 싶다면, wrapper를 정의할 때 관리 코드를 작성하고 해당 관리 코드를 다양한 프로퍼티에 적용하면서 재사용해야함
  • 키워드 @propertyWrapper

예시

@propertyWrapper
struct TwelveOrLess {
    private var number = 0
    var wrappedValue: Int {
        get { return number }
        set { number = min(newValue, 12) }
    }
}
  • property wrapper를 정의하기 위하여 wrappedValue 프로퍼티를 정의한 구조체, 열거형, 클래스를 구현함
  • 이때 TwelveOrLess 구조체는 래핑하는 값이 늘 12보다 작거나 같게 설정됨
  • wrappedValue에 새로운 값을 대입하면 12와 비교하여 더 작은 값을 number에 set해주고, wrappedValue를 불러오면 현재 저장된 값을 return해줌
  • 이때 number가 private으로 설정된 것은 구현에서만 사용될 수 있도록 제한을 둔 것, 다른 곳에서 작성하는 코드는 wrappedValue로만 해당 값에 접근할 수 있고 직접적으로 number에 접근이 불가능
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 property wrapper를 사용하여 항상 12이하의 값을 저장할 수 있도록 구현한 struct
  • var rectangle을 초기화한 후 print하면 0을 얻을 수 있는 것은 number가 0으로 초기화 돼있기 때문
  • 24로 Height를 수정하려고 하면 12와 비교 후 더 작은 값을 저장하기 때문에 24가 아닌 12가 저장됨

활용

  • 프로퍼티에 래퍼를 적용할 때 컴파일러에서 래퍼를 위한 저장소를 제공하는 코드 + 레퍼를 통해 프로퍼티에 접근하는 코드를 합성함
    • 프로퍼티 래퍼 자체는 래핑된 값을 저장하는 역할만 하기 때문에 이에 대한 합성 코드는 없음
  • @propertyWrapper 키워드 없이도 프로퍼티 래퍼의 역할을 해낼 수 있는 방법이 있음. 해당 역할을 그냥 struct내부에 작성해주는 방법
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 }
    }
}
  • 이때 _height와 _width는 프로퍼티 래퍼의 인스턴스인 TwelveOrLess() 를 저장하고 있음
  • 그리고 동시에 height와 width의 getter, setter는 wrappedValue에 접근하고 있음

래핑된 프로퍼티를 위한 초기값 설정 (Setting Initial Values for Wrapped Properties)

  • 래핑된 프로퍼티는 외부에서 초기값을 수정할 수 없음 ex. SmallRectangle의 width, height
  • 초기값을 설정하기 위해서는 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개의 초기화 구문을 지원하고 있음
  1. 파라미터를 아무것도 받지 않는 초기화: maximum = 12, number = 0
struct ZeroRectangle {
    @SmallNumber var height: Int
    @SmallNumber var width: Int
}

var zeroRectangle = ZeroRectangle()
print(zeroRectangle.height, zeroRectangle.width)
// Prints "0 0"
  1. value만 받는 초기화: 이때 maximum은 자동으로 12로 정해지고 입력한 값과 12를 비교하여 number를 치고화함
struct UnitRectangle {
    @SmallNumber var height: Int = 1
    @SmallNumber var width: Int = 1
}

var unitRectangle = UnitRectangle()
print(unitRectangle.height, unitRectangle.width)
// Prints "1 1"
  1. wrappedValue와 maximum을 모두 입력받는 초기화: 주어진 value와 maximum을 모두 사용하여 maximum과 number를 초기화함
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"

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

  • 래핑된 값 외에도 projected value를 통해 추가적인 기능을 노출할 수 있음
  • projected value의 이름은 wrappedValue와 이름이 동일하지만 앞에 $ 표시가 붙어잇음
  • projected value의 타입은 상관없음! 다른 데이터 타입의 인스턴스도 가능, wrapper의 인스턴스를 노출하기 위해 self도 반환할 수 있음
@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"
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
    }
}
  • 위의 코드에서 resize(to: large)를 수행하면 height와 width가 12, 12로 변경되고 각각의 projected value도 true로 업데이트됨
  • return $height || $width 는 프로퍼티 래퍼가 height 혹은 width를 변경했는지 판단하기 위하여 체크

전역과 지역 변수 (Global and Local Variables)

정의

전역변수: 함수, 메서드, 클로저, 타입 컨텍스트 외부에 정의된 변수

지역변수: 함수, 메서드, 클로저, 타입 컨텍스트 내부에 정의된 변수

참고

전역 상수, 변수는 lazy property처럼 항상 느리게 계산됨 하지만 lazy 키워드가 필요한것은 x

특징

  • computed local variable에 프로퍼티 래퍼 적용 가능
  • global variable/computed variable에는 적용 불가능

예시

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

    myNumber = 10
    // now myNumber is 10

    myNumber = 24
    // now myNumber is 12
}

타입 프로퍼티 (Type Properties)

정의

  • 인스턴스에 속하는 프로퍼티인 인스턴스 프로퍼티와는 다르게 타입 자체에 속하는 타입 프로퍼티를 선언할 수 있음 → 딱 하나만 존재함
  • 특정 타입에 모든 인스턴스에 보편적인 값을 정의하는데 유용함
  • var, let 둘 다 가능(동일하게 computed는 늘 var로 선언)

참고

  • stored Type property는 늘 초기값을 가지고 있어야함

→ 초기화 시 저장된 타입 프로퍼티에 값을 할당할 수 있는 초기화를 가지고 있지 않기 때문에~

  • stored type property는 lazy하게 초기화됨. 여러 스레드가 동시에 접근해도 한 번만 초기화됨.

타입 프로퍼티 구문 (Type Property Syntax)

  • 키워드 static
  • class의 computed type property인 경우, 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
    }
}

타입 프로퍼티 조회와 설정 (Querying and Setting Type Properties)

접근

타입.프로퍼티로 접근할 수 있음

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의 값이 변경되는 경우에는 관찰자가 다시 호출되지 않음

0개의 댓글