Swift 정리 - 10. 프로퍼티와 메서드


Swift의 프로퍼티와 메서드에 대해 알아보자.


Swift.org:Properties Swift.org:Methods

  • property : 클래스, 구조체, 열거형과 값들을 연관 짓는다.
  • method : 특정 타입에 관련된 함수

Properties

프로퍼티는 크게 세 가지로 나뉜다.

  1. Stored Properties, 저장 프로퍼티 : 인스턴스의 일부로, 상수와 변수로 값을 저장한다. 클래스, 구조체에서만 사용할 수 있다.
  2. Computed Properties, 연산 프로퍼티 : 저장하지 않고 값을 계산한다. 클래스, 구조체, 열거형에서 모두 사용할 수 있다.
  3. Type Properties, 타입 프로퍼티 : 타입 자체와 연관되는 프로퍼티

추가로 프로퍼티의 값이 변하는지를 감시하는 property observer를 정의할 수도 있다. 프로퍼티 감시자는 저장 프로퍼티에 추가할 수 있고, 부모 클래스에서 상속받을 수 있다.

또한 여러 프로퍼티들의 getter와 setter 내에 있는 코드를 재사용하기 위해 property wrapper를 사용할 수도 있다.

Stored Properties

저장 프로퍼티는 특정 클래스나 구조체의 인스턴스의 일부로서 저장된 상수나 변수를 의미한다. 저장 프로퍼티는 상수, 변수 모두 가능하다.

저장 프로퍼티를 정의할 때 기본값, 초깃값을 지정할 수 있다. 연산 프로퍼티도 마찬가지다.

struct FixedLengthRange {
    var firstValue: Int = 1
    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

// 초깃값이 할당된 저장프로퍼티에 전달인자로 초깃값을 넘기지 않아도 된다.
var anotherRange = FixedLengthRange(length: 3)

구조체는 memberwise 이니셜라이저를 자동으로 생성하기 때문에 이를 사용해서 인스턴스를 생성하는데, 클래스는 그렇지 않기 때문에 초기화할 때 저장 프로퍼티에 값을 할당하기가 번거롭다. 클래스의 저장 프로퍼티에 초깃값을 지정하면 따로 사용자 정의 이니셜라이저를 구현할 필요가 없다.

class Cat {
    var name: String = "cat" // stored property
    var age: Int = 1 // stored property
}

// 초깃값을 모든 저장 프로퍼티에 지정해줬기 때문에 사용자 정의 이니셜라이저를 사용하지 않아도 된다.
var oneCat = Cat()

위에서 봤듯이 초깃값을 지정하니 이니셜라이저를 작성할 때 훨씬 간편해졌다. 하지만 초깃값이 아니라 다른 값으로 초기화하고 싶은 경우에는 이런 방법이 좋지 않을 수 있다.

인스턴스를 생성할 때 이니셜라이저를 통해 초깃값을 보내야 하는 이유는 저장 프로퍼티 타입이 옵셔널이 아니기 때문이다. 따라서 인스턴스의 저장 프로퍼티는 인스턴스가 생성될 때 값이 꼭 있는 상태여야 한다. 반면 저장 프로퍼티 타입이 옵셔널로 설정되어 있다면 값이 없어도 된다는 것을 의미하기 때문에 초깃값을 넣어주지 않아도 된다.

class Cat {
    var name: String // stored property
    var age: Int? // stored property
    
    // age는 옵셔널이기 때문에 이니셜라이저에서 초깃값을 전달하지 ㅇ낳아도 된다.
    init(name: String) {
        self.name = name
    }
}

var oneCat = Cat(name: "cat")

Lazy Stored Properties

Lazy stored property는 프로퍼티가 처음 사용될 때까지 초깃값을 설정하지 않는 프로퍼티다. 선언할 때 lazy 키워드를 사용한다.

Lazy 프로퍼티는 프로퍼티의 초깃값이 인스턴스의 초기화가 완전히 끝날 때까지 알려지지 않은 값들과 같은 외부의 요소들에 의존하는 경우 유용하다. 또한 프로퍼티의 초깃값이 한 번 설정하기 굉장히 어렵고 많은 연산이 요구되는 경우에 필요하지 않을 때까지 초깃값을 계산하고 싶지 않은 경우에도 유용하다. 즉 lazy 프로퍼티를 사용해서 불필요한 성능저하나 공간 낭비를 줄일 수 있다.

class DataImporter {
    /*
    외부 파일의 데이터를 불러오는 클래스.
    초기화 하는데 오랜 시간이 걸린다는 것을 가정한다.
    */
    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")
// manager의 importer 프로퍼티가 호출되지 않았기 때문에, importer에 할당할 DataImporter 인스턴스는 아직 생성되지 않았다.

// 호출되는 시점에 DataImporter 인스턴스가 생성됐다
print(manager.importer.filename)

멀티쓰레드와 lazy 프로퍼티 멀티 쓰레드 환경에서 lazy 프로퍼티에 동시다발적으로 접근하면 한 번만 초기화 된다는 보장이 없다. 생성되지 않은 lazy 프로퍼티에 많은 쓰레드가 비슷한 시점에 접근하면 여러 번 초기화 도리 수 있다.

Computed Properties

클래스, 구조체, 열거형은 연산 프로퍼티를 정의할 수 있다. 연산 프로퍼티는 실제로 값을 저장하지 않는다. 대신, 다른 프로퍼티와 값들을 간점적으로 접근하고 값을 설정하기 위해 getter와 setter를 제공한다.

  • getter : 인스턴스 내/외부의 값을 연산해 적절한 값을 돌려준다
  • setter : 인스턴스 내부의 프로퍼티 값을 간접적으로 설정한다

저장 프로퍼티가 있는데 굳이 연산 프로퍼티를 사용해야 하는 이유는 무엇일까? 인스턴스 외부에서 메서드를 통해 인스턴스 내부의 은닉화된 값을 접근한다고 생각해보자. 보통 프로퍼티의 값을 가져오는데 메서드 한 개, 프로퍼티 값을 설정하는데 메서드 한 개 이렇게 두 개의 메서드를 사용한다. 이렇게 두 개의 메서드를 사용해도 되지만, 프로퍼티를 통해 값을 설정하는 게 코드를 더 가독성 있게 만들 수 있고 간편하기도 하다.

다만 연산 프로퍼티는 읽기 전용일 수 있지만, 쓰기 전용으로 만들 수는 없다. 메서드는 getter 메서드를 안 만들고 setter 메서드만 만들어서 쓰기 전용으로 만들 수 있는데 연산 프로퍼티는 이런 기능은 지원하지 않는다.

struct Point {
    var x = 0.0, y = 0.0
}
struct Size {
    var width = 0.0, height = 0.0
}
struct Rect {
    // 저장 프로퍼티
    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(newCenter) {
            origin.x = newCenter.x - (size.width / 2)
            origin.y = newCenter.y - (size.height / 2)
        }
    }
}
var square = Rect(origin: Point(x: 0.0, y: 0.0),
                  size: Size(width: 10.0, height: 10.0))
let initialSquareCenter = square.center
// initialSquareCenter is at (5.0, 5.0)
square.center = Point(x: 15.0, y: 15.0)
print("square.origin is now at (\(square.origin.x), \(square.origin.y))")
// Prints "square.origin is now at (10.0, 10.0)"

Shorthand Setter Declaration

만약 연산 프로퍼티 세터가 새로 설정될 값에 대한 이름을 명시하지 않았다면 디폴트로 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)
        }
    }
}

Shorthand Getter Declaration

만약 게터의 바디 전체가 하나의 표현이면, 게터는 해당 표현을 리턴한다.

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

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

Read-Only Computed Properties

연산 프로퍼티 중에 getter는 있는데 setter는 없으면 읽기 전용 연산 프로퍼티다. 읽기 전용 연산 프로퍼티는 말 그대로 값을 리턴하기만 하고 다른 값은 할당할 수 없다.

연산 프로퍼티를 선언할 때 무조건 var 키워드로 프로퍼티를 변수로 선언해야 한다. 왜냐하면 이들의 값은 고정되어 있지 않기 때문이다.

또한 읽기 전용 프로퍼티에서 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

프로퍼티 감시자는 프로퍼티의 값이 변경됐는지를 관찰하고 응답한다. 프로퍼티 감시자는 프로퍼티에 값이 설정될 때마다 호출되는데, 현재 프로퍼티가 가진 값과 똑같은 값으로 설정되어도 호출된다.

프로퍼티 감시자를 추가할 수 있는 곳은 다음과 같다.

  • 내가 정의한 저장 프로퍼티
  • 내가 상속받은 저장 프로퍼티
  • 상속 받은 연산 프로퍼티

상속받은 프로퍼티의 경우에는 것은 서브 클래스에서 해당 프로퍼티에 프로퍼티 감시자를 추가하면 된다. 내가 정의한 연산 프로퍼티의 경우에는 프로퍼티 감시자를 추가하기 보다는 프로퍼티의 세터를 사용하는 것이 좋다.

프로퍼티 감시자에는 두 가지 메서드가 있다.

  • willSet : 값이 저장되기 직전에 호출된다.
  • didSet : 새 값이 설정된 직후에 호출된다.

willSet 감시자를 구현하면 새로 설정될 값이 상수 파라미터로 전달된다. 이 파라미터의 이름을 구현에서 정의할 수 있다. 만약 이름을 따로 명시하지 않았다면 자동으로 newValue라는 이름이 붙는다.

didSet 감시자를 구현하면 프로퍼티의 이전 값이 상수 파라미터로 전달된다. 이 파라미터의 이름은 자동으로 oldValue가 된다.

부모 클래스 프로퍼티의 willSetdidSet 감시자는 부모 클래스 이니셜라이저가 호출된 후, 프로퍼티가 자식 클래스 이니셜라이저로 설정될 때 호출된다. 부모 클래스의 감시자는 부모 클래스 이니셜라이저가 호출되기 전에 클래스가 자신의 프로퍼티를 설정할 때는 호출되지 않는다.

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

만약 감시자가 있는 프로퍼티를 함수에 in-out 파라미터로 전달하면, willSetdidSet 감시자는 항상 호출된다. 이는 in-out 파라미터의 copy-in copy-out 메모리 모델 때문이다. 값은 항상 함수의 끝에서 프로퍼티에 다시 설정된다.

Property Wrappers

프로퍼티 래퍼는 프로퍼티가 어떻게 저장되는지와 코드가 프로퍼티를 어떻게 정의하는지를 관리하는지를 분리하는 레이어를 추가한다. 예를 들어 만약 thread-safety 체크를 하거나 데이터베이스에 데이터를 저장하는 프로퍼티를 제공한다면, 이 코드를 모든 프로퍼티마다 작성해야 할 것이다. 이때 프로퍼티 래퍼를 사용하면 래퍼를 정의할 때 관리 코드를 작성하고 이 관리 코드를 여러 프로퍼티에 재사용해서 적용할 수 있다.

프로퍼티 래퍼를 정의하기 위해 구조체, 열거형, 클래스가 wrappedValue 프로퍼티를 정의하게 한다.

@propertyWrapper
struct TwelveOrLess {
    private var number = 0
    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"

rectangle.height = 10
print(rectangle.height)
// Prints "10"

rectangle.height = 24
print(rectangle.height)
// Prints "12"

Setting Initial Values for Wrapped Properties

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

// 프로퍼티에 래퍼를 적용하고 초깃값을 설정하지 않으면 Swift는 `init()` 이니셜라이저로 래퍼를 세팅한다.
var zeroRectangle = ZeroRectangle()
print(zeroRectangle.height, zeroRectangle.width)
// Prints "0 0"

struct UnitRectangle {
    @SmallNumber var height: Int = 1 // `init(wrappedValue:)` 이니셜라이저로 변환되어 초깃값이 생성된다.
    @SmallNumber var width: Int = 1
}

var unitRectangle = UnitRectangle()
print(unitRectangle.height, unitRectangle.width)
// Prints "1 1"

커스텀 attribute 뒤에 괄호로 인자를 감싸면 Swift는 이 인자로 래퍼를 설정할 수 있는 이니셜라이저를 사용한다.

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"

narrowRectangle.height = 100
narrowRectangle.width = 100
print(narrowRectangle.height, narrowRectangle.width)
// Prints "5 4"

Projecting a Value From a Property Wrapper

프로퍼티 래퍼는 projected value를 정의해서 추가적인 기능을 구현할 수 있다.

@propertyWrapper
struct SmallNumber {
    private var number: Int
    private(set) var projectedValue: Bool

    var wrappedValue: Int {
        get { return number }
        set {
            if newValue > 12 {
                number = 12
                projectedValue = true
            } else {
                number = newValue
                projectedValue = false
            }
        }
    }

    init() {
        self.number = 0
        self.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"

Global and Local Variables

  • 전역 변수 : 함수, 메서드, 클로저, 아니면 타입 context 밖에서 선언된 변수다.
  • 지역 변수 : 함수, 메섣, 클로저 내에서 선언된 변수

지금까지 봤던 예제들에 있는 전역, 지역 변수는 모두 저장 변수다. 저장 변수는 저장 프로퍼티 처럼, 값을 저장하는 역할을 한다.

연산 변수를 정의할 수도 있고, 저장 변수에 감시자를 정의할 수도 있다. 이는 전역과 지역에서 모두 가능하다. 연산 변수는 값을 저장하기 보다는 연산하고, 연산 프로퍼티와 같은 문법으로 작성된다.

전역 변수와 상수는 항상 lazy하게 연산되는데, lazy 저장 프로퍼티와 비슷하다. 즉 전역 변수, 전역 상수는 처음 접근할 때 최초로 연산이 이루어진다. 다른 점은 전역 상수와 변수는 lazy 키워드가 붙을 필요가 없다는 것이다.

프로퍼티 래퍼를 지역 저장 변수에는 할당할 수 있지만, 전역 변수나 연산 변수에는 안된다.

func someFunction() {
    @SmallNumber var myNumber: Int = 0

    myNumber = 10
    // now myNumber is 10

    myNumber = 24
    // now myNumber is 12
}

Type Property

인스턴스 프로퍼티는 특정 타입의 인스턴스에 속하는 프로퍼티다. 인스턴스를 생성할 때마다 다른 인스턴스와 구별되는 자신만의 프로퍼티 값을 가질 수 있다.

타입 자체에 속하는 프로퍼티를 생성할 수도 있다. 이 프로퍼티는 인스턴스를 얼마나 많이 만들었는지와는 상관없이 오로지 하나의 원본만 존재한다. 이런 프로퍼티를 타입 프로퍼티라 한다.

타입 프로퍼티는 모든 인스턴스가 공용으로 접근하고 값을 변경할 수 있는 변수 등을 정의할 때 유용하다. 저장 타입 프로퍼티는 상수나 변수로 모두 생성될 수 있다. 연산 타입 프로퍼티는 일반 연산 프로퍼티와 마찬가지로 변수로만 선언된다.

타입 저장 프로퍼티에는 항상 기본값을 할당해야 한다. 이는 저장 타입 프로퍼티를 초기화할 때 적절한 값을 설정해줄 수 있는 이니셜라이저가 없기 때문이다. 저장 타입 프로퍼티는 lazy 프로퍼티 같이 동작한다. 즉 처음 접근되었을 때가 되서야 초기화가 된다. 얘네들은 여러 쓰레드가 동시다발적으로 접근해도 오직 한 번만 초기화된다.

Type Property Syntax

Swift에서 타입 프로퍼티는 타입을 정의할 때 같이 작성한다. static 키워드로 선언하고, 클래스 타입의 연산 타입 프로퍼티의 경우에는 class 키워드를 대신 써서 자식 클래스가 부모 클래스의 구현을 오버라이드할 수 있게 할 수 있다.

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 SomeClass {
    static var storedTypeProperty = "Some value."
    static var computedTypeProperty: Int {
        return 27
    }
    class var overrideableComputedTypeProperty: Int {
        return 107
    }
}

Querying and Setting Type Properties

타입 프로퍼티는 인스턴스 프로퍼티와 같은 방법으로 쿼리되고 설정된다.

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"

키 경로

함수는 일급 객체로서 상수나 변수에 참조를 할당할 수 있었다.

func sayHi() {
    print("Hi")
}

var functionReference = sayHi()

프로퍼티도 값을 바로 꺼내오지 않고 프로퍼티의 위치만 참조할 수 있다. 바로 keyPath를 사용하는 건데, 키 경로를 사용해서 어떤 타입의 프로퍼티를 참조해야 하는지 미리 지정할 수 있다.

키 경로는 역슬래시와 타입, 마침표 경로로 구성된다.

\타입이름.경로.경로.경로
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.owner)) // WritableKeyPath<Stuff, Person>

자신을 나타내는 키 경로인 \.self를 사용하면 인스턴스 그 자체를 표현하는 키 경로가 된다.

Method

메서드는 특정 타입에 연관된 함수들이다. 클래스, 구조체, 열거형 모두 인스턴스 메서드를 정의할 수 있다. 이 인스턴스 메서드들은 인스턴스의 기능을 캡슐화한다. 클래스, 구조체, 열거형은 타입 자체와 연관된 타입 메서드를 정의할 수도 있다.

인스턴스 메서드

인스턴스 메서드는 특정 클래스, 구조체, 열거형의 인스턴스에 속하는 함수다. 인스턴스와 관련된 기능을 실행하고, 선언은 일반 함수와 똑같은 문법으로 선언하면 된다.

인스턴스 메서드는 특정 인스턴스가 있을 때만 호출될 수 있으므로 인스턴스가 없는 상태에서 단독으로 호출될 수 없다.

class Counter {
    var count = 0
    func increment() {
        count += 1
    }
    func increment(by amount: Int) {
        count += amount
    }
    func reset() {
        count = 0
    }
}

let counter = Counter()
// the initial counter value is 0
counter.increment()
// the counter's value is now 1
counter.increment(by: 5)
// the counter's value is now 6
counter.reset()
// the counter's value is now 0

The self Property

모든 인스턴스는 self라는 프로퍼티를 가지고 있다. 이는 인스턴스 자기 자신을 의미한다. self 프로퍼티를 사용해서 인스턴스 메서드 안에서 현재 인스턴스를 참조할 수 있다.

func increment() {
    self.count += 1
}

보통 함수 내에서 self를 쓰지 않아도 되지만 꼭 써야 하는 경우도 있다. 만약 인스턴스 메서드의 파라미터 이름이 인스턴스의 프로퍼티 이름과 같다면 self를 명시하는 것이 좋다.

struct Point {
    var x = 0.0, y = 0.0
    func isToTheRightOf(x: Double) -> Bool {
        return self.x > x
    }
}
let somePoint = Point(x: 4.0, y: 5.0)
if somePoint.isToTheRightOf(x: 1.0) {
    print("This point is to the right of the line where x == 1.0")
}
// Prints "This point is to the right of the line where x == 1.0"

인스턴스 메서드에서 값 타입 수정하기

구조체와 열거형은 값 타입이다. 기본적으로 값 타입의 프로퍼티는 인스턴스 메서드에서 수정될 수 없다.

하지만 수정하고 싶다면 mutating 키워드를 붙인다. 이 키워드가 붙은 메서드는 메서드에서 값을 바꿀 수 있고, 변경된 값은 메서드가 끝날 때 원래 구조에 덮어씌워진다.

struct Point {
    var x = 0.0, y = 0.0
    mutating func moveBy(x deltaX: Double, y deltaY: Double) {
        x += deltaX
        y += deltaY
    }
}
var somePoint = Point(x: 1.0, y: 1.0)
somePoint.moveBy(x: 2.0, y: 3.0)
print("The point is now at (\(somePoint.x), \(somePoint.y))")
// Prints "The point is now at (3.0, 4.0)"

### Mutating 메서드에서 self에 할당하기

Mutating 메서드는 self 프로퍼티에 새로운 인스턴스를 할당할 수 있다.

 struct Point {
    var x = 0.0, y = 0.0
    mutating func moveBy(x deltaX: Double, y deltaY: Double) {
        self = Point(x: x + deltaX, y: y + deltaY)
    }
}

enum TriStateSwitch {
    case off, low, high
    mutating func next() {
        switch self {
        case .off:
            self = .low
        case .low:
            self = .high
        case .high:
            self = .off
        }
    }
}
var ovenLight = TriStateSwitch.low
ovenLight.next()
// ovenLight is now equal to .high
ovenLight.next()
// ovenLight is now equal to .off

Type Method

위에서 본 인스턴스 메서드는 특정 타입의 인스턴스가 호춣하는 것이다. 타입 자체가 호출할 수 있는 메서드도 있는데, 이런 메서드를 타입 메서드라고 한다. 함수를 선언할 떄 static 키워드를 붙여 정의한다. class 키워드를 사용할 수도 있는데, 이는 부모 클래스에 정의된 해당 메서드를 자식 클래스에서 오버라이딩 할 수 있게 해준다.

class SomeClass {
    class func someTypeMethod() {
        // type method implementation goes here
    }
}
SomeClass.someTypeMethod()

타입 메서드 바디에서 사용되는 self는 해당 타입의 인스턴스가 아니라 타입 자체를 참조한다. 이는 타입 프로퍼티와 타입 메서드 파라미터들을 명확하게 하기 위해 self를 사용할 수 있다는 뜻이다.

struct LevelTracker {
    static var highestUnlockedLevel = 1
    var currentLevel = 1

    static func unlock(_ level: Int) {
        if level > highestUnlockedLevel { highestUnlockedLevel = level }
    }

    static func isUnlocked(_ level: Int) -> Bool {
        return level <= highestUnlockedLevel
    }

    @discardableResult
    mutating func advance(to level: Int) -> Bool {
        if LevelTracker.isUnlocked(level) {
            currentLevel = level
            return true
        } else {
            return false
        }
    }
}

class Player {
    var tracker = LevelTracker()
    let playerName: String
    func complete(level: Int) {
        LevelTracker.unlock(level + 1)
        tracker.advance(to: level + 1)
    }
    init(name: String) {
        playerName = name
    }
}