Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

프로퍼티 #46

Open
simoniful opened this issue Oct 24, 2022 · 0 comments
Open

프로퍼티 #46

simoniful opened this issue Oct 24, 2022 · 0 comments

Comments

@simoniful
Copy link
Owner

simoniful commented Oct 24, 2022

프로퍼티(Property)는 클래스, 구조체, 열거형과 연관된 값
저장 프로퍼티(stored property)는 인스턴스의 일부로써 상수나 변수를 저장
계산 프로퍼티(computed property)는 값을 계산하는 기능
저장 프로퍼티는 클래스와 구조체에서만 사용 가능하며,
계산 프로퍼티는 클래스와 구조체, 열거형 모두에서 사용 가능

프로퍼티는 보통 특정 타입의 인스턴스와 연관
또한, 타입 프로퍼티처럼 그 자신의 타입과 관련될 수도 있다

프로퍼티 옵저버를 정의해서 프로퍼티의 값의 변화를 관찰 가능
프로퍼티 옵저버는 자신의 저장된 프로퍼티, 또는 부모 클래스로부터 상속받은 프로퍼티에도 적용하여 관찰 가능

여러 프로퍼티의 게터와 세터 코드를 재사용하기 위해 프로퍼티 래퍼를 사용 가능

저장 프로퍼티 (Stored Properties)

가장 기본적인 형태의 프로퍼티
저장 프로퍼티(stored property)는 특정 클래스나 구조체 인스턴스의 일부분으로써 저장된 상수 또는 변수
저장 프로퍼티는 변수 / 상수로 정의 가능

저장 프로퍼티를 정의할 때 기본 값을 지정 가능
초기화 과정에서 저장 프로퍼티에 초기 값을 설정하거나 수정하는 것도 가능
이는 상수 저장 프로퍼티에도 허용

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은 상수로 선언됐으며, firstValue 프로퍼티가 변수로 선언되었음에도 바꾸는 것이 불가능
구조체는 값 타입(value type)이기 때문
값 타입의 인스턴스가 상수로 지정되면, 그것의 모든 프로퍼티도 상수로 지정

클래스는 참조 타입이기 때문에 가능
참조 타입을 상수로 선언해도 인스턴스의 변수 프로퍼티는 변경 가능

2) 지연된 저장 프로퍼티

지연된 저장 프로퍼티(lazy stored property)는 처음으로 사용되기 전까지는 초기 값이 계산되지 않는 프로퍼티
선언부 앞에 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 has not yet been created

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

DataManager 클래스는 [String]로 초기화되는 data 저장 프로퍼티를 소유
DataManager의 목적은 문자열 데이터 배열에 대한 접근 또는 관리를 제공

DataManager 클래스 기능의 일부는 파일로부터 데이터를 불러오는 것
해당 기능은 초기화하는 데 어느정도의 시간이 필요하기에 DataImporter 클래스로부터 제공
DataImporter 인스턴스는 파일을 열고, 그것의 내용을 메모리에 읽기 때문에 인스턴스를 초기화할 때 시간이 소요된다고 가정

DataManager 클래스는 파일로부터 불러오지 않아도 데이터를 다루는 것이 가능
DataManager 인스턴스를 만들 때 DataImporter 인스턴스까지 만들 필요가 없다는 뜻
대신, DataImporter가 처음으로 사용될 때 그것을 만들도록 구성

lazy 키워드가 표기됐기 때문에 importer 프로퍼티의 DataImporter 인스턴스는 프로퍼티에 처음접근할 때만 만들어진다

만악 초기화되지 않은 lazy 프로퍼티가 여러 스레드에서 동시에 접근할 경우, 프로퍼티가 하나만 생성된다고 보장할 수 없다

3) 저장된 프로피티와 인스턴스 변수

Objective-C는 값을 저장하거나 클래스 인스턴스를 참조하기 위한 두 가지 방법을 제공
ex. 점 연산자: instance.property = value / set 연산: instance.setProperty(value)
추가적으로 인스턴스 변수를 프로퍼티에 저장된 값에 대한 백업 저장소로 사용 가능
뿐만 아니라 뿐만 아니라 메모리 관리와 관련한 개념도 프로퍼티에 함께 명시
ex. @Property (nonatomic, retain) NSString *propertyName와 같은 형태

Swift는 이러한 개념을 하나의 프로퍼티 선언에 통합
Swift 프로퍼티는 해당 인스턴스 변수를 갖지 않고, 프로퍼티의 저장 공간에 직접적으로 접근하지 않음
이러한 접근법은 서로 다른 문맥에서 값에 접근하는 방식에 따른 혼란을 피하고,
프로퍼티의 선언을 하나의 일정한 구문으로 간소화
프로퍼티에 대한 모든 정보(이름, 타입, 메모리 관리 특성을 포함한)는 타입 정의의 일부분으로써 하나의 장소에 정의

계산 프로퍼티 (Computed Properties)

클래스, 구조체, 열거형은 계산 프로퍼티(computed property)를 정의 가능
여기에는 실제 값이 저장되지 않는다
대신 다른 프로퍼티와 값을 간접적으로 검색하고 설정할 수 있는 게터와 옵셔널 세터를 제공

// 기하학적 모양에 대한 Point, Size, Rect 세 개의 구조체를 정의

// Point는 x와 y좌표를 캡슐화
struct Point {
    var x = 0.0, y = 0.0
}

// Size는 너비와 높이를 캡슐화
struct Size {
    var width = 0.0, height = 0.0
}

// Rect는 origin point와 size를 통해 사각형을 정의
struct Rect {
    var origin = Point()
    var size = Size()
    // 계산 프로퍼티 - center 정의
    var center: Point {
       // 처음 가운데 위치는 origin과 size에 의해 결정 - 가운데 위치를 명시적인 Point 값으로 저장 X
       // Rect는 center 계산 프로퍼티를 위한 커스텀 게터와 세터를 정의
       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)
        }
    }
}

// 새로운 Rect 변수인 square 정의 - (0, 0)의 origin point와 10의 width와 height로 초기화
var square = Rect(origin: Point(x: 0.0, y: 0.0), size: Size(width: 10.0, height: 10.0))

// 최초값은 남겨두고 center 프로퍼티는 새로운 값인 (15, 15)로 할당
let initialSquareCenter = square.center
square.center = Point(x: 15.0, y: 15.0)

// center 프로퍼티를 세팅하는 것은 center의 세터를 호출하고, origin 프로퍼티에 저장된 x, y 값을 수정
// 정사각형은 새로운 위치로 옮겨진다
print("square.origin is now at (\(square.origin.x), \(square.origin.y))")
// Prints "square.origin is now at (10.0, 10.0)"

1) 세터 선언 축약

계산 프로퍼티의 세터가 새로운 값의 이름을 정의하지 않으면, 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)
        }
    }
}

2) 게터 선언 축약

게터의 전체 바디가 한 줄로 표현된다면 그 표현을 암시적으로 반환 가능

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) 읽기 전용 계산 프로퍼티

게터는 있지만 세터는 없는 계산 프로퍼티를 읽기 전용 계산 프로퍼티(read-only computed property)라고 부른다
읽기 전용 프로퍼티는 점 문법으로 접근할 수 있고 항상 값을 반환하지만, 새로운 값을 설정할 수는 없다

읽기 전용 계산 프로퍼티를 포함한 모든 계산 프로퍼티는 값이 고정되어 있지 않기 때문에 반드시 변수로 선언
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"

프로퍼티 옵저버 (Property Observers)

프로퍼티 옵저버(property observer)는 프로퍼티 값의 변화를 관찰하고 변화 시 반응
프로퍼티 옵저버는 프로퍼티 값이 설정될 때마다 호출
현재 값과 똑같은 값이 들어오더라도 마찬가지

  • 커스텀하게 정의한 저장 프로퍼티
  • 부모 클래스로 상속 받은 저장 프로퍼티
  • 부모 클래스로 상속 받은 계산 프로퍼티

상속된 프로퍼티의 경우 자식 클래스에서 해당 프로퍼티를 오버라이딩하여 옵저버를 추가
오버라이딩의 경우 별도로 정리 예정
스스로 커스텀하게 정의한 계산 프로퍼티의 경우, 관찰자를 만드는 대신 settter를 사용하여 값 변경에 대한 관찰과 응답 구성

프로퍼티 옵저버엔 두 가지 옵션이 있다.

  • willSet은 값이 저장되기 이전에 호출된다.
  • didSet은 새로운 값이 저장된 이후에 즉시 호출된다.

willSet 옵저버를 구현했다면 새 값의 매개 변수 이름을 지정할 수 있는데,
지정하지 않으면 기본 값으로 newValue를 사용

didSet 옵저버를 구현한다면 바뀌기 전의 매개 변수 이름을 지정할 수 있는데,
지정하지 않으면 oldValue가 기본값으로 사용

부모 클래스 프로퍼티의 willSet 및 didSet 관찰자는
부모 클래스 이니셜라이저가 호출된 후 자식 클래스 이니셜라이저에서 프로퍼티를 설정할 때 호출
자식 클래스가 자체 프로퍼티를 설정하는 동안 부모 클래스 이니셜라이저가 호출되기 전에 호출되지 않는다

아래는 willSet과 didSet을 사용하는 예시
새로운 클래스 StepCounter를 정의
만보계 또는 다른 걸음 계산기로부터 들어오는 데이터를 사용하여 하루 루틴에서의 운동을 추적

// Int의 totalSteps 프로퍼티를 선언
// 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은 항상 호출
인아웃 매개 변수에서는 프로퍼티가 항상 복사(카피 인 카피 아웃 메모리 모델)되기 때문
함수가 끝날 때 프로퍼티는 원래 값에 새 값을 덮어 쓰게 된다

프로퍼티 래퍼 (Property Wrapper)

프로퍼티 래퍼(property wrapper)는
프로퍼티가 저장되는 방식을 다루는 코드와
프로퍼티를 정의하는 코드를 분리하는 단계를 더한다

ex. 스레드 안전 체크를 하거나 데이터베이스에 있는 데이터를 저장하는 프로퍼티
모든 프로퍼티에 확인과 저장에 대한 코드를 작성
프로퍼티 래퍼를 사용한다면, 래퍼를 정의할 때 관리 기능을 하는 코드는 한 번만 작성
그리고 여러 프로퍼티에 그 코드를 적용함으로써 재사용 가능

// TwelveOrLess 구조체는 그것이 감싸는 값이 항상 12 이하의 숫자를 포함한다는 것을 보장
@propertyWrapper
struct TwelveOrLess {
    // number의 선언은 private으로 표시
    // number 프로퍼티는 오직 TwelveOrLess의 구현에서만 사용될 수 있다는 의미
    private var number: Int
    init() { self.number = 0 }

    // wrappedValue 프로퍼티를 정의하여 프로퍼티 래퍼 구성
    // 세터는 새로운 값이 항상 12 이하임을 보장하고, 게터는 저장된 값을 반환
    var wrappedValue: Int {
        get { return number }
        set { number = min(newValue, 12) }
    }
}

// 래퍼의 이름을 프로퍼티 전에 속성처럼 표기하여 프로퍼티에 래퍼를 적용 가능
struct SmallRectangle {
    @TwelveOrLess var height: Int
    @TwelveOrLess var width: Int
}

var rectangle = SmallRectangle()
print(rectangle.height)
// Prints "0"
// height와 width 프로퍼티는 TwelveOrLess로부터 초기 값 0을 갖게 된다

rectangle.height = 10
print(rectangle.height)
// Prints "10"
// 숫자 10을 rectangle.height에 저장하는 것은 성공

rectangle.height = 24
print(rectangle.height)
// Prints "12"
// 24를 저장하려 하면 세터의 규칙에 의해 12가 대신 저장

프로퍼티에 래퍼를 적용할 때,
컴파일러는 래퍼를 제공하는 저장 공간과, 래퍼를 통해 프로퍼티에 접근하는 코드를 합성한다
프로퍼티 래퍼의 행위를 특별한 속성 문법의 장점(@TwelveOrLess)을 취하지 않고 사용하여 코드 작성도 가능

struct SmallRectangle {
    // _height와 _width 프로퍼티는 TwelveOrLess 프로퍼티 래퍼 인스턴스를 저장
    private var _height = TwelveOrLess()
    private var _width = TwelveOrLess()
    
    // height와 width에 대한 게터와 세터로 wrappedValue 프로퍼티에 접근 가능
    var height: Int {
        get { return _height.wrappedValue }
        set { _height.wrappedValue = newValue }
    }
    var width: Int {
        get { return _width.wrappedValue }
        set { _width.wrappedValue = newValue }
    }
}

1) 포장된 프로퍼티 (Wrapped Properties)에 초기 값 설정하기

위의 예시는 TwelveOrLess의 정의에서 number에 초기 값을 설정했다

// TwelveOrLess 구조체는 그것이 감싸는 값이 항상 12 이하의 숫자를 포함한다는 것을 보장
@propertyWrapper
struct TwelveOrLess {
    // number의 선언은 private으로 표시
    // number 프로퍼티는 오직 TwelveOrLess의 구현에서만 사용될 수 있다는 의미
    private var number: Int
    init() { self.number = 0 }

    // wrappedValue 프로퍼티를 정의하여 프로퍼티 래퍼 구성
    // 세터는 새로운 값이 항상 12 이하임을 보장하고, 게터는 저장된 값을 반환
    var wrappedValue: Int {
        get { return number }
        set { number = min(newValue, 12) }
    }
}

이런 프로퍼티 래퍼를 사용하는 코드는 TwelveOrLess에 의해 포장된 프로퍼티에 서로 다른 초기 값을 특정할 수 없다
ex. SmallRectangle은 height와 width에 초기 값을 전달하지 못한다
초기 값과 다른 커스터마이징을 위해, 프로퍼티 래퍼는 initializer를 추가해야 한다

@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 ZeroRectangle {
    @SmallNumber var height: Int
    @SmallNumber var width: Int
}

var zeroRectangle = ZeroRectangle()
print(zeroRectangle.height, zeroRectangle.width)
// Prints "0 0"
// 초기 값 없이 프로퍼티에 래퍼를 적용할 때, Swift는 init() initializer를 사용

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

var unitRectangle = UnitRectangle()
print(unitRectangle.height, unitRectangle.width)
// Prints "1 1"
// 프로퍼티에 초기 값을 지정한다면 Swift는 init(wrappedValue:) initializer를 사용해 래퍼를 설정

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"
// 커스텀 속성(@SmallNumber) 소괄호 안에 인자를 전달하면, 
// Swift는 이 인자를 받아들이는 initializer를 사용해 래퍼를 설정
// ex. 초기 값과 maximam 값을 준다면 Swift는 init(wrappedValue:maximum:) initializer를 사용

narrowRectangle.height = 100
narrowRectangle.width = 100
print(narrowRectangle.height, narrowRectangle.width)
// Prints "5 4"
// maximam 값에 기반하여 결과가 도출

프로퍼티 래퍼에 인자를 포함하여 래퍼의 초기 상태를 설정하고 그것이 생성될 때 래퍼에 다른 옵션을 넣는 게 가능
해당 문법은 프로퍼티 래퍼를 사용하는 가장 일반적인 문법으로써 사용

프로퍼티 래퍼 인자를 포함할 때 할당 연산자를 사용하여 초기 값을 지정 가능
매개변수로 wrappedValue를 축약하여 일반적인 할당으로 표현 가능
Swift는 할당 연산을 wrappedValue 인자와 같이 다루며, 포함한 인자를 수용하는 initializer를 사용

struct MixedRectangle {
    // height를 포장하는 SmallNumber의 인스턴스는 최댓값이 12인 SmallNumber(wrappedValue: 1)을 호출하며, 
    // width를 감싸는 인스턴스는 SmallNumber(wrappedValue: 2, maximum: 9)를 호출
    @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"

2) 프로퍼티 래퍼로부터 값을 예상하기

프로퍼티 래퍼는 projected value를 정의함으로써 새로운 기능을 드러낼 수 있다
ex. 데이터베이스에 대한 접근을 관리하는 프로퍼티 래퍼는
해당 projected value에 flushDatabaseConnection() 메소드를 노출하는 게 가능

projected value의 이름은 달러 사인($)으로 시작한다는 점만 빼면 wrapped value와 같다
개발자가 정의하는 코드에서는 $로 시작하는 프로퍼티를 정의할 수 없기 때문에, 이 값은 다른 프로퍼티에 간섭하지 않는다

아래의 코드는 새로운 값을 프로퍼티에 저장하기 전에
프로퍼티 래퍼가 조정을 했는지 여부를 추적하기 위해
projectedValue 프로퍼티를 SmallNumber 구조체에 더한다

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

프로퍼티 래퍼는 어떤 타입의 projected value든 반환 가능
예제에서는 프로퍼티 래퍼가 정보 하나만을 노출하면 되기 때문에 Bool 타입을 projectedValue로 사용
더 많은 정보를 드러내야 할 필요가 있는 래퍼는 다른 데이터 타입의 인스턴스를 반환 가능

또는 자기 자신의 projected value로써 래퍼의 인스턴스를 self를 사용해 노출 가능
프로퍼티 게터 또는 인스턴스 메소드와 같이 타입의 일부인 코드로부터 projected value에 접근할 때는 self를 생략 가능
일반적인 프로퍼티 접근 방식과 동일

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

var someSizedRectangle = SizedRectangle()

someSizedRectangle.height = 10
someSizedRectangle.width = 10
print(someSizedRectangle.$height, someSizedRectangle.$width)
// Prints "false, false"

someSizedRectangle.resize(to: .large)
print(someSizedRectangle.$height, someSizedRectangle.$width)
// Prints "true, true"

프로퍼티 래퍼는 게터와 세터를 위한 쉽고 편리한 문법이기 때문에,
다른 프로퍼티와 동일한 방법으로 height와 width에 접근 가능

전역 변수와 지역 변수

계산 프로퍼티와 프로퍼티 옵저버의 기능은 전역 변수와 지역 변수 모두에 사용 가능
전역 변수는 함수, 메소드, 클로저, 타입 컨텍스트 밖에서 정의된 변수이고, 지역 변수는 그 안에서 정의된 변수

지난 챕터들에서 만났던 전역 / 지역 변수는 모두 저장된 변수(stored variable)였다
저장된 변수(stored variable)는 저장 프로퍼티와 마찬가지로 특정 타입의 값에 대한 저장 공간을 제공하고, 값을 설정하거나 검색할 수 있다

계산된 변수(computed variable)나 저장된 변수를 위한 옵저버(observer)도 전역 / 지역 변수로 정의할 수 있다
계산된 변수는 계산 프로퍼티와 같은 방식으로 값을 저장하기 보다는 계산한다

전역 상수, 변수는 지연된 저장 프로퍼티와 비슷하게 지연 계산하지만, 지연된 저장 프로퍼티와 다르게 lazy 키워드를 붙일 필요가 없다
지역 상수, 변수는 지연 계산되지 않는다

타입 프로퍼티 (Type Property)

인스턴스 프로퍼티는 특정한 타입의 인스턴스에 속한 프로퍼티를 말한다
해당 타입의 인스턴스를 생성할 때마다, 다른 인스턴스로부터 분리된 자신의 고유한 프로퍼티 집합을 가진다

타입의 인스턴스 하나가 아니라 타입 자체에 속한 프로퍼티를 정의 가능
타입에 해당되는 단 하나의 프로퍼티만 생성된다
이러한 프로퍼티를 타입 프로퍼티(type property)라 부른다

타입 프로퍼티는 모든 인스턴스가 사용하는 상수 프로퍼티처럼
특정 타입의 인스턴스에 공통되는 값을 정의할 때 유용
또는 그 타입의 모든 인스턴스에 공통되는 값을 저장하는 변수 프로퍼티를 정의할 때도 유용

저장 타입 프로퍼티는 변수 또는 상수가 가능
계산 프로퍼티는 항상 변수 프로퍼티로만 선언 가능

저장 인스턴스 프로퍼티와 달리, 저장 타입 프로퍼티에는 반드시 기본 값을 설정
타입 자신은 초기화 시간에 저장 타입 프로퍼티에 값을 할당할 수 있는 initializer를 가질 수 없기 때문

저장 타입 프로퍼티는 최초 접근시에 지연 초기화
여러 스레드가 동시에 접근해도 한 번만 초기화되는 것을 보장하며, lazy 키워드를 필요로 하지 않는다

1) 타입 프로퍼티 문법

C나 Objective-C에서는 정적 상수 / 변수를
전역적인 정적 변수인 타입과 연관지어 정의한다
그러나, Swift에서는 타입 프로퍼티는 타입 정의의 일부분으로써 내부 컬리 브레이스 안에 작성된다
각각의 타입 프로퍼티는 명시적으로 해당 타입이 지원하는 범위가 정해져 있다

static 키워드를 사용하여 타입 프로퍼티를 정의
클래스 타입에 대한 계산 타입 프로퍼티에서는
class 키워드를 사용하여 자식 클래스가 부모 클래스의 구현을 오버라이딩하여 재구성 가능하다

// 값 타입인 struct, enum의 타입 프로퍼티 정의 
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의 타입 프로퍼티 정의
// class 키워드를 사용하여 해당 class를 상속 받을 경우 자식 클래스가 오버라이딩하여 재구성 가능 
class SomeClass {
    static var storedTypeProperty = "Some value."
    static var computedTypeProperty: Int {
        return 27
    }
    class var overrideableComputedTypeProperty: Int {
        return 107
    }
}

위의 계산 타입 프로퍼티 예시는 읽기 전용 계산 타입프로퍼티지만,
읽기와 쓰기가 모두 가능한 계산 타입 프로퍼티 역시 계산 인스턴스 프로퍼티와 같은 문법을 사용하여 정의 가능

2) 타입 프로퍼티의 접근와 설정

타입 프로퍼티는 인스턴스(instance) 프로퍼티와 마찬가지로 점 문법을 사용하여 접근 및 설정이 가능
그러나, 타입 프로퍼티는 해당 타입의 인스턴스가 아닌 해당 타입에 대해여 접근 및 설정한다

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"

아래의 예시는 오디오 채널을 모델링하는 구조체의 일부로써
두 개의 저장 타입 프로퍼티를 사용한다
각각의 채널은 0에서 10 사이의 오디오 레벨을 갖는다

아래 그림은 이러한 오디오 채널 중 두 개를 결합하여
스테레오 오디오 레벨 미터를 모델링하는 방법을 도식화 한 것이다
채널의 오디오 수준이 0이면 해당 채널의 표시등이 하나도 켜지지 않는다
오디오 레벨이 10이면 해당 채널의 모든 조명이 켜진다
그림에서 왼쪽 채널의 전류 레벨은 9이고 오른쪽 채널의 전류 레벨은 7이다

// AudioChannel 구조체는 두 개의 저장 타입 프로퍼티를 정의
struct AudioChannel {
    // 상수 저장 타입 프로퍼티
    // 오디오 레벨이 가질 수 있는 최대 레벨 값을 정의
    // 모든 AudioChannel 인스턴스에 대해 10의 상수 값을 가짐
    static let thresholdLevel = 10

    // 변수 저장 타입 프로퍼티
    // 모든 AudioChannel 인스턴스에 의해 받는 최대 입력 값을 추적, 기본값 0
    static var maxInputLevelForAllChannels = 0

    // 변수 저장 인스턴스 프로퍼티 
    // 현재 오디오 레벨 값을 정의, 기본값 0
    // didSet 프로퍼티 옵저버를 통해서 값이 변할 때 마다 2가지 사항 체크
    var currentLevel: Int = 0 {
        didSet {
            if currentLevel > AudioChannel.thresholdLevel {
                // cap the new audio level to the threshold level
                // 만약 오디오 신호로 10보다 더 큰 값이 들어온다면, thresholdLevel 값으로 수정
                // didSet 관찰자는 currentLevel을 다른 값으로 설정하지만, 관찰자를 다시 호출하지는 않는다
                currentLevel = AudioChannel.thresholdLevel
            }
            if currentLevel > AudioChannel.maxInputLevelForAllChannels {
                // store this as the new overall maximum input level
                // 새로운 신호가 들어올 때 마다 비교하여 최대 값일 경우 maxInputLevelForAllChannels에 저장 
                AudioChannel.maxInputLevelForAllChannels = currentLevel
            }
        }
    }
}

// 스테레오를 위한 2채널 구성
var leftChannel = AudioChannel()
var rightChannel = AudioChannel()

// 만약 leftChannel의 currentLevel을 7로 설정한다면, 
// maxInputLevelForAllChannels 타입 프로퍼티는 7로 수정
leftChannel.currentLevel = 7
print(leftChannel.currentLevel)
// Prints "7"
print(AudioChannel.maxInputLevelForAllChannels)
// Prints "7"

// 만약 currentLevel에 11을 넣으려 한다면, 
// rightChannel의 currentLevel 프로퍼티는 최댓값인 10으로 정해지며
// maxInputLevelForAllChannels 프로퍼티 역시 10으로 수정
rightChannel.currentLevel = 11
print(rightChannel.currentLevel)
// Prints "10"
print(AudioChannel.maxInputLevelForAllChannels)
// Prints "10"
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant