property

봄이아빠·2024년 9월 14일
0

Swift Language

목록 보기
5/5
post-thumbnail
post-custom-banner

Apple 공식문서

프로퍼티, property

struct, class 그리고 enum에서 사용되는 기능으로 내가 그저 변수라고만 생각하고 struct, class에서 선언했었던 변수, 상수들이 모두 저장 프로퍼티(stored property)였다. 프로퍼티의 종류는 3가지로 stored property, computed property, type property가 있다.
저장 프로퍼티에는 lazy수정자를 붙여 초기화 시점을 조절할 수 있다. 지연 저장 프로퍼티의 초기화 시점은 해당 프로퍼티가 처음 사용될 때로 그전까진 초기화되지 않기 때문에 복잡하거나 계산비용이 큰 프로퍼티에 대한 초기화에 대해서 유용하다.

class GetFile {
    var name = "file.txt" // stored property
    
    init() {print("initialize text")} // 초기화 됐을 때 문장을 출력하여 체크
}

class ManageFile {
    lazy var importer: GetFile = .init() // lazy stored property
    var textFile = [String]() //
    
}

var myDiaryManager = ManageFile.init()// ManageFile을 초기화 함 그러나 GetFile의 초기화 구문이 실행되지 않았음

myDiaryManager.textFile.append("Today is happy") //myDiaryManager의 textFile 프로퍼티에 대한 값을 수정했으나 importer의 값은 초기화 되지 않음
myDiaryManager.importer.name = "diary.txt" // importer.name이 사용되는 순간 초기화가 되면서 초기화 구문이 실행 됨
/*
 initialize text
 */

애플의 예시처럼 초기화에 큰 비용이 들지만 반드시 사용하는 것이 아닌 프로퍼티에 대해서 lazy는 좋은 수정자로서 역할을 수행하지만 초기화 되지 않은 지연 저장 프로퍼티에 여러 스레드가 동시 접근할 경우 초기화가 단 한번만 실행된다는 보장이 없어 이로 인한 성능 저하가 일어날 수 있다.

연산 프로퍼티는 프로퍼티 내부에 getter와 옵셔널 setter를 가지는 프로퍼티이다. 직접적으로 값을 저장하는 것이 아닌 저장 프로퍼티로부터 값을 받아와 연산해주는 역할을 한다. 때문에 항상 var로 선언되어야 하고 getter가 return을 해줘야 하기 때문에 항상 타입을 명시해줘야 한다.

/*
 1. 숫자를 문자열로 입력 받음
 2. 입력받은 문자열을 숫자로 바꿈
 3. e나 π등을 받으면 해당 상수로 변경
 */
struct CalculatorCore {
    var displayString: String
    var swapStringToDouble: Double {
        get {
            if let displayNum = Double(displayString) { //String을 무조건 Double로 변환할 수 있는 것이 아니기에 옵셔널 더블 타입을 가짐
                return displayNum //문자열이 숫자로만 되어있다면 그대로 더블 타입으로 반환
            }else {
                switch displayString {
                case "e": return 2.718
                case "π": return 3.14159 //특정한 문자라면 해당 문자에 맞는 숫자 반환
                default: return 0 //이 외의 문자에는 0을 반환
                }
            }
        }
        
        set {
            displayString = String(newValue) //newValue라는 기본 이름을 사용하여 이름 작성을 생략했음
        }
    }
}

var forTest = CalculatorCore.init(displayString: "π") // π입력

let typeTest1 = type(of: forTest.displayString)
let typeTest2 = type(of: forTest.swapStringToDouble)

print("입력 받은 타입은:", typeTest1,"/ 변환 된 타입은:", typeTest2)
print("돌려받은 값은:", forTest.swapStringToDouble)

forTest.swapStringToDouble = 39494

print("setter가 호출되어 변경된 값은:", forTest.displayString)

/*
입력 받은 타입은: String / 변환 된 타입은: Double
돌려받은 값은: 3.14159
setter가 호출되어 변경된 값은: 39494.0
*/

getter와 setter를 표현하는 방식은 바뀔 수 있다. 짧은 getter 선언 시 암시적인 반환을 가진 함수처럼 return을 생략해줄 수 있고, setter은 set(새로운 값을 담을 변수)와 같이 선언을 시작해야 한다. 이 부분을 생략한다면 newValue라는 기본 이름을 통해 입력 받은 값을 사용할 수 있다. 또한 setter는 생략이 가능하여 get만 존재하는 읽기 전용 연산 프로퍼티를 선언할 수 있다. 연산프로퍼티는 내가 느끼기엔 변수를 위한 작은 메서드 같은 느낌이었다.

저장 프로퍼티와 연산 프로퍼티의 몇몇 경우에 프로퍼티 관찰자(Property Observers)를 추가할 수 있다. 정확한 경우는 다음과 같다

  • 저장된 프로퍼티
  • 상속한 저장 프로퍼티
  • 상속한 연산 프로퍼티

상속하지 않은 연산프로퍼티에서는 위의 목록에 없는 것처럼 옵저버를 설정할 수 없다. setter가 옵저버의 역할을 수행할 수 있기 때문에 중복으로 옵저버를 둘 필요가 없는 것이다. 당연하게도 값의 수정이 불가능한 get-only 연산 프로퍼티는 옵저버를 쓰려고 할 경우 에러가 난다. 연산 프로퍼티에 옵저버를 추가하고 싶다면 프로퍼티를 상속한 후 override를 통해 옵저버를 넣어줄 수 있다.
상속은 struct에서는 불가능하고 class에서 가능하다.

struct MoneyCounter {
    var cash: Int = 50000 {
        willSet {
            print("기존 잔고는 \(cash)입니다. 변경 될 금액은 \(newValue)입니다.")
        }
        
        didSet {
            print("기존 잔고와의 차액은 \(cash - oldValue)입니다.") //willSet과 didSet에도 기본 이름인 newValue와 oldValue를 쓸 수 있다. 이 외의 경우엔 직접 (변수이름)을 지정해주면 된다.
            if cash < 0 {
                print("잔액이 마이너스가 되었습니다.")
            }
        }
    }
}

var myMoney = MoneyCounter(cash: 4000)

myMoney.cash = 2000 /* 기존 잔고는 4000입니다. 변경 될 금액은 2000입니다. willSet 부분
                     기존 잔고와의 차액은 -2000입니다. didSet 부분*/

myMoney.cash = 1000 /* 기존 잔고는 2000입니다. 변경 될 금액은 1000입니다.
                     기존 잔고와의 차액은 -1000입니다.*/

myMoney.cash = -100 /* 기존 잔고는 1000입니다. 변경 될 금액은 -100입니다.
                     기존 잔고와의 차액은 -1100입니다.
                     잔액이 마이너스가 되었습니다.*/

class Point {
    var point: Int
    
    var getPoint: Int {
        get {
            point = point + 1
            return point
        }
        set {
            point = newValue + point
        }
    }
    
    init(point: Int) {
        self.point = point
    }
}

class PointObservers: Point {
    override var point: Int {
        willSet{
            print("포인트가 변경됩니다. 현재 포인트: \(newValue)")
        }
    }
    
    override var getPoint: Int {
        willSet {
            print("추가된 포인트는 \(newValue).")
        }
    }
}

var myPoint = PointObservers(point: 10)
myPoint.point = 4
myPoint.point = myPoint.getPoint //Point 클래스의 getPoint에서 point를 set 하고 있기 때문에 이 코드에서 설정되므로 한 번, 연산프로퍼티 내부에서 설정되므로 한 번
//총 두 번 willSet이 호출된다.
/*
 포인트가 변경됩니다. 현재 포인트: 5
 포인트가 변경됩니다. 현재 포인트: 5
 */
myPoint.getPoint = 40 // getPoint의 set이 호출되면서 getPoint의 옵저버가 작동하고 이어서 getPoint의 set 부분에서 point가 수정되므로 point의 옵저버도 작동한다.
/* 추가된 포인트는 40.
 포인트가 변경됩니다. 현재 포인트: 45 */
//만약 위의 두 줄의 출력 순서를 바꾸고 싶다면 willSet과 didSet이 호출되는 순서를 생각하여 수정하면 된다.

프로퍼티 래퍼는 애플 문서에서 프로퍼티가 저장되는 방법을 관리하는 코드와 프로퍼티를 정의하는 코드 사이에 분리계층을 추가하는 것이라고 소개된다. 이로 인해 안정성 검사 등을 프로퍼티에서 수행할 때 반복적으로 사용되는 코드를 프로퍼티 래퍼로 한 번만 작성한 후 여러 프로퍼티에 적용하여 코드를 줄일 수 있다.

프로퍼티 래퍼를 만들기 위해 wrappedValue를 정의한 클래스, 구조체, 열거형 등을 만든다. 그리고 @propertyWrapper를 붙여 프로퍼티 래퍼를 정의하고 있음을 알린다.

프로퍼티 래퍼 내부의 변수는 연산 프로퍼티를 통해서만 수정될 수 있도록 private을 붙여 외부에선 접근할 수 없도록 한다.

프로퍼티 래퍼로 감싼 변수에 초기값을 넣을 경우 Swiftsms init(wrappedValue:)를 찾는다. 따라서 해당 구문이 있어야 초기화가 가능하다.

@propertyWrapper
struct VipPoint {
    private var vipGrade: Int //vip 등급에 대한 초기값을 지정하지 않음
    private var maxPoint: Int = 10
    private var point: Int = 0
    
    var userVipGrade: Int {
        get { vipGrade }
        set { vipGrade = newValue}
    }
    
    var wrappedValue: Int {
        get { point }
        set { point = min(newValue, maxPoint) }
    }
    //이 밑으로 나오는 초기화 구문을 작성해주어야 이후 다른 변수에서 초기화를 할 수 있다.
    //
    init(){
        vipGrade = 0
        maxPoint = 10
        point = 0
    }
    
    init(wrappedValue: Int) {
        self.point = wrappedValue
        vipGrade = 0
    }
    
    init(vipGrade: Int) {
        self.vipGrade = vipGrade
        if vipGrade < 3 {
            self.maxPoint = 10
        } else if vipGrade < 5 {
            self.maxPoint = 20
        } else {
            self.maxPoint = 30
        }
    }
    
    init(wrappedValue: Int, vipGrade: Int) {
        self.point = wrappedValue
        self.vipGrade = vipGrade
        if vipGrade < 3 {
            self.maxPoint = 10
        } else if vipGrade < 5 {
            self.maxPoint = 20
        } else {
            self.maxPoint = 30
        }
    }
}

struct UserPoint1 {
    @VipPoint(vipGrade: 1) var userPoint: Int = 6 // 바로 = 값을 입력시 입력 된 값은  init(wrappedValue:) 로 전달 됨 따라서 초기화 구문에 wrappedValue에 대한 argument가 있으면 안 됨
    ///따라서 포인트가 6으로 설정 됨
}

var user1 = UserPoint1()

print(user1.userPoint) // 구조체에서 초기화 됐던 6이 출력 됨

user1.userPoint = 19 // vip 등급에 대한 초기화를 따로 해주지 않았으므로 기본 등급인 0에 따라 최대 포인트는 10이 됨

print(user1.userPoint) // 최대 포인트인 10이 출력

struct UserPoint2 {
    @VipPoint(wrappedValue: 5, vipGrade: 4) var userPoint: Int
} //vip

var user2 = UserPoint2()
print(user2.userPoint) //초기 포인트가 5로 설정되어있으므로 5 출력

user2.userPoint = 19

print(user2.userPoint) // vip등급에 따라 최대 포인트가 늘어났으므로 19 출력

예시를 적어보면서 느낀 거지만 프로퍼티 래퍼에 대한 초기값을 지정해준 뒤에 다시 해당 프로퍼티 래퍼 내부의 변수에 대한 접근이 어렵다는 걸 느꼈다. 즉 vip 등급에 변화가 있을 상황인 내 예시에서는 프로퍼티 래퍼를 사용하는 것이 맞는 예시가 아니라고 느껴졌다. vip 등급에 따른 포인트 제한은 연산 프로퍼티로 구현하면 충분할 듯 하다.

프로퍼티 래퍼는 투영된 값(projectedValue)을 정의하여 추가적인 기능을 가질 수 있다. 투영된 값의 이름은 앞에 달러 표시, $를 붙여 사용하고 이 외에는 동일하며 $로 시작하는 프로퍼티는 정의할 수 없기 때문에 절대 정의된 프로퍼티를 방해할 수 없다. 아래 예제 아래 부분에서는 vip등급이 올랐는지에 대한 여부를 Bool타입으로 반환하며 이 외에도 어떤 타입이든 반환 가능하며 래퍼의 인스턴스 노출을 위해 self를 반환할 수도 있다.

@propertyWrapper
struct VipPoint {
    private var vipGrade: Int //vip 등급에 대한 초기값을 지정하지 않음
    private var maxPoint: Int = 10
    private var point: Int = 0
    
    var userVipGrade: Int {
        get { vipGrade }
        set { vipGrade = newValue}
    }
    
    var wrappedValue: Int {
        get { point }
        set { point = min(newValue, maxPoint) }
    }
    //이 밑으로 나오는 초기화 구문을 작성해주어야 이후 다른 변수에서 초기화를 할 수 있다.
    //
    init(){
        vipGrade = 0
        maxPoint = 10
        point = 0
    }
    
    init(wrappedValue: Int) {
        self.point = wrappedValue
        vipGrade = 0
    }
    
    init(vipGrade: Int) {
        self.vipGrade = vipGrade
        if vipGrade < 3 {
            self.maxPoint = 10
        } else if vipGrade < 5 {
            self.maxPoint = 20
        } else {
            self.maxPoint = 30
        }
    }
    
    init(wrappedValue: Int, vipGrade: Int) {
        self.point = wrappedValue
        self.vipGrade = vipGrade
        if vipGrade < 3 {
            self.maxPoint = 10
        } else if vipGrade < 5 {
            self.maxPoint = 20
        } else {
            self.maxPoint = 30
        }
    }
}

struct UserPoint1 {
    @VipPoint(vipGrade: 1) var userPoint: Int = 6 // 바로 = 값을 입력시 입력 된 값은  init(wrappedValue:) 로 전달 됨 따라서 초기화 구문에 wrappedValue에 대한 argument가 있으면 안 됨
    ///따라서 포인트가 6으로 설정 됨
}

var user1 = UserPoint1()

print(user1.userPoint) // 구조체에서 초기화 됐던 6이 출력 됨

user1.userPoint = 19 // vip 등급에 대한 초기화를 따로 해주지 않았으므로 기본 등급인 0에 따라 최대 포인트는 10이 됨

print(user1.userPoint) // 최대 포인트인 10이 출력

struct UserPoint2 {
    @VipPoint(wrappedValue: 5, vipGrade: 4) var userPoint: Int
} //vip

var user2 = UserPoint2()
print(user2.userPoint) //초기 포인트가 5로 설정되어있으므로 5 출력

user2.userPoint = 19

print(user2.userPoint) // vip등급에 따라 최대 포인트가 늘어났으므로 19 출력


@propertyWrapper
struct Vip {
    private var grade: Int
    private(set) var projectedValue: Bool = false //투영된 값에 대한 정의
    
    var wrappedValue: Int {
        get { grade }
        set {
            if grade < newValue {
                grade = newValue
                projectedValue = true
            } else {
                grade = newValue
                projectedValue = false
            }
        }
    }
    
    init(grade: Int, projectedValue: Bool) {
        self.grade = grade
        self.projectedValue = projectedValue
    }
    
    init(wrappedValue: Int) {
        self.grade = wrappedValue
    }
}

struct User {
    @Vip var userVipGrade: Int = 0 //grade가 0으로 초기화 됨
}

var user: User = User()

user.userVipGrade = 5 // grade를 5로 설정함
print(user.$userVipGrade) // upgrade 되었는지에 대한 판단으로 true가 출력 됨

user.userVipGrade = 3
print(user.$userVipGrade) // upgrade가 되지 않았으므로 false 출력 됨


enum ChangeGrade {
    case upgrade, downgrade
}

struct VipInfo {
    @Vip var user1: Int = 0
    
    mutating func promotion(to grade: ChangeGrade) {
        switch grade {
        case .upgrade:
            user1 = user1 + 1
        case .downgrade:
            user1 = user1 - 1
        }
    }
}

var manageUserVip = VipInfo(user1: 5)
print("초기화 직후 user1의 vip: \(manageUserVip.user1), 등급이 올랐는지 여부: \(manageUserVip.$user1)")
manageUserVip.promotion(to: .upgrade)
print("업그레이드 후 user1의 vip: \(manageUserVip.user1), 등급이 올랐는지 여부: \(manageUserVip.$user1)")

프로퍼티를 관찰하고 계산하기 위한 기능들은 대부분 지역 변수와 전역 변수에도 사용이 가능하다. 그러나 프로퍼티 래퍼는 전역 변수나 전역 연산 변수에 사용할 수 없다.

@Vip var myUser1 //Property wrappers are not yet supported in top-level code 오류 발생

마지막으로 타입 프로퍼티(Type Property)에 대해 알아보겠다. 사실 프로퍼티에 대해 공부하기 위한 첫 출발점이 강의 중 타입 프로퍼티라는 말이 이해가 안 된 것이었다. 앞서 언급한 프로퍼티들이 특정 타입의 인스턴스에 속하는 프로퍼티였다면 타입 프로퍼티는 특정 타입 자체에 속하는 프로퍼티이다. 새로운 인스턴스를 생성하는 것 -> 새로운 프로퍼티를 생성하는 것이었지만 타입 프로퍼티는 처음 호출된 이후 생성되고 이후 해당 프로퍼티를 모든 타입이 공유한다.
처음 호출 된 이후 생성된다는 것은 즉 lazy와 같은 지연이 이루어진다는 것이다. 그러나 lazy stored property와 달리 let으로도 정의할 수 있다. 지연 저장된 프로퍼티는 초기화 과정에서 값이 없음으로 초기화 된 후 다시 호출 될 때 값을 초기화하기 때문에 값에 변화가 생기는 것이기 때문에 let이 불가능하지만 타입 프로퍼티는 초기화 과정에 연관되지 않기 때문에 let으로도 선언 가능하다. 그러나 초기화 과정에 연관되지 않는다는 것은 다시 말해 초기화를 시켜줄 수 없다는 뜻이고 따라서 반드시 초기값을 설정해주어야 한다.
마치 전역변수같은 느낌이 드는 타입프로퍼티는 싱글톤 패턴을 만드는 데 아주 유용하다고 한다.
타입프로퍼티에 접근하는 방법은 프로퍼티의 이름이 아닌 타입의 이름에다 점문법으로 접근하는 것이다.

struct Score {
    static var maxScore: Int = 999
    static var bestRecord: Int = 0 //static으로 타입 프로퍼티를 선언함, 반드시 초기값을 가지고 있어야 함
    static var bestUser: String = "404"
    
    var userId: String = ""
    
    var userScore: Int = 0 {
        didSet{
            if userScore > Score.maxScore {
                print("\(userId) hit the highest Score!!")
                userScore = Score.maxScore // didSet이 다시 호출되진 않음
            }
            if oldValue < userScore {
                print("best of your record \(userId)!!")
            }
            if userScore > Score.bestRecord {
                print("best score of all users!!")
                Score.bestRecord = userScore
                Score.bestUser = userId
            }
            
        }
    }
    
    init(userId: String) {
        self.userId = userId
    }
}

var user1 = Score(userId: "foo")
var user2 = Score(userId: "bar")

user1.userScore = 99 /* best of your record foo!!
                        best score of all users!!*/
print("유저 중 최고 기록은 \(Score.bestUser)\(Score.bestRecord)점!") //유저 중 최고 기록은 foo의 99점!
user1.userScore = 30 // 점수 갱신이 없기에 didSet으로 출력되는 문구 없음
user2.userScore = 50 //best of your record bar!!
print("유저 중 최고 기록은 \(Score.bestUser)\(Score.bestRecord)점!") //유저 중 최고 기록은 foo의 99점!
user2.userScore = 1000 /*
                        bar hit the highest Score!!
                        best of your record bar!!
                        best score of all users!!
                        */
print("유저 중 최고 기록은 \(Score.bestUser)\(Score.bestRecord)점!") //유저 중 최고 기록은 bar의 999점!

점수에 대한 Score 구조체가 존재하고 Score를 통해 점수를 기록하는 모든 유저들 사이에 최고 기록에 대한 타입 프로퍼티가 공유된다. 또한 타입 프로퍼티에 대한 변화에 따른 공지를 didSet을 통해 출력시켜준다.
기본적인 키워드는 static이며 클래스에서는 클래스 내부에 class를 통해 정의하고 이후 하위 클래스에서 override할 수 있다.(static을 사용하면 override가 불가능)

post-custom-banner

0개의 댓글