part2. 프로퍼티와 메서드1

구름코딩·2020년 8월 31일

프로퍼티

정의 : 클래스, 구조체 또는 열거형 등에 관련된 값

3가지 분류

저장 프로퍼티

  • 인스턴스의 변수 또는 상수
  • 구조체와 클래스에서만 사용

var로 선언한 프로퍼티 : 변수 저장 프로퍼티
let으로 선언한 프로퍼티 : 상수 저장 프로퍼티

구조체와 클래스의 저장 프로퍼티
구조체는 저장 프로퍼티를 모두 포함하는 이니셜라이저(init)를 자동으로 생성
하지만, 클래스의 저장 프로퍼티는 옵셔널이 아니라면 프로퍼티 기본값을 지정해주거나 사용자 정의 이니셜라이저(init)을 통해 반드시 초기화 해줘야한다

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

//구조체
    struct Dot {
        var x: Int  //변수 저장 프로퍼티
        var y: Int  //변수 저장 프로퍼티
    }
    
    //구조체에는 기본적으로 **저장프로퍼티를 매개변수로 같은 이니셜라이저**가 있다
    let woonsikDot: Dot = Dot(x: 10, y: 20)
    
    //클래스
    class Position {
        var point: Dot
        let name: String
        
        func print_info_position() {
            print("위치: \(point.x), \(point.y) 이름: \(name)")
        }
        
        //프로퍼티의 기본값을 지정해주지 않는다면 이니셜라이저를 통해 따로 정의해야한다
        init (point: Dot, name: String){
            self.point = point
            self.name = name
        }
    }
    
    //사용자 정의 이니셜라이저를 호출
    //그렇지 않으면 프로퍼티 초기값을 할당할 수 없어서 인스턴스 생성 자체가 불가능
    let woonsikPosition: Position = Position(point: woonsikDot, name: "woonsik")
    
    woonsikPosition.print_info_position()
//출력
위치: 10, 20 이름: woonsik

구조체는 프로퍼티에 맞는 이니셜라이저를 자동으로 제공하지만, 클래스는 직접 이니셜라이저를 구현하거나 저장 프로퍼티에 초기값을 지정해 줘야한다

저장 프로퍼티의 초기값 지정

//저장 프로퍼티의 초기값 지정
    print("\n저장프로퍼티 초기값 지정\n")
    
    struct Dot_HaveValue {
        var x: Int = 0
        var y: Int = 0
    }
    let woonsikPosition2: Dot_HaveValue = Dot_HaveValue()
    let woonsikPosition3: Dot_HaveValue = Dot_HaveValue(x: 15, y: 15)
    print("\(woonsikPosition2.x), \(woonsikPosition2.y)")
    print("\(woonsikPosition3.x), \(woonsikPosition3.y)")
    
    class Position2 {
        var point: Dot_HaveValue = Dot_HaveValue()
        var name: String = "Unknown"
    }
    let woonPosition: Position2 = Position2()
    woonPosition.point = woonsikPosition2
    woonPosition.name = "woonsik"
    print("\(woonPosition.point), \(woonPosition.name)")
//출력
0, 0
15, 15
Dot_HaveValue(x: 0, y: 0), woonsik

클래스에서 초기값을 미리 지정을 해줘서 클래스의 인스턴스에서 이니셜라이저 구현없이 값의 할당이 이루어 질수 있게되었다. 하지만, 인스턴스를 생성한 후에 원하는 값을 일일이 할당해야하고, 미리 초기값을 준 후에 값을 변경해야 하므로 let을 사용하는 상수선언을 할수 없다

인스턴스를 생성할 때 이니셜라이저를 통해 초기값을 보내야 하는 이유
프로퍼티가 옵셔널이 아닌 값으로 선언되어 있기 때문으로, 인스턴스를 생성할 때 프로퍼티값에 꼭 값이 있는 상황이어야 한다. 따라서, 저장 프로퍼티값이 옵셔널이라면 값이 있어도 그만 없어도 그만이므로 초기값을 넣어주지 않아도 된다.
즉, 이니셜라이저에서 옵셔널 프로퍼티에 값을 꼭 할당하지 않아도 된다

옵셔널 저장 프로퍼티

특정 프로퍼티는 옵셔널로 선언하여 값을 비우고, 나머지는 이니셜라이저를 사용하여 초기값을 할당할 수 있다

//옵셔널 저장 프로퍼티
    //굳이 초기값을 지정하지 않아도되는 옵셔널 프로퍼티
    print("옵셔널 저장 프로퍼티")
    struct Dot2 {
        var x: Int
        var y: Int
    }
    
    class Position3 {
        //현재 사람의 위치를 모를수도 있으므로 - 옵셔널
        var pos: Dot2?
        let name: String
        
        func print_pos_position3() {
            if let tmpPos: Dot2 = pos {
                print("\(name) 위치 x: \(tmpPos.x), y: \(tmpPos.y)")
            }
        }
        
        init (name: String) {
            self.name = name
        }
    }
    
    //이름은 필수지만 위치는 모를수 있다
    let personInfo: Position3 = Position3(name: "woonsik")
    
    //후에 다시 위치정보 입력
    personInfo.pos = Dot2(x: 100, y: 100)
    personInfo.print_pos_position3()
출력
woonsik 위치 x: 100, y: 100

지연 저장 프로퍼티

//지연 저장 프로퍼티
    //필요할 때 값이 할당, 지연 저장 프로퍼티는 호출이 있어야 값을 초기화한다
    //이때, lazy 키워드를 사용
    //다만 상수는 인스턴스가 생성되기전에 초기화 되어야 하므로 필요할 때 값을 할당하는
    //지연 저장 프로퍼티 lazy속성은 var 변수에만 사용 가능하다
    struct Dot3 {
        var x: Int = 0
        var y: Int = 0
    }
    
    class Position4 {
        lazy var point: Dot3 = Dot3()
        let name: String
        
        init (name: String) {
            self.name = name
        }
    }
    
    let woonsikPosition4: Position4 = Position4(name: "woonsik")
    
    //이 아래 코드를 통해 point 프로퍼티로 처음 접근할 때
    //point프로퍼티의 Dot3가 생성된다
    print(woonsikPosition4.point)
출력
Dot3(x: 0, y: 0)

연산 프로퍼티

  • 특정 연산의 실행한 결과값을 의미
  • 클래스, 구조체, 열거형에서 사용

메서드를 사용하여 구현한 경우

print("\n연산 프로퍼티\n")
    
    //연산프로퍼티를 사용하지 않고 메서드를 사용하여 구현한 경우
    print("//연산프로퍼티를 사용하지 않고 메서드를 사용하여 구현한 경우")
    
    struct Point {
        var x: Int
        var y: Int
        
        func oppositePoint() -> Self {
            return Point(x: -x, y: -y)
        }
        
        mutating func setOppositePoint(_ opposite: Point) {
            x = -opposite.x
            y = -opposite.y
        }
    }
    
    var woonsikPoint: Point = Point(x: 10, y: 10)
    
    //현재 좌표
    print(woonsikPoint)
    
    //대칭좌표
    print(woonsikPoint.oppositePoint())
    
    //대칭좌표를 설정
    woonsikPoint.setOppositePoint(Point(x: 10, y: 10))
    
    //현재 좌표
    print(woonsikPoint)

출력
//연산프로퍼티를 사용하지 않고 메서드를 사용하여 구현한 경우
Point(x: 10, y: 10)
Point(x: -10, y: -10)
Point(x: -10, y: -10)

연산 프로퍼티를 사용하는 경우

//연산 프로퍼티를 사용하여 구현한 경우
    print("\n연산 프로퍼티를 사용하여 구현한 경우\n")
    
    struct Point2 {
        var x: Int
        var y: Int
        
        var oppositePoint: Point2 { //연산 프로퍼티
            //접근자
            get {
                //return 생략 가능
                Point2(x: -x, y: -y)
            }
            //설정자 (설정자를 생략하는 경우 읽기전용으로만 가능, 쓰기전용만으로는 할수 없다(only set x)
            set {
                x = -newValue.x
                y = -newValue.y
            }
        }
    }
    var woonsikPoint2: Point2 = Point2(x: 10, y: -10)
    print("좌표")
    print(woonsikPoint2)
    print("반대좌표")
    print(woonsikPoint2.oppositePoint)
    
    print("\n설정자 이용하여 좌표설정\n")
    woonsikPoint2.oppositePoint = Point2(x: 10, y: 20)
    
    print("설정자 좌표")
    print(woonsikPoint2)
    print("설정자 반대 좌표")
    print(woonsikPoint2.oppositePoint)
출력
연산 프로퍼티를 사용하여 구현한 경우

좌표
Point2(x: 10, y: -10)
반대좌표
Point2(x: -10, y: 10)

설정자 이용하여 좌표설정

설정자 좌표
Point2(x: -10, y: -20)
설정자 반대 좌표
Point2(x: 10, y: 20)

프로퍼티 감시자

  • 프로퍼티의 값이 변하는 것을 감시하는 것으로, 프로퍼티의 값이 변할 때 값의 변화에 따른 특정 작업을 실행한다.
  • 저장 프로퍼티에 적용가능하며 부모클래스로부터 상속을 받을 수 있다
  • 지연 저장 프로퍼티에는 사용할 수 없다
  • 프로퍼티의 값이 변경되기 직전에 호출되는 willSet, 변경된 직후에 호출되는 didSet메서드가 있으며, 기본 매개변수로 newValueoldValue가 있다
//프로퍼티 감시자
    print("\n프로퍼티 감시자\n")
    
    class Account {
        var credit: Int = 0 {
            willSet {
                print("\(credit)에서 \(newValue)로 변경될 예정입니다.")
            } didSet {
                print("\(oldValue)에서 \(credit)으로 변경되었습니다.")
            }
        }
    }
    
    //나의 신용 선언
    let myCredit: Account = Account()
    //신용도 변경
    myCredit.credit = 1
    
//출력
프로퍼티 감시자

0에서 1로 변경될 예정입니다.
0에서 1으로 변경되었습니다.

연산프로퍼티의 접근자와 설정자를 통해 프로퍼티 감시자를 구현할수 있으므로 연산 프로퍼티는 상속받았을 때만 프로퍼티 재정의를 통해 프로퍼티 감시자를 사용한다

   //상속받은 연산 프로퍼티의 프로퍼티 감시자 구현
    print("\n//상속받은 연산 프로퍼티의 프로퍼티 감시자 구현\n")
    
    class Account_bank {
        var credit: Int = 0 {
            willSet {
                print("나의 잔고가 \(credit)원에서 \(newValue)원으로 변경될 예정입니다.")
            } didSet {
                print("나의 잔고가 \(oldValue)원에서 \(credit)원으로 변경되었습니다.")
            }
        }
        var dollarCredit: Double {
            get {
                return Double(credit) / 1000.0
            }
            set {
            //접근자와 설정자를 통한 프로퍼티 감시자 구현 예시
                credit = Int(newValue * 1000)
                print("잔액을 \(newValue)달러로 변경중...")
            }
        }
    }
    class ForeignAccount: Account_bank {
        override var dollarCredit: Double {
            willSet {
                print("잔액이 \(dollarCredit)달러에서 \(newValue)달러로 변경될 예정입니다")
            }
            didSet {
                print("잔액이 \(oldValue)달러에서 \(dollarCredit)달러로 변경되었습니다")
            }
        }
    }
    let myAccount: ForeignAccount = ForeignAccount()
    myAccount.credit = 8000
    
    myAccount.dollarCredit = 45
    
//출력
//상속받은 연산 프로퍼티의 프로퍼티 감시자 구현

나의 잔고가 0원에서 8000원으로 변경될 예정입니다.
나의 잔고가 0원에서 8000원으로 변경되었습니다.
잔액이 8.0달러에서 45.0달러로 변경될 예정입니다 <-- 오버라이드를 통한 프로퍼티 감시자
나의 잔고가 8000원에서 45000원으로 변경될 예정입니다.
나의 잔고가 8000원에서 45000원으로 변경되었습니다.
잔액을 45.0달러로 변경중... <-- 기존 접근자, 설정자를 통한 프로퍼티 변경 감시자 역활
잔액이 8.0달러에서 45.0달러로 변경되었습니다 <-- 오버라이드를 통한 프로퍼티 감시자

입출력 매개변수와 프로퍼티 감시자
프로퍼티 감시자가 있는 프로퍼티를 함수의 입출력 매개변수의 전달인자로 전달한다면 항상 willSet과 didSet 감시자를 호출한다. 값이 변경되든 안되든 함수가 종료될때 항상 값을 다시 할당하기 때문

연산 프로퍼티와 프로퍼티 감시자는 프로퍼티에 뿐만 아니라 전역변수 지역변수에도 사용할 수 있다

참고로, 전역변수와 전역상수는 지연 저장 프로퍼티와 같이 처음 접근될 때 최초로 연산이 이뤄지므로 lazy 속성이 필요없다. 반대로 지역변수 및 지역상수는 절대로 지연 연산되지 않는다

지정변수의 감시자와 연산변수

//지정변수의 감시자와 연산변수
    print("/n지정변수의 감시자와 연산변수\n")
    var wonInPoket: Int = 2000 {
        willSet {
            print("주머니의 돈이 \(wonInPoket)원에서 \(newValue)원으로 변경될 예정입니다")
        }
        didSet {
            print("주머니의 돈이 \(oldValue)원에서 \(wonInPoket)원으로 변경되었습니다")
        }
    }
    var dollarInPoket: Double {
        get {
            return Double(wonInPoket) / 1000.0
        }
        set {
            wonInPoket = Int(newValue * 1000)
            print("주머니의 돈을 \(newValue)달러로 변경 중입니다")
        }
    }
    print("3000원\n")
    wonInPoket = 3000
    print("5달러\n")
    dollarInPoket = 5
//출력
지정변수의 감시자와 연산변수

3000원

주머니의 돈이 2000원에서 3000원으로 변경될 예정입니다
주머니의 돈이 2000원에서 3000원으로 변경되었습니다
5달러

주머니의 돈이 3000원에서 5000원으로 변경될 예정입니다
주머니의 돈이 3000원에서 5000원으로 변경되었습니다
주머니의 돈을 5.0달러로 변경 중입니다

타입 프로퍼티

  • 특정 타입에 사용되는 프로퍼티를 일컫는다

  • 모든 인스턴스가 공통으로 사용하는 값 혹은 모든 인스턴스에서 공용으로 접근하고 변경할 수 있는 변수 등을 정의할 때 유용하다

  • 저장 타입 프로퍼티와 연산 타입 프로퍼티가 존재
    저장 타입 프로퍼티 : 변수 또는 상수로 선언가능, 초기값 설정 필수, 지연 연산이 된다
    연산 타입 프로퍼티 : 변수로만 선언가능

타입 프로퍼티와 인스턴스 프로퍼티와의 차이

class AClass {
        //저장 타입 프로퍼티
        static var typeProperty: Int = 0
        
        //저장 인스턴스 프로퍼티
        var instanceProperty: Int = 0 {
            didSet {
                Self.typeProperty = instanceProperty + 100
            }
        }
        //연산 타입 프로퍼티
        static var typeComputedProperty: Int {
            get {
                return typeProperty
            }
            set {
                typeProperty = newValue
            }
        }
    }
    AClass.typeProperty = 11
    
    let classInstance: AClass = AClass()
    classInstance.instanceProperty = 100
    
    print(AClass.typeProperty)
    print(AClass.typeComputedProperty)
//출력
typeProperty는 타입프로퍼티로 인스턴스를 생성하지 않고도 사용할수 있고, 타입에 해당하는 값이다. 따라서 인스턴스에 접근하지 않고도 타입 이름만으로도 프로퍼티를 사용할수 있다

타입 프로퍼티의 상수로서의 사용

print("\n타입 프로퍼티의 상수로서의 사용\n")
    
    class Account_T {
        static let dollarExchangeRate: Double = 1000.0
        
        var credit: Int = 0
        
        var dollarValue: Double {
            get {
                return Double(credit) / Self.dollarExchangeRate
            }
            set {
                //Account_T.dollarExchangeRate == Self.dollarExchangeRate
                credit = Int(newValue * Account_T.dollarExchangeRate)
                print("잔액을 \(newValue)달러로 변경 중입니다.")
            }
        }
    }
    
    let instance: Account_T = Account_T()
    instance.credit = 1000
    print(instance.dollarValue)
    instance.dollarValue = 20
//출력
1.0
잔액을 20.0달러로 변경 중입니다.

키 경로

함수가 일급 시민으로서 상수나 변수에 참조를 할당 할수 있듯이 프로퍼티도 값을 바로 꺼내오는것이 아닌 어떤 프로퍼티의 위치만 참조하도록 할수 있다.
-> 키 경로 (keyPath)를 활용

키경로는 AnyKeyPath라는 클래스로부터 파생된다 기본형태는 \타입이름.경로.경로.경로이며 경로는 프로퍼티 이름이다

대표적으로 확장된 키 경로 타입

  • WritableKeyPath<Root, Value>
    • 값 타입에 키 경로 타입으로 읽고 쓸수 있는 경우에 적용
  • ReferenceWritableKeyPath<Root, Value>
    • 참조타입, 즉 클래스 타입에 키 경로 타입으로 읽고 쓸 수 있는 경우에 적용
class Person {
        var name: String
        init(name: String) {
            self.name = name
        }
    }
    struct Stuff {
        var name: String
        var owner: Person {
            didSet {
                print("\(owner.name)으로 주인이 변경됩니다...")
            }
        }
    }
    
    print(type(of: \Person.name))
    print(type(of: \Stuff.name))
//
ReferenceWritableKeyPath<Person, String>
WritableKeyPath<Stuff, String>
//
    let woonsik = Person(name: "woonsik")
    let hana = Person(name: "hana")
    let macbook = Stuff(name: "Mac Book Pro", owner: woonsik)
    var iphone = Stuff(name: "iPhone11", owner: hana)
    
    let stuffNameKeyPath = \Stuff.name
    let ownerKeyPath = \Stuff.owner
    
    let ownerNameKeyPath = ownerKeyPath.appending(path: \.name)
    
    print("제품이름: ",macbook[keyPath: stuffNameKeyPath])
    print("제품이름: ",iphone[keyPath: stuffNameKeyPath])
    print("주인이름: ",macbook[keyPath: ownerNameKeyPath])
    print("주인이름: ",iphone[keyPath: ownerNameKeyPath])
    
    iphone[keyPath: ownerKeyPath] = woonsik
    
    print("주인이름: ",iphone[keyPath: ownerNameKeyPath])
//
제품이름:  Mac Book Pro
제품이름:  iPhone11
주인이름:  woonsik
주인이름:  hana
woonsik으로 주인이 변경됩니다...
주인이름:  woonsik
profile
내꿈은 숲속의잠자는공주

0개의 댓글