본문 바로가기

문법

[Swift]프로퍼티와 메서드 알아보기 1부

주의 : 영문서 번역 중 오역이 있을 수 있으며 정확하지않는 정보가 있을 수 있습니다.

 

 

프로퍼티와 메서드 알아보기 1부

 

- 프로퍼티(Propertie)
- 저장 프로퍼티(Stored Propertie)
- 지연 저장 프로퍼티(lazy Stored Propertie)
- 연산 프로퍼티(Computed Propertie)
- 프로퍼티 감시자(Propertie Observerse)
- 전역변수(Global Variable)와 지역변수(Local Variable)

 

 

프로퍼티는 클래스, 구조체 또는 열거형 등에 관련된 값을 뜻한다. 메서드는 특징 타입에 관련된 함수를 뜻한다.

우리가 앞서 봤던 변수나, 상수 등이 어떤 목적으로쓰이냐, 어디에서 어떻게 쓰이냐에 따라 용어가 조금씩 달라진 뿐이다.

프로퍼티

Swift Language Guide

해석)

"프로퍼티는 특정한 클래스, 구조체, 이넘의 값을 연결한다. 저장된 프로퍼티는 한 인스턴스의 일부처럼 값을 변/상수에 저장하는 반면

계산된 프로퍼티는 값을 저장하는 대신 계산한다. 계산된 프로퍼티는 클래스, 구조체, 이넘에서 사용할 수 있지만 저장된 프로퍼티는 클래스와 구조체에서만 사용할 수 있다. " 라고 나와있다.

 

프로퍼티는 크게 3가지로 나눌 수 있다.

- 저장프로퍼티(Stored Propertie)

- 연산프로퍼티(Computed Propertie)

- 타입프로퍼티(개념만 익히고 예제는 다음 포스팅인 2부에서 다루겠다.)

 

저장(Stored) 프로퍼티는 인스턴스의 변수 또는 상수를 의미합니다.

연산(Computed) 프로퍼티는 값을 저장한 것이아니라 특정 연산을 실행한 결괏값을 의미한다.

연산프로퍼티는 클래스, 구조체, 열거형에 쓰일 수 있다.

저장프로퍼티는 구조체와 클래스에서만 사용할 수 있다.

저장프로퍼티와 연산프로퍼티는 특정 타입의 인스턴스에 사용되는 것을 뜻하지만

특정 타입에 사용되는 프로퍼티도 존재한다. 이를 타입(Type) 프로퍼티라고 한다.

 

정리해보자면 기존 프로그래밍 언어에서 사용되던 인스턴스 변수는 저장프로퍼티로, 클래스 변수는 타입프로퍼티로 구분할수있다.

 

더불어 프로퍼티 값이 변하는 것을 감시하는 프로퍼티 감시자(property Observers)도 있다.

프로퍼티 감시자는 프로퍼티의 값이 변할 때 값의 변화에 따른 특정 작업을 실행한다.

프로퍼티 감시자는 저장프로퍼티에 적용할 수 있으며 부모 클래스로 부터 상속받을 수 있다.

 

저장 프로퍼티

클래스 또는 구조체의 인스턴스와 연관된 값을 저장하는 가장 단순한 개념의 프로퍼티입니다.

저장 프로퍼티는 var키워드를 사용하면 변수 저장프로퍼티 let 키워드를 사용하면 상수 저장 프로퍼티가 됩니다.

저장 프로퍼티를 정의할때 프로퍼티 기본값*과 초깃값을 지정해 줄 수 있다.

 

NOTE

구조체와 클래스의 저장프로퍼티

구조체의 저장프로퍼티가 옵셔널이 아니더라도, 구조체저장 프로퍼티를 모두 포함하는 이니셜라이저를

자동으로 생성합니다. 하지만 클래스의 저장 프로퍼티옵셔널이 아니라면 프로퍼티 기본값을 지정해주거나

사용자 정의 이니셜라이저를 통해 반드시 초기화해주어야 합니다. 또 클래스 인스턴스의 상수 프로퍼티는

인스턴스가 초기와(이니셜라이즈)될 때 한번만 값을 할당할 수 있으며, 자식 클래스에서 이 초기화를

변경(재정의) 할 수 없습니다.

 

저장 프로퍼티의 선언 및 인스턴스 생성

// 좌표
struct CoordinatePoint {
    var x: Int //저장 프로퍼티
    var y: Int //저장 프로퍼티
}

//구조체에는 기본적으로 저장 프로퍼티를 매개변수로 갖는 이니셜라이저가 있다.
let quokkaPoint = CoordinatePoint(x: 100, y: 50)

// 사람의 위치 정보
class Position {
    var point: CoordinatePoint
    //저장 프로퍼티(변수) - 위치(point)는 변경할 수 있음을 뜻한다.
    let name: String // 저장 프로퍼티(상수)
    
    //프로퍼티 기본값을 지정해주지 않는다면 이니셜라이저를 따로 정의해주어야 한다.
    init(name: String, cureentPoint: CoordinatePoint) {
        self.name = name
        self.point = cureentPoint
    }
}

let quokkaPosition = Position(name: "quokka", cureentPoint: quokkaPoint)

사용자 정의 이니셜라이저를 호출해야만 합니다. 그렇지않으면 프로퍼티 초기값을 할당할 수 없기때문에 인스턴스 생성이불가하다.

 

구조체는 프로퍼티에 맞는 이니셜라이저를 자동으로 제공하지만 클래스는 그렇지 않아서 클래스 인스턴스의

저장 프로퍼티를 사용하는 일은 좀 번거롭다. 하지만 클래스의 저장 프로퍼티에 초깃값을 지정해주면 따로

사용자 정의 이니셜라이저를 구현해줄 필요가 없다.

 

// 저장 프로퍼티의 초깃값 지정
// 좌표

struct CoordinatePoint2 {
    var x: Int = 0 //저장 프로퍼티
    var y: Int = 0//저장 프로퍼티
}

// 프로퍼티의 초깃값을 할당햇다면 굳이 전달인자로 초깃값을 넘길 필요가 없습니다.
let quokkaPoint2 = CoordinatePoint2()

// 물론 기존에 초깃값을 할당할 수 있는 이니셜라이저도 사용 가능하다.
let wizplanPoint = CoordinatePoint2(x: 100, y: 50)

print("quokka point : \(quokkaPoint2.x), \(quokkaPoint2.y) ")
// quokka point : 0, 0
print("wizplan Point : \(wizplanPoint.x), \(wizplanPoint.y) ")
// wizplan Point : 100, 50

// 사람의 위치 정보
class Position2 {
    var point: CoordinatePoint2 = CoordinatePoint2()
    var name: String = "Unknown"
}

//초기값을 지정해줬다면 사용자 정의 이니셜라이저를 사용하지 않아도 된다.
let quokkaPosition2 = Position2()

quokkaPosition2.point = quokkaPoint2
quokkaPosition2.name = "quokka"

 

초깃값을 미리 지정했더니 인스턴스를 만드는 과정이 훨씬 간편해졌다.

그러나 의도와 맞지않게 인스턴스가 사용될 가능성이 남아있고, 인스턴스를 생성한 후에 원하는 값을 일일이

할당해야해서 불편했다. 또 Position2의 name 프로퍼티는 한 번 값을 할당해준 후에 변경하지 못하도록

상수로 정의해주고 싶었는데, 인스턴스를 생성한 후에 값을 할당해주어야 하기때문에 그렇게 할 수도 없었다.

 

인스턴스를 생성할 때 이니셜라이저를 통해 초기값을 보내야하는 이유는 프로퍼티가 옵셔널이 아닌 값으로

선언되어 있기 때문이다. 그러므로 인스턴스는 생성할 때 프로러티에 값이 꼭 있는 상태여야 합니다. 그런데

저장 프로퍼티의 값이 있어도 그만, 없어도 그만인 옵셔널이라면 굳이 초기값을 넣어주지 않아도 된다. 즉,

이니셜라이저에서 옵셔널 프로퍼티에 꼭 값을 할당해주지 않아도 된다.

 

옵셔널의 사용과 사용자 정의 이니셜라이저를 적절히 혼합하여 의도에 맞는 구조체와 클래스를 정의해보자

 

옵셔널을 사용한 사용자 정의

// 좌표

struct CoordinatePoint3 {
    // 위치는 x,y 값이 모두 있어야하므로 옵셔널이면 안된다
    var x,y: Int
}

// 사람의 위치 정보
class Position3 {
    //현재 사람의 위치를 모를 수 도 있다. - 옵셔널
    var point: CoordinatePoint3?
    let name: String
    
    init(name: String) {
        self.name = name
    }
}

//이름은 필수지만 위치는 모를 수 있다.
let quokkaPosition3 = Position3(name: "quokka")

//위치를 알면 그 때 위치값을 할당해준다.
quokkaPosition3.point = CoordinatePoint3(x: 20, y: 10)

print("quokkaPosition3: \(quokkaPosition3.point?.x) / \(quokkaPosition3.point?.y)")
// quokkaPosition3: Optional(20) / Optional(10)

이렇게 옵셔널과 이니셜라이저를 적절히 사용하면 다른 프로그래머가 사용할 때, 내가 처음의도 했던 대로 구조체와 클래스를 사용할 수 있도록 유도할 수 있다.

 

 

 

지연 저장 프로퍼티

인스턴스를 생성할 때 프로퍼티에 값이 필요없다면 프로퍼티를 옵셔널로 선언해줄 수 있다. 그런데 그것과는

조금 다른 용도로 필요할때 값이 할당되는 지연저장프로퍼티(lazy Stored Propertie)가 있다.

지연 저장 프로퍼티는 호출이 있어야 값을 초기화하며, 이때 lazy키워드를 사용한다.

 

상수는 인스턴스가 완전히 생성되지 전에 초기화해야 하므로 필요할 때 값을 할당하는 지연 저장 프로퍼티와는

맞지 않다. (이해가 잘안됨) 따라서 지연 저장프로퍼티는 var 키워드를 사용하여 변수로 정의한다.

 

지연 저장프로퍼티는 주로 복잡한 클래스나 구조체를 구현할때 많이 사용된다. 클래스 인스턴스의 저장 프로퍼티로

다른 클래스 인스턴스나 구조체 인스턴스를 할당해야할 때가 있다. 이럴때 인스턴스를 초기화하면서 저장 프로퍼티로

쓰이는 인스턴스들이 한번에 생성되어야한다면??? 또, 굳이 모든 저장프로퍼티를 사용할 필요가 없다면????

이 질문의 답이 지연 저장 프로퍼티 사용이다. 지연저장 프로퍼티를 잘 사용하면 불필요한 성능 저하나 공간 낭비를

줄일 수 있다.

 

지연 저장 프로퍼티를 선언해보자

지연 저장 프로퍼티 선언

struct CoordinatePointLazy {
    var x: Int = 0
    var y: Int = 0
}

class PositionLazy {
    lazy var point = CoordinatePointLazy()
    let name: String
    
    init(name: String) {
        self.name = name
    }
}

let quokkaPosition4 = PositionLazy(name: "quokka")

//이 코드를 통해 point 프로퍼티로 처음 접근할 때
// point 프로퍼티의 CoordinatePoint가 생성된다.
print(quokkaPosition4.point)
// CoordinatePointLazy(x: 0, y: 0)

// lazy가 아닐때의 결괏값
// CoordinatePointLazy(x: 0, y: 0)

 

NOTE

다중 스레드 환경에서 지연저장 프로퍼티에 동시 다발적으로 접근할 때는 한번만 초기화 된다는 보장이 없다.

생성되지않은 지연 저장 프로퍼티에 많은 스레드가 비슷한 시점에 접근하다면, 여러번 초기활 될 수 있다.

 

 

 

연산 프로퍼티

 

연산 프로퍼티는 실제 값을 지정하는 프로퍼티가 아니라, 특정 상태에 따른 값을 연산하는 프로퍼티입니다.

인스턴스 내/외부의 값을 연산하여 적절한 값을 돌려주는 접근자(getter)의 역할이나 은닉화된 내부의

프로퍼티 값을 간접적으로 설정하는 설정자(setter)의 역할을 할 수도 있습니다. 클래스, 구조체,

열거형에 연산 프로퍼티를 정의할 수 있습니다.

 

'굳이 메서드를 두고 왜 연산 프로퍼티를 쓸까?' 라는 의문이 들 수도 있으니 그 이유를 생각해보자

인스턴스 외부에서 메서드를 통해 인스턴스 내부 값에 접근하려면 메서드를 두개(접근자, 설정자) 구현해야한다.

또한 이를 감수하고 메서드로 구현한다 해도 두 메서드가 분산 구현되어 코드의 가독성이 나빠질 위험이 있다.

 

타인의 코드를 보는 프로그래머의 입장에서는 프로퍼티가 메서드 형식보다 훨씬 더 간편하고 직관적이기도 하다.

다만 연산 프로퍼티는 접근자인 get메서드만 구현해둔 것처럼 읽기 전용 상태로 구현하기 쉽지만,

쓰기 전용 상태로 구현할 수 없다는 단점이 있다.

메서드로는 설정자 메서드만 구현하여 쓰기 전용상태로 구현할수 있지만 연산프로퍼티는 그것이 불가능하다.

 

메서드로 구현된 접근자와 설정자

// 메서드로 구현된 접근자와 설정자

struct CoordinatePoint5 {
    var x, y: Int //저장프로퍼티
    
    // 대칭점을 구하는 메서드 - 접근자
    // Self는 타입 자기 자신을 뜻한다.
    // Self 대신 CoordinatePoint를 사용해도 된다.
    func oppositePoint() -> Self {
        return CoordinatePoint5(x: -x, y: -y)
    }
    
    //대칭점을 설정하는 메서드 - 설정자
    mutating func setOppositePoint(_ opposite: CoordinatePoint5) {
        x = -opposite.x
        y = -opposite.y
    }
}

var quokkaPosition5 = CoordinatePoint5(x: 10, y: 5)

// 현재 좌표
print(quokkaPosition5)
//CoordinatePoint5(x: 10, y: 5)

// 대칭 좌표
print(quokkaPosition5.oppositePoint())
//CoordinatePoint5(x: -10, y: -5)

// 대칭 좌표를 (15,10)으로 설정하면
quokkaPosition5.setOppositePoint(CoordinatePoint5(x: 25, y: 15))

// 현재 좌표는 -25, -15으로 설정된다.
print(quokkaPosition5) // -25, -15

**이런 식으로 연산 프로퍼티를 사용하면 하나의 프로퍼티에 접근자와 설정저가 모두 모여있고

해당 프로퍼티가 어떤 역할을 하는지 좀 더 명확하게 표현 가능하다. 인스턴스를 사용하는 입장에서도 마치

저장 프로퍼티인 것처럼 편하게 사용할 수 있습니다.**

 

설정자의 매개변수로 원하는 이름을 소괄호 안에 명시해주면 set메서드 내부에서 전달받은 전달인자를 사용할 수 있습니다.

관용적인 표현으로 newValue로 매개변수 이름을 대신할 수 있습니다. 그럴 경우에는 매개변수를 따로 표기하지 말아야 합니다.(= 메서드의 매개변수를 받는건 외부의 데이터를 받는 용도인데 내부 타입으로 설정이 되었기때문에 매개변수를 와일드카드를 사용하여 생략하는게 정석이라는 표현같음)

 

접근자내부의 코드가 단 한 줄이고 그 결괏값의 타입이 프로퍼티의 타입과 같다면 return 키워드를 생략해도

그 결괏값이 접근자의 반환값이 된다.(= 메서드 반환값코드가 한줄이면 return이 생략가능하다.)

 

// 매개변수 이름을 생략한 설정자
struct CoordinatePoint6 {
    var x, y: Int //저장 프로퍼티
    
    // 대칭 좌표
    var oppositePoint: CoordinatePoint6 { //연산프로퍼티
        //접근자
        get {
            //이곳에서 return 키워드를 생략 할 수 있다.
            return CoordinatePoint6(x: -x, y: -y)
        }
        
        //설정자
        set {
            x = -newValue.x
            y = -newValue.y
        }
    }
}

let quokkaPosition6 = CoordinatePoint6(x: 10, y: 5)
print(quokkaPosition6.oppositePoint)

 

굳이 대칭점을 설정해줄 필요가 없으면 읽기 전용으로 연산프로퍼티를 사용할 수도 있다.

연산 프로퍼티를 읽기 전용으로 구현하려면 get메서드만 사용하면 된다.

 

(get)읽기 전용인 연산프로퍼티

struct CoordinatePoint7 {
    var x, y: Int //저장 프로퍼티
    
    //대칭 좌표
    var oppositePoint: CoordinatePoint7 {
        //접근자
        get {
            CoordinatePoint7(x: -x, y: -y)
        }
    }
}

var quokkaPosition7 = CoordinatePoint7(x: 10, y: 20)

//현재좌표
print(quokkaPosition7)

//대칭좌표
print(quokkaPosition7.oppositePoint)

//설정자를 구현하지 않았으므로 오류!!
// 오류 - Cannot assign to property: 'oppositePoint' is a get-only property
//quokkaPosition7.oppositePoint = CoordinatePoint7(x: 15, y: 10)

//인스턴스 생성후 설정하는것은 가능,,?
quokkaPosition7.x = 15
quokkaPosition7.y = 10
print("\(quokkaPosition7.x) / \(quokkaPosition7.y)")

 

 

프로퍼티 감시자

 

프로퍼티 감시자(Propertie Observerse)를 사용하면 프로퍼티의 값이 변경됨에 따라 적절한 작업을 취할 수 있다.

 

프로퍼티 감시자는 프로퍼티의 값이 새로 할당될 때마다 호출한다. 이때 변경되는 값이 현재의 값과 같더라도 호출한다.

프로퍼티 감시자는 저장 프로퍼티 뿐만 아니라 프로퍼티를 재정의해 상속받은 저장 프로퍼티 또는 연산 프로퍼티에도

적용할 수 있다. 물론 상속받지 않은 연산프로퍼티에는 프로퍼티 감시자를 사용할 필요가 없으며 할 수도 없다.

연산프로퍼티의 접근자와 설정자를 통해 프로퍼티 감시자를 구현할 수 있기때문이다. 연산 프로퍼티는 상속받았을때만

프로퍼티 재정의를 통해 프로퍼티 감시자를 사용한다.

 

프로퍼티 감시자에는 프로퍼티의 값이 변경되기 직전에 호출하는 willSet 메서드와. 프로퍼티의 값이 변경된 직후에

호출하는 didSet 메서드가 있다. willSet메서드didSet메서드에는 매개변수가 하나씩 있다. wilSet메서드에

전달되는 전달인자는 프로퍼티가 변경될 값이고, didSet메서드에 전달되는 전달인자는 프로퍼티가 변경되지 전의 값이다.

그래서 매개변수의 이름을 따로 지정하지 않으면 willSet메서드에는 newValue가, didSet(oldValuename)처럼

willSet이나 didSet다음에 서괄호로 감싼 이름을 적어주면 됩니다.

 

NOTE

oldValue와 didSet

didSet 감시자 코드 블록 내부에서 oldValue값을 참조하지 않거나 매개변수 목록에 명시적으로 매개변수를 적어

(예: didSet(oldValueName))주지 않으면 didSet코드 블록이 실행되지 않습니다.

 

프로퍼티 감시자

// 프로퍼티 감시자
class Account {
    var credit: Int = 0 {
        willSet(newValue) {
            print("잔액이 \(credit)원에서 \(newValue)으로 변경될 예정입니다.")
        }
        
        didSet(oldValue) {
            print("잔액이 \(oldValue)원에서 \(credit)원으로 변경되었습니다.")
        }
    }
}

let myAccount = Account()
// Account 인스턴스에 접근
print(myAccount)// PropertieAndMethod.Account2

// Account 인스턴스내 credit프로퍼티에 접근
print(myAccount.credit) // 0

// Account 인스턴스내 credit프로퍼티에 접근하여 값을 변경
//잔액이 0원에서 3000으로 변경될 예정입니다.
print(myAccount.credit = 3000)
//잔액이 0원에서 3000원으로 변경되었습니다.

// => 정말로 credit프로퍼티에 접근하는게아니라 값을 변경, 즉, 설정자에 접근할때 willSet didSet이 감지되는것
// 클래스의 프로퍼티 값이 메모리에서 해제될때 나오는 deinit메서드와 유사한면이 있다.

// willSet과 didSet의 전달인자명을 변경해서 사용할 수 있다.

 

willSet과 didSet의 전달인자명 변경하여 사용

// willSet과 didSet의 전달인자명을 변경해서 사용할 수 있다.
class Account2 {
    var credit: Int = 0 {
        willSet(expectedValueChange) {
            print("잔액이 \(credit)원에서 \(expectedValueChange)으로 변경될 예정입니다.")
        }
        
        didSet(beforeValueChange) {
            print("잔액이 \(beforeValueChange)원에서 \(credit)원으로 변경되었습니다.")
        }
    }
}

let myAccount2 = Account2()

// myAccount2 인스턴스에 접근
print(myAccount2)// PropertieAndMethod.Account2

// Account2 인스턴스내 credit프로퍼티에 접근
print(myAccount2.credit) // 0

// Account2 인스턴스내 credit프로퍼티에 접근하여 값을 변경
//잔액이 0원에서 3000으로 변경될 예정입니다.
print(myAccount2.credit = 3000)
//잔액이 0원에서 3000원으로 변경되었습니다.

 

클래스를 상속받았다면 기존의 연산프로퍼티를 재정의하여 프로퍼티 감시자를 구현할 수도 있다.

연산 프로퍼티를 재정의해도 기존의 연산 프로퍼티 기능(접근자와 설정자, get과 set메서드)은 동작한다.

 

연산프로퍼티인 dollarValue가 포함되어있는 Account클래스를 상속받은 ForeignAccount 클래스에서

기존 dollarValue 프로퍼티를 재정의하여 프로퍼티 감시자를 구현하는 예제이다.

 

주석(실제로는 콘솔에 출력되는 문자열)의 흐름을 통해 언제 어떤 메서드가 호출되는지 확인할 수 있다.

 

상속받은 연산 프로퍼티의 프로퍼티. 감시자 구현

print("====상속받은 감시자프로퍼티====")
// 상속받은 연산 프로퍼티의 프로퍼티. 감시자 구현
class Account3 {
    var credit: Int = 0 { // 저장 프로퍼티
        willSet(expectedValueChange) {
            print("잔액이 \(credit)원에서 \(expectedValueChange)으로 변경될 예정입니다. 415")
        }
        
        didSet(beforeValueChange) {
            print("잔액이 \(beforeValueChange)원에서 \(credit)원으로 변경되었습니다. 419")
        }
    }
    
    var dollarValue: Double { // 연산 프로퍼티
        get {
            Double(credit)
        }
        
        set {
            credit = Int(newValue * 1000)
            print("잔액을 \(newValue)달러로 변경 중입니다. 430")
        }
    }
}

class ForeignAccount: Account3 {
    override var dollarValue: Double {
        willSet {
            print("잔액이 \(dollarValue)달러에서 \(newValue)달러로 변경될 예정입니다. 438")
        }
        didSet {
            print("잔액이 \(oldValue)달러에서 \(dollarValue)달러로 변경하였습니다. 441")
        }
    }
}

let myAccountParentTest = Account3()
// 부모클래스의 getter를 접근했을때
print(myAccountParentTest.dollarValue)// 0.0

// 부모클래스의 set을 시도했을때
myAccountParentTest.dollarValue = 3 //잔액이 0원에서 3000으로 변경될 예정입니다. 415
//잔액이 0원에서 3000원으로 변경되었습니다. 419
//잔액을 3.0달러로 변경 중입니다. 430

let myAccount3 = ForeignAccount()
myAccount3.credit = 1000 // 잔액이 0원에서 1000원으로 변경될 예정입니다.
//잔액이 0원에서 1000원으로 변경되었습니다.
print(myAccount3.credit)


//자식 willSet-> 부모 willSet -> 부모 didSet -> 부모 set -> 자식 didSet
myAccount3.dollarValue = 2  
//잔액이 0.0달러에서 2.0달러로 변경될 예정입니다. 438
//잔액이 0원에서 2000으로 변경될 예정입니다. 415
//잔액이 0원에서 2000원으로 변경되었습니다. 419
//잔액을 2.0달러로 변경 중입니다. 430
//잔액이 0.0달러에서 2000.0달러로 변경하였습니다. 441

print(myAccount3.dollarValue) // 2000.0

위예제에서 상속 받은 클래스의 인스턴스 값을 변경할때의 순서)

자식 willSet-> 부모 willSet -> 부모 didSet -> 부모 set -> 자식 didSet

중간 포인트 정리

- 프로퍼티 감시자는 프로퍼티의 값이 새로 할당될 떄마다 호출한다.

- 프로퍼티 감시자(Observerse Propertie)에는 willSet 과 didSet 메서드가 존재한다.

- willSet메서드에 전달되는 전달인자는 프로퍼티가 변경될 값이고 didSet의 메서드에 전달되는 전달인자는 프로퍼티가 변경되기 전의 값이다.

- willSet, didSet, set메서드는 말그대로 모두 프로퍼티의 값이 변경되었을때만 실행한다.

- getter메서드는 값을 호출할때 실행한다.

 

 

전역변수와 지역변수

 

앞서 설명한 연산 프로퍼티와 프로퍼티 감시자는 전역변수와 지역변수 모두에 사용할 수 있다. 따라서 프로퍼티에

한정하지 않고, 전역에서 쓰일 수 있는 변수와 상수에도 두 기능을 사용할 수 있다. 함수나 메서드, 클로저,

클래스, 구조체, 열거형 등의 범위 안에 포함되지 않았던 변수나 상수, 즉 우리가 프로퍼티를 다루기 전에 계속해서

사용했던 변수와 상수는 모두 전역변수 또는 전역상수에 해당한다.

 

우리가 이제 까지 변수라고 통칭했떤 전역변수 또는 지역변수는 저장변수라고 할 수 있다.

저장변수는 마치 저장프로퍼티처럼 값을 저장하는 역할을 한다. 그런데 전역변수나 지역변수를 연산변수로 구현할 수도

있으며, 프로퍼티 감시자를 구현할 수도 있따.

 

참, 전역변수 또는 전역상수는 지연 저장 프로퍼티처럼 처음 접근할 때 최초로 연산이 이루어진다.

lazy 키워드를 사용하여 연산을 늦출 필요가 없다. 반대로 지역변수 및 지역상수는 절대로 지연 연산되지 않는다.

 

저장변수에 감시자를 구현한 것과 연산 변수를 구현한 것을 아래에서 볼 수 있다.

 

전역 저장변수의 감시자와 전역연산변수

print("===저장변수의 감시자와 연산변수===")
// 저장변수의 감시자와 연산변수
var wonInPocket: Int = 2000 {
    willSet{
        print("주머니의 돈이 \(wonInPocket)원에서 \(newValue)원으로 변경될 예정입니다.")
    }
    
    didSet {
        print("주머니의 돈이 \(oldValue)원에서 \(wonInPocket)원으로 변경되었습니다.")
    }
}

var dollarInPoket: Double {
    get {
        Double(wonInPocket)
    }
    
    set {
        wonInPocket = Int(newValue * 1000.0)
        print("주머니의 달러를 \(newValue)달러로 변경 중입니다.")
    }
}


dollarInPoket = 3.5 // 주머니의 돈이 2000원에서 3500원으로 변경될 예정입니다.
// 주머니의 돈이 2000원에서 3500원으로 변경되었습니다.
// 주머니의 달러를 3.5달러로 변경 중입니다.

 

 

포인트 정리

- 프로퍼티는 클래스, 구조체 또는 열거형 등에 관련된 값을 뜻하고, 메서드는 특징 타입에 관련된 함수를 뜻한다.

- 프로퍼티 종류에는 크게 3가지가 있다.(저장, 연산, 타입) 프로퍼티

- 연산프로퍼티는 클래스,구조체,열거형에 사용되지만. 저장프로퍼티는 구조체와 클래스에서만 사용할 수 있다.

- 클래스의 저장 프로퍼티는 옵셔널이 아니라면 프로퍼티 기본값을 지정해주거나 사용자 정의 이니셜라이저를 통해 반드시 초기화해주어야 한다. 만약 둘 중 하나라도 이행되지않으면 프로퍼티 초기값을 할당할 수 없기때문에 인스턴스 생성이불가하다.

 

- 인스턴스를 생성할 때 이니셜라이저를 통해 초기값을 보내야하는 이유는 프로퍼티가 옵셔널이 아닌 값으로 선언되어 있기 때문이다.

- 옵셔널과 이니셜라이저를 적절히 사용하면 의도에 맞게 구조체와 클래스를 사용할 수 있다.

- 지연저장 프로퍼티는 호출이 있어야 값을 초기화하기때문에 잘 사용하면 불필요한 성능 저하나 공간 낭비를 줄일 수 있다.

- 다중 스레드 환경에서 지연저장 프로퍼티에 동시 다발적으로 접근할 때는 한번만 초기화 된다는 보장이 없다.

- 연산 프로퍼티는 실제 값을 지정하는 프로퍼티가 아니라, 특정 상태에 따른 값을 연산한다.

- 저장프로퍼티와 저장프로퍼티를 접근하는 메서드 혹은 연산프로퍼티와 저장프로퍼티를 설정하는 메서드 혹은 연산프로퍼티가 존재하면

더 역할분배가 잘 이루어지는 코드를 작성할 수 있다.

 

- 설정자의 매개변수로 원하는 이름을 소괄호 안에 명시해주면 set메서드 내부에서 전달받은 전달인자를 사용할 수 있다.

- 프로퍼티 감시자는 프로퍼티의 값이 변할 때 값의 변화에 따른 특정 작업을 실행한다.

- 프로퍼티의 값이 변경되기 직전에 호출하는 willSet 메서드의 전달인자는 프로퍼티가 변경될 값이고

  프로퍼티의 값이 변경된 직후에 호출하는 didSet 메서드의 전달인자는 프로퍼티가 변경기지 전의 값이다.

 

- didSet 감시자 코드 블록 내부에서 oldValue값을 참조하지 않거나 매개변수 목록에 명시적으로 매개변수를 적어주지 않으면 didSet코드 블록이 실행되지 않는다.

 

- 클래스를 상속받은 상태에서 연산 프로퍼티를 재정의해도 기존의 연산 프로퍼티 기능(접근자와 설정자, get과 set메서드)은 동작한다.

  위와 같은 상황일때 자식 연산프로퍼티 값을 변경할 경우 호출순서는

  자식 willSet-> 부모 willSet -> 부모 didSet -> 자식 didSet 이렇게 된다.

 

- willSet, didSet, set메서드는 말그대로 모두 프로퍼티의 값이 변경되었을때만 실행한다.

- getter메서드는 값을 호출할때 실행한다.

- 전역변수 또는 전역상수는 지연 저장 프로퍼티처럼 처음 접근할 때 최초로 연산이 이루어진다.

  반대로 지역변수 및 지역상수는 절대로 지연 연산되지 않는다.

 

궁금증

- PositionLazy타입내 에서 지연 저장 프로퍼티인  point 에 초기자를 반드시 설정해야한다고 나온다. 그것도 init()으로 지정하면 에러발생하고 프로퍼티생성 시에 바로 인스턴스화를 시켜놓어야 에러가 사라진다. 왜그런걸까 ?

에러발생
ok - 지연저장 프로퍼티를 인스턴스화 시켜줌

- 메모리에 인스턴스가 저장되는 시점이 인스턴스화하는 시점일까 ?

- (이해가 잘안됨)  상수는 인스턴스가 완전히 생성되기 전에 초기화해야 하므로 필요할 때 값을 할당하는 지연 저장 프로퍼티와는

맞지 않다. 

- 열거형에서 저장프로퍼티를 사용할 수 없는 이유가 무엇일까 ?

 

 

Reference

- 야곰책 3판 Swift 프로그래밍

- https://docs.swift.org/swift-book/LanguageGuide/Properties.html