[Swift] 객체지향 프로그래밍과 스위프트 - 프로퍼티와 메서드

koi·2022년 10월 9일
0
post-thumbnail
  • 프로퍼티 : 클래스, 구조체 또는 열거형 등에 관련된 값
  • 메서드 : 특정 타입에 관련된 함수

프로퍼티

프로퍼티는 크게 저장, 연산, 타입 프로퍼티로 나눌 수 있다.

  • 저장 프로퍼티
    • 특정 타입의 인스턴스에 사용
    • 인스턴스의 변수 또는 상수
    • 클래스구조체에서만 사용할 수 있음
  • 연산 프로퍼티
    • 특정 타입의 인스턴스에 사용
    • 값을 저장한 것이 아니라 특정 연산을 실행한 결괏값
    • 클래스, 구조체, 열거형에서 쓸 수 있음
  • 타입 프로퍼티
    • 특정 타입에 사용됨

  • 프로퍼티 감시자
    • 프로퍼티의 값이 변하는 것을 감시
    • 저장 프로퍼티 뿐만 아니라 상속받은 저장 프로퍼티 또는 연산 프로퍼티를 재정의해서 적용할 수 있음
    • 프로퍼티 감시자는 프로퍼티의 값이 변할 때의 값의 변화에 따른 특정 작업을 실행함
    • 부모클래스로부터 상속받을 수 있음

📌 저장 프로퍼티

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

  • var 키워드를 사용하면 변수 저장 프로퍼티
  • let 키워드를 사용하면 상수 저장 프로퍼티
  • 기본값과 초깃값 지정 가능
  • 구조체는 저장 프로퍼티가 옵셔널이 아니더라도 저장 프로퍼티를 모두 포함하는 이니셜라이저를 자동으로 생성함
  • 클래스의 저장 프로퍼티는 옵셔널이 아니라면 프로퍼티 기본값을 지정해주거나 사용자 정의 이니셜라이저를 통해 반드시 초기화해주어야 함
  • 클래스 인스턴스의 상수 프로퍼티는 인스턴스가 초기화될 때 한번만 값을 할당할 수 있음.
    • 자식클래스에서 이 초기화를 변경(재정의)할 수 없음

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

struct CoordinatePoint {
    var x: Int 
    var y: Int
}
// 구조체에는 기본적으로 저장 프로퍼티를 매개변수로 갖는 이니셜라이저가 있음
let tomaPoint: CoordinatePoint = CoordinatePoint(x: 10, y: 5)


class Position {
    var point: CoordinatePoint
    let name: String
    
    // 프로퍼티 기본값을 지정하지 않는다면 이니셜라이저를 따로 정의해야함
    init(name: String, currentPoint: CoordinatePoint) {
        self.name = name
        self.point = currentPoint
    }
}

// 사용자 정의 이니셜라이저를 호출해야만함
// 그렇지 않으면 프로퍼티 초깃값을 할당할 수 없기 때문에 인스턴스 생성이 불가능함
let tomaPosition: Position = Position(name: "toma", currentPoint: tomaPoint)

구조체는 프로퍼티에 맞는 이니셜라이저를 자동으로 제공하지만, 클래스는 그렇지 않아서 클래스 인스턴스의 저장 프로퍼티를 사용하는 일은 더 번거로움.

하지만 클래스의 저장 프로퍼티에 초깃값을 지정해주면 따로 사용자 정의 이니셜라이저를 구현할 필요가 없음.

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

struct CoordinatePoint {
    var x: Int = 0
    var y: Int = 0
}
// 프로퍼티에 초깃값을 할당했다면 굳이 전달인자로 초깃값을 넘길 필요가 없음
let tomaPoint: CoordinatePoint = CoordinatePoint()

// 물론 초깃값을 할당할 수 있는 이니셔라이저도 사용 가능
let matoPoint: CoordinatePoint = CoordinatePoint(x: 10, y: 5)


class Position {
    var point: CoordinatePoint = CoordinatePoint() // 저장 프로퍼티
    let name: String = "Unknown" // 저장 프로퍼티
} 

// 초기값을 지정했다면 사용자 정의 이니셜라이저를 사용하지 않아도 됨
let tomaPositon: Position = Position()

tomaPosition.point = tomaPoint
tomaPosition.name = "toma"

초깃값을 미리 지정하면 인스턴스를 만드는 과정이 훨씬 간편해진다.

그러나 의도와 맞지않게 인스턴스가 사용될 가능성이 있고, 인스턴스를 생성한 후 일일이 원하는 값을 할당해주어야 한다

Positon의 name 프로퍼티는 한번 값을 할당해준 후 변경하지 못하도록 상수로 정의하고 싶었으나, 인스턴스를 생성한 후 값을 할당해주어야 하기 때문에 그렇게 할수 없었다.

저장 프로퍼티에 값이 있어도 그만, 없어도 그만인 옵셔널이라면 굳이 초깃값을 넣어주지 않아도 된다. 즉, 이니셜라이저에서 옵서녈 프로퍼티에 꼭 값을 할당해주지 않아도 된다.


옵셔널 저장 프로퍼티

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

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

// 이름은 필수지만 위치는 모를 수도 있다.
let tomaPosition: Position = Position(name: "toma")

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

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

지연 저장 프로퍼티

프로퍼티를 옵셔널로 선언하는 것과는 조금 다은 용도로 필요할 때 값이 할당되는 지연 저장 프로퍼티가 있다.

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

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

따라서 지연 저장 프로퍼티는 var 키워드를 사용하여 번수로 정의한다.

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

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

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

let tomaPosition: Position = Position(name: "toma")

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

다중 스레드와 지연 저장 프로퍼티 다중 스레드 환경에서 지연 저장 프로퍼티에 동시다발적으로 접근할 때는 한 번만 초기화된다는 보장이 없다.
생성되지 않은 지연 저장 프로퍼티에 많은 스레드가 비슷한 시점에 접근한다면, 여러 번 초기화될 수 있다.


📌 연산 프로퍼티

  • 실제 값을 저장하는 프로퍼티가 아니라, 특정 상태에 따른 값을 연산하는 프로퍼티
  • 인스턴스 내/외부의 값을 연산하여 적절한 값을 돌려주는 접근자의 역할
  • 은닉화된 내부의 프로퍼티 값을 간접적으로 설정하는 설정자의 역할
  • 클래스, 구조체, 열거형에 연산 프로퍼티를 정의할 수 있음
  • 프로퍼티가 메서드 형식보다 훨씬 더 간편하고 직관적임
    • 연산 프로퍼티는 읽기 전용 상태로 구현하기 쉽지만, 쓰기 전용 상태로 구현할 수 없다는 단점이 있음
    • 메서드로는 설정자 메서드만 구현하여 쓰기 전용 상태로 구현할 수 있음

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

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


var tomaPosition: CoordinatePoint = CoordinatePoint(x: 10, y: 20)

// 현재 좌표
print(tomaPosition)  // 10, 20

// 대칭 좌표
print(tomaPosition.oppositePoint()) // -10, -20

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

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

연산 프로퍼티의 정의와 사용

struct CoordinatePoint {
    var x: Int // 저장 프로퍼티
    var y: Int // 저장 프로퍼티
    
    // 대칭 좌표
    var oppositePoint: CoordinatePoint { // 연산 프로퍼티
        // 접근자
        get {
            return CoordinatePoint(x: -x, y: -y)
        }
        
        // 설정자
        set(opposite) {
            x = -opposite.x
            y = -opposite.y
        }
    }
}


var tomaPosition: CoordinatePoint = CoordinatePoint(x: 10, y: 20)

// 현재 좌표
print(tomaPosition)  // 10, 20

// 대칭 좌표
print(tomaPosition.oppositePoint) // -10, -20

// 대칭 좌표를 (15, 10)으로 설정하면
tomaPosition.oppositePoint = CoordinatePoint(x: 15, y: 10)

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

이런 식으로 연산 프로퍼티를 사용하면 하나의 프로퍼티에 접근자와 설정자가 모두 모여있고, 해당 프로퍼티가 어떤 역할을 하는지 좀 더 명확하게 표현 가능함

  • 설정자의 매개변수로 원하는 이름을 소괄호 안에 명시해주면 set 메서드 내부에서 전달받은 전달인자를 사용할 수 있음
    관용적인 표현으로 newValue로 매개변수 이름을 대신할 수 있다. 그럴 경우에는 매개변수를 따로 표기하지 말아야 한음
  • 접근자 내부의 코드가 단 한 줄이고, 그 결괏값의 타입이 프로퍼티의 타입과 같다면 return 키워드를 생략해도 그 결괏값이 접근자의 반환값이 됨

매개변수 이름을 생략한 설정자

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

읽기 전용 연산 프로퍼티

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

프로퍼티 감시자

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

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

  • 저장 프로퍼티뿐만 아니라 프로퍼티를 재정의해 상속받은 저장 프로퍼티 또는 연산 프로퍼티에도 적용할 수 있다.

  • 상속받지 않은 연산 프로퍼티에는 프로퍼티 감시자를 사용할 필요가 없으며 할수도 없다. 연산 프로퍼티의 접근자와 설정자를 통해 프로퍼티 감시자를 구현할 수 있기 때문이다.
    연산 프로퍼티는 상속받았을 때만 프로퍼티 재정의를 통해 프로퍼티 감시자를 사용한다.

  • 프로퍼티의 값이 변경되기 직전에 호출하는 willSet 메서드와 프로퍼티의 값이 변경된 직후에 호출하는 didSet 메서드가 있다.

  • willSet 메서드와 didSet 메서드에는 매개변수가 하나씩 있다.

  • willSet 메서드에 전달되는 전달인자는 프로퍼티가 변경될 값이고 매개변수의 이름을 따로 지정하지 않으면 newValue가 자동 지정된다.

  • didSet 메서드에 전달되는 전달인자는 프로퍼티가 변경되기 전의 값이고 매개변수의 이름을 따로 지정하지 않으면 oldValue가 자동 지정된다.

class Account {
    var credit: Int = 0 {
        willSet {
            print("잔액이 \(credit)원에서 \(newValue)원으로 변경될 예정이다.")
        }
        
        didSet {
            print("잔액이 \(oldValue)원에서 \(credit)원으로 변경되었다.")
        }
    }
}


let myAccount: Account = Account()
// 잔액이 0원에서 1000원으로 변경될 예정이다.
myAccount.credit = 1000
// 잔액이 0원에서 1000원으로 변경되었다.

연산 프로퍼티를 상속받았다면 재정의하여 프로퍼티 감시자를 구현할 수도 있다.
연산 프로퍼티를 재정의해도 기존의 연산 프로퍼티 기능(접근자, 설정자 get, get 메서드)은 동작한다.

class Account {
    var credit: Int = 0 {
        willSet {
            print("잔액이 \(credit)원에서 \(newValue)원으로 변경될 예정입니다.")
        }
        
        didSet {
            print("잔액이 \(oldValue)원에서 \(credit)원으로 변경되었습니다.")
        }
    }
    
    var dollarValue: Double { // 연산 프로퍼티
        get {
            return Double(credit) / 1000.0
        }
        
        set {
            credit = Int(newValue * 1000)
            print("잔액을 \(newValue)달러로 변경 중입니다.")
        }
    }
}

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

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

// 잔액이 1.0달러에서 2.0달러로 변경될 예정입니다.
// 잔액이 1000원에서 2000원으로 변경될 예정입니다.
// 잔액이 1000원에서 2000원으로 변경되었습니다.

myAccount.dollarValue = 2 // 잔액을 2.0달러로 변경 중입니다.
// 잔액이 1.0달러에서 2.0달러로 변경되었습니다.

만약 프로퍼티 감시자가 있는 프로퍼티를 함수의 입출력 매개변수의 전달인자로 전달한다면 항상 willSet과 didSet 감시자를 호출한다.
함수 내부에서 값이 변경되든 되지 않든 간에 함수가 종료되는 시점에 값을 다시 쓰기 때문이다.

전역변수와 지역변수

  • 함수나 메서드, 클로저, 클래스, 구조체, 열거형 등의 범위 안에 포함되지 않은 변수, 상수는 모두 전역변수 또는 전역상수에 해당된다.
  • 연산 프로퍼티와 프로퍼티 감시자는 전역변수와 지역변수 모두에 사용할 수 있다.
  • 전역변수, 전역상수는 지연 저장 프로퍼티처럼 처음 접근할 때 최초로 연산이 이루어진다.
    lazy 키워드를 사용해 연산을 늦출 필요가 없다.
  • 지역변수, 지역상수는 절대로 지연 연산되지 않는다.
var wonInPocket: Int = 2000 {
    willSet {
        print("주머니의 돈이 \(wonInPocket)원에서 \(newValue)원으로 변경될 예정입니다.")
    }
    
    didSet {
        print("주머니의 돈이 \(oldValue)원에서 \(wonInPocket)원으로 변경되었습니다.")
    }
}


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

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

📌 타입 프로퍼티

이제까지 알아본 프로퍼티 개념은 모두 타입을 정의하고 해당 타입의 인스턴스가 생성되었을 때 사용할 수 있는 인스턴스 프로퍼티이다.
인스턴스 프로퍼티는 인스턴스를 새로 생성할 때마다 초깃값에 해당하는 값이 프로퍼티의 값이 되고, 인스턴스마다 다른 값을 지닐 수 있다.

  • 각각의 인스턴스가 아닌 타입 자체에 속하는 프로퍼티를 타입 프로퍼티라고 함
  • 타입 프로퍼티는 타입 자체에 영향을 미치는 프로퍼티이다.
  • 인스턴스의 생성 여부와 상관없이 타입 프로퍼티의 값은 하나이며, 그 타입의 모든 인스턴스가 공통으로 사용하는 값, 모든 인스턴스에서 공용으로 접근하고 값을 변경할 수 있는 변수 등을 정의할 때 유용하다.
  • 타입 프로퍼티의 2가지 종류
    • 저장 타입 프로퍼티
      • 변수 또는 상수로 선언할 수 있음
      • 반드시 초깃값을 설정해야함
      • 지연 연산됨
      • 지연 저장 프로퍼티와 달리 다중 스레드 환경이라도 단 한 번만 초기화된다는 보장을 받음
    • 연산 타입 프로퍼티
      • 변수로만 선언할 수 있음
class AClass {
    // 저장 타입 프로퍼티
    static var typeProperty: Int = 0
    
    // 저장 인스턴스 프로퍼티
    var instanceProperty: Int = 0 {
        didSet {
            // Self.typeProperty는
            // AClass.typeProperty와 같은 표현이다.
            Self.typeProperty = instanceProperty + 100
        }
    }
    
    // 연산 타입 프로퍼티
    static var typeComputedProperty: Int {
        get {
            return typeProperty
        }
        
        set {
            typeProperty = newValue
        }
    }
}


AClass.typeProperty = 123

let classInstance: AClass = AClass()
classInstance.instanceProperty = 100

print(AClass.typeProperty) // 200
print(AClass.typeComputedProperty) // 200
  • 타입 프로퍼티는 인스턴스를 생성하지 않고도 사용할 수 있으며 타입에 해당하는 값이다.
  • 인스턴스에 접근할 필요 없이 타입 이름만으로 프로퍼티를 사용할 수 있다.
  • 타입 프로퍼티를 타입 상수로 사용할 수도 있다.
class Account{
    
    // 타입 상수
    static let dollarExchangeRate: Double = 1000.0
    
    var credit: Int = 0
    
    var dollarValue: Double {
        get {
            return Double(credit) / Self.dollarExchangeRate
            // Self.dollarExchangeRate는 Account.dollarExchangeRate와 같은 표현
            // Self는 Account 타입을 가리킴.
        }
        set {
            credit = Int(newValue * Account.dollarExchangeRate)
            print("잔액을 \(newValue)달러로 변경 중입니다")
        }
    }  
}

키 경로

함수는 일급시민(일급객체)으로서 상수나 변수에 함수의 참조를 할당할 수 있다.

func someFuction(paramA: Any, paramB: Any) {
	print("SomeFunction called...")
}
var functionReference = someFuction(paramA:paramB:)

functionReference("A", "B") // SomeFunction called...
functionReference = anotherFunction(paramA:paramB:)

함수를 참조해두고 나중에 원할 때 호출하거나 다른 함수를 참조하도록 할 수도 있다.

키 경로를 이용해 프로퍼티도 이처럼 값을 바로 꺼내오는 것이 아니라 어떤 프로퍼티의 위치만 참조할 수 있도록 할 수 있다.

키 경로를 사용하여 간접적으로 특정 타입의 어떤 프로퍼티 값을 가리켜야할지 미리 지정해두고 사용할 수 있다.
키경로는 역슬래시/타입, 마침표. 경로로 구성된다.

class Person {
    var name: String
    
    init(name: String) {
        self.name = name
    }
}

struct Stuff {
    var name: String
    var owner: Person
}

print(type(of: \Person.name)) // ReferenceWritableKeyPath<Person, String>
print(type(of: \Stuff.name)) // WritableKeyPath<Stuff, String>
  • 키 경로를 잘 활용하면 프로토콜과 마찬가지로 타입 간 의존성을 낮추는 데 많은 도움을 준다
  • 애플의 프레임워크는 키-값 코딩 등 많은 곳에 키 경로를 활용한다.

키 경로는 타입 외부로 공개된 인스턴스 프로퍼티 혹은 서브스크립트에 한하여 표현할 수 있다.

클로저를 대체할 수 있는 키 경로 표현

  • 스위프트 5.2 버전 부터 (SomeType) -> Value 타입의 클로저를 키 경로 표현으로 대체하여 사용할 수 있다.
  • .name은 클로저 표현인 {$0.name}의 표현과 같은 역할을 한다.

struct Person {
    let name: String
    let nickname: String?
    let age: Int
    
    var isAdult: Bool {
        return age > 18
    }
}

let yagom: Person = Person(name: "yagom", nickname: "bear", age: 100)
let hana: Person = Person(name: "hana", nickname: "na", age: 100)
let happy: Person = Person(name: "happy", nickname: nil, age: 3)

let family: [Person] = [yagom, hana, happy]
let names: [String] = family.map(\.name) // ["yagom", "hana", "happy"]
let nicknames: [String] = family.compactMap(\.nickname) // ["bear", "na"]
let adults: [String] = family.filter(\.isAdult).map(\.name) // ["yagom", "hana"]

메서드

메서드는 특정 타입에 관련된 함수를 뜻한다.
클래스, 구조체, 열거형 등은 실행하는 기능을 캡슐화한 인스턴스 메서드를 정의할 수 있다.

📌 인스턴스 메서드

  • 클래스, 구조체, 열거형 등은 실행하는 기능을 캡슐화한 인스턴스 메서드를 정의할 수 있다
  • 특정 타입의 인스턴스에 속한 함수
  • 인스턴스 내부의 프로퍼티 값을 변경하거나 특정 연산결과를 반환하는 등 인스턴스와 관련된 기능을 실행
  • 함수와 달리 특정 타입 내부에 구현
    → 인스턴스가 존재할 때 사용할 수 있음, 함수와 유일한 차이점
class LevelClass {
    // 현재 레벨을 저장하는 저장 프로퍼티
    var level: Int = 0 {
        // 프로퍼티 값이 변경되면 호출하는 프로퍼티 감시자
        didSet {
            print("Level \(level)")
        }
    }
    
    // 레벨이 올랐을 때 호출할 메서드
    func levelUp() {
        print("Level Up!")
        level += 1
    }
    
    // 레벨이 감소했을 때 호출할 메서드
    func levelDown() {
        print("Level Down")
        level -= 1
        if level < 0 {
            reset()
        }
    }
    
    // 특정 레벨로 이동할 때 호출할 메서드
    func jumpLevel(to: Int) {
        print("Jump to \(to)")
        level = to
    }
    
    // 레벨을 초기화할 때 호출할 메서드
    func reset() {
        print("Reset!")
        level = 0
    }
}


var levelClassInstance: LevelClass = LevelClass()
levelClassInstance.levelUp() // Level Up!
//Level 1

levelClassInstance.levelDown() // Level Down
// Level 0

levelClassInstance.levelDown() // Level Down
// Level -1
// Reset!
// Level 0

levelClassInstance.jumpLevel(to: 3) // Jump to 3
// Level3

자신의 프로퍼티 값을 수정할 때 클래스의 인스턴스 메서드는 크게 신경 쓸 필요가 없지만,

구조체나 열거형 등은 값 타입이므로 메서드 앞에 mutationg 키워드를 붙여서 해당 메서드가 인스턴스 내부의 값을 변경한다는 것을 명시해야 함

struct LevelStruct {
    // 현재 레벨을 저장하는 저장 프로퍼티
    var level: Int = 0 {
        // 프로퍼티 값이 변경되면 호출하는 프로퍼티 감시자
        didSet {
            print("Level \(level)")
        }
    }
    
    // 레벨이 올랐을 때 호출할 메서드
    mutating func levelUp() {
        print("Level Up!")
        level += 1
    }
    
    // 레벨이 감소했을 때 호출할 메서드
    mutating func levelDown() {
        print("Level Down")
        level -= 1
        if level < 0 {
            reset()
        }
    }
    
    // 특정 레벨로 이동할 때 호출할 메서드
    mutating func jumpLevel(to: Int) {
        print("Jump to \(to)")
        level = to
    }
    
    // 레벨을 초기화할 때 호출할 메서드
    mutating func reset() {
        print("Reset!")
        level = 0
    }
}


var levelClassInstance: LevelStruct = LevelStruct()
levelClassInstance.levelUp() // Level Up!
//Level 1

levelClassInstance.levelDown() // Level Down
// Level 0

levelClassInstance.levelDown() // Level Down
// Level -1
// Reset!
// Level 0

levelClassInstance.jumpLevel(to: 3) // Jump to 3
// Level3

self 프로퍼티

  • 모든 인스턴스는 암시적으로 생성된 self 프로퍼티를 가짐
  • 인스턴스 자기 자신을 가르키는 프로퍼티임
  • 인스턴스를 더 명확히 지칭하고 싶을 때 사용함
  • 스위프는 자동으로 메서드 내부에 선언된 지역변수를 먼저 사용, 그다음 메서드 매개변수, 그다음 인스턴스의 프로퍼티를 찾아서 유추함
class LevelClass {
	var level: Int = 0
    func jumpLevel(to level: Int) {
    	print("Jump to \(level)")
        self.level = level
    }
}
  • self 프로퍼티의 또 다른 용도로 값 타입 인스턴스 자체의 값을 치환할 수 있다.
  • 클래스의 인스턴스는 잠초 타입이라서 self 프로퍼티에 다른 참조를 할당할 수 없다.
  • 구조체나 열거형 등은 self 프로퍼티를 사용해 자신 자체를 치환할 수 있다.
class LevelClass {
    var level: Int = 0
    
    func reset() {
        // 오류!! self 프로퍼티 참조 변경 불가!
//        self = LevelClass()
    }
}

struct LevelStruct {
    var level: Int = 0
    
    mutating func levelUp() {
        print("Level Up!")
        level += 1
    }
    
    mutating func reset() {
        print("Reset!")
        self = LevelStruct()
    }
}

var levelStructInstance: LevelStruct = LevelStruct()
levelStructInstance.levelUp() // Level Up!
print(levelStructInstance.level) // 1

levelStructInstance.reset() // Reset!
print(levelStructInstance.level) // 0

enum OnOffSwitch {
    case on, off
    
    mutating func nextState() {
        self = self == .on ? .off : .on
    }
}

var toggle: OnOffSwitch = OnOffSwitch.off
toggle.nextState()
print(toggle) // on

인스턴스를 함수처럼 호출하도록 하는 메서드

사용자 정의 명목 타입의 호출 가능한 값을 구현하기 위해 인스턴스를 함수처럼 호출할 수 있도록 하는 메서드가 있다.
→ 특정 타입의 인스턴스를 문번적으로 함수를 사용하는 것처럼 보이게 할 수 있다.

callAsFunction이라는 이름의 메서드를 구현하면 된다. 이 메서드는 매개변수와 반환 타입만 다르다면 개수에 제한 없이 원하는 만큼 만들 수 있다.
mutating 키워드도 사용할 수 있고, throws와 rethrows도 함께 사용할 수 있다.

struct Puppy {
    var name: String = "멍멍이"
    
    func callAsFunction() {
        print("멍멍")
    }
    
    func callAsFunction(destination: String) {
        print("\(destination)(으)로 달려갑니다")
    }
    
    func callAsFunction(something: String, times: Int) {
        print("\(something)(을)를 \(times)번 반복합니다.")
    }
    
    func callAsFunction(color: String) -> String {
        return "\(color) 응가"
    }
    
    mutating func callAsFunction(name: String) {
        self.name = name
    }
}


var doggy: Puppy = Puppy()
doggy.callAsFunction() // 멍멍
doggy() // 멍멍
doggy.callAsFunction(destination: "집") // 집(으)로 달려갑니다
doggy(destination: "뒷동산") // 뒷동산(으)로 달려갑니다
doggy(something: "재주넘기", times: 3) // 재주넘기(을)를 3번 반복합니다
print(doggy(color: "무지개색")) // 무지개색 응가
doggy(name: "댕댕이")
print(doggy.name) // 댕댕이

하지만 메서드를 호출하는 것 외에 함수 표현으로는 사용할 수 없다.

let function: (String) -> Void = doggy(destination:) // ❌
let function: (String) -> Void = doggy.callAsFunction(destination:) // ✅

📌 타입 메서드

  • 메서드에도 인스턴스 메서드와 타입 메서드가 있다
  • 타입 자체에 호출이 가능한 메서드가 타입메서드, 메서드 앞에 static 키워드를 사용하여 나타냄
  • 클래스의 타입 메서드는 static 키워드와 class 키워드를 사용할 수 있음
    • static - 상속 후 메서드 재정의가 불가능
    • class - 상속 후 메서드 재정의 가능
class AClass {
    static func staticTypeMethod() {
        print("AClass staticTypeMethod")
    }
    
    class func classTypeMethod() {
        print("Aclass classtypeMethod")
    }
}

class BClass: AClass {
    /*
    // 오류 발생!! 재정의 불가!
    override static func staticTypeMethod() {
        
    }
    */
    
    override class func classTypeMethod() {
        print("BClass classTypeMethod")
    }
}

AClass.staticTypeMethod() // AClass staticTypeMethod
AClass.classTypeMethod()  // AClass classTypeMethod
BClass.classTypeMethod()  // BClass classTypeMethod
  • 인스턴스 메서드는 self가 인스턴스를 가리킴
  • 타입 메서드의 self는 타입을 가리킴
  • 그래서 타입 메서드에서 self 프로퍼티를 사용하면 타입 프로퍼티 및 타입 메서드를 호출할 수 있다
// 시스템 음량은 한 기기에서 유일한 값이어야 한다.
struct SystemVolume {
    // 타입 프로퍼티를 사용하면 언제나 유일한 값이 된다.
    static var volume: Int = 5
    
    // 타입 프로퍼티를 제어하기 위해 타입 메서드를 사용한다.
    static func mute() {
        // SystemVolume.volume = 0과 같은 표현이다.
        // Self.volume = 0과도 같은 표현이다.
        self.volume = 0
    }
}

// 내비게이션 역할은 여러 인스턴스가 수행할 수 있다.
class Navigation {
    // 내비게이션 인스턴스마다 음량을 따로 설정할 수 있다.
    var volume: Int = 5
    
    // 길 안내 음성 재생
    func guideWay() {
        // 내비게이션 외 다른 재생원 음소거
        SystemVolume.mute()
    }
    
    // 길 안내 음성 종료
    func finishGuideWay() {
        // 기존 재생원 음량 복구
        SystemVolume.volume = self.volume
    }
}

SystemVolume.volume = 10

let myNavi: Navigation = Navigation()

myNavi.guideWay()
print(SystemVolume.volume) // 0

myNavi.finishGuideWay()
print(SystemVolume.volume) // 5

한장 정리

profile
Don't think, just do 🎸

0개의 댓글