소피it블로그

[Swift 공식문서] Methods 정리 본문

개발_iOS/스위프트

[Swift 공식문서] Methods 정리

sophie_l 2022. 6. 5. 20:58

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

 

Methods — The Swift Programming Language (Swift 5.6)

Methods Methods are functions that are associated with a particular type. Classes, structures, and enumerations can all define instance methods, which encapsulate specific tasks and functionality for working with an instance of a given type. Classes, struc

docs.swift.org

메서드는 특정 타입과 연관된 함수이다. 클래스, 구조체, 에뉴머레이션은 모두 인스턴스 메서드를 정의할 수 있는데, 이는 해당 타입의 인스턴스에서 사용할 수 있는 특정 태스크와 기능을 포함한다. 클래스, 구조체, 그리고 에뉴머레이션은 타입 메서드 또한 정의할 수 있는데 이는 타입 그 자체와 관련된 메서드이다. 타입 메서드는 옵젝티브-C에서의 클래스 메서드와 비슷하다.

구조체와 에뉴머레이션이 메서드를 정의할 수 있다는 점은 C나 옵젝티브-C와 스위프트의 큰 차이점이다. 스위프트에서는 클래스, 구조체, 에뉴머레이션 중 무엇을 고르든 선택한 타입에 유연하게 메서드를 정의해줄 수 있다.

 

1. 인스턴스 메서드

 

인스턴스 메서드는 특정 클래스, 구조체, 또는 에뉴머레이션의 인스턴스에 속하는 함수이다. 이는인스턴스 프라퍼티에 접근하고 수정할 수 있게 하는 방법을 제공하거나, 인스턴스의 목적과 연관된 기능을 제공함으로써 해당 인스턴스들의 기능을 뒷받침해준다. 인스턴스 메서드는 함수와 동일한 문법을 갖는다.

인스턴스 메서드는 해당 타입의 중괄호 안에 적어준다. 인스턴스 메서드는 해당 타입의 다른 인스턴스 메서드와 프라퍼티들에 암시적으로 접근할 수 있다. 인스턴스 메서드는 자신이 해당하는 타입의 특정한 인스턴스에서만 호출될 수 있다. 인스턴스 없이 단독으로는 호출될 수 없다.

이하는 액션이 일어나는 횟수를 세는 데 사용하는 Counter 클래스를 정의하는 예시이다.

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

카운터 클래스는 세 가지 인스턴스 메서드를 제공한다.

  • increment()는 카운터를 1씩 증가시킨다.
  • increment(by: Int)는 카운터를 특정 정수값만큼 증가시킨다.
  • reset()은 카운터를 0으로 리셋한다.

Counter 클래스는 또한 현재의 카운터 값을 기록하기 위해 변수 프라퍼티인 count를 정의한다.

인스턴스 메서드는 프라퍼티처럼 도트 구문으로 호출할 수 있다.

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

함수의 매개변수는 함수 바디 안에서 사용할 매개변수명과 함수를 부를 때 사용할 인수 라벨을 가질 수 있다. 메서드 매개변수 역시 마찬가지인데, 이는 메서드가 타입과 연관만 되었을 뿐이지 함수이기 때문이다.

 

(1) self 프라퍼티

 

타입의 모든 인스턴스는 self라고 불리는 내재적인 프라퍼티를 갖는데, 이는 인스턴스 그 자체와 동등한 것이다. self 프라퍼티는 현재의 인스턴스를 인스턴스 메서드 안에서 부를 때 사용한다.

위의 예시의 increment() 메서드는 이렇게 써줄 수도 있다.

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

실전에서는 코드에 self를 자주 써줄 필요가 없다. self를 겉으로 써주지 않아도 메서드 안에서 알려진 프라퍼티나 메서드명을 사용할 경우 Swift는 현재의 인스턴스의 프라퍼티나 메서드를 언급하고 있다고 추정한다. 위의 예시에서 Counter의 세 가지 인스턴스 메서드에서 self.count 대신 count를 써줬다는 점으로 확인해볼 수 있다.

이 규칙에 대한 주된 예외는 인스턴스 메서드의 매개변수명이 해당 인스턴스의 프라퍼티와 같을 경우에 발생한다. 이런 상황에서는 매개변수명이 우선순위가 되며 프라퍼티를 더 제대로 된 이름으로 불러줘야만 한다. 매개변수명과 프라퍼티명을 구별해주기 위해 self 프라퍼티를 사용해야 한다.

이 예시에서 self는 x라고 불리는 메서드 매개변수와 똑같이 x라고 불리는 인스턴스 프라퍼티를 구별하게 해준다.

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"

접두어 self가 없다면 스위프트는 x가 매개변수 x를 지칭하는 것이라고 추정할 것이다.

 

(2) Modifying Value Types from Within Instance Methods

 

구조체와 에뉴머레이션은 밸류 타입이다. 밸류 타입의 프라퍼티는 인스턴스 메서드 안에서 변경되지 못하는 것이 디폴트이다.

그러나 구조체나 에뉴머레이션의 프라퍼티를 특정 메서드 안에서 수정해줄 필요가 있는 경우 해당 메서드에 mutating을 사용해줄 수 있다. 그렇게 되면 해당 메서드는 메서드 내부에서 프라퍼티를 mutate 즉 변경할 수 있게 되고, 변화가 생기면 메서드가 끝난 후 원래의 구조체에 덧씌워진다. 메서드는 또한 내재적인 self 프라퍼티에 완전히 새로운 인스턴스를 할당해줄 수 있다. 이 새로운 인스턴스는 메서드가 종료되면 원래 존재하던 것을 대체할 것이다.

mutating 키워드를 메서드의 func 키워드 앞에 적어줌으로써 해당 기능을 사용할 수 있다.

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

Point 구조체는 mutating 메서드인 moveBy(x:y:)를 정의하는데, 이는 Point 인스턴스를 일정량 움직인다. 새로운 포인트로 돌아가는 대신 이 메서드를 사용하면 호출된 포인트를 수정할 수 있다. mutating 키워드는 프라퍼티를 수정할 수 있게 하기 위해 정의에 추가되었다.

구조체 타입의 상수에는 mutating 메서드를 호출할 수 없다. 상수 프라퍼티는 변경될 수 없기 때문이다.

let fixedPoint = Point(x: 3.0, y: 3.0)
fixedPoint.moveBy(x: 2.0, y: 3.0)
// this will report an error

 

(3) Mutating 메서드 안에서 self에 할당하기

 

mutating 메서드는 내재된 self 프라퍼티에 완전히 새로운 인스턴스를 할당할 수 있다. 위의 Point 예시는 다음과 같이 써줄 수도 있다.

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

mutating 메서드 moveBy(x:y:)의 위 버전은 x와 y값이 타겟 위치로 설정된 새로운 구조체를 생성한다. 이 대체 메서드를 호출하면 이전 버전을 호출했을 때와 완전히 같은 결과가 나온다.

에뉴머레이션의 mutating 메서드는 같은 이넘에서 다른 케이스로 내재된 self 매개변수를 설정해줄 수 있다.

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

이 예시는 3개의 상태로 이루어진 switch문을 갖는 에뉴머레이션을 정의한다. 이 스위치는 next() 메서드가 호출될 때마다 off, low, high라는 세 가지 파워 상태를 순환한다.

 

2. 타입 메서드

 

위에 묘사된 바와 같이 인스턴스 메서드는 특정 타입의 인스턴스에서 호출할 수 있는 메서드이다. 한편, 타입 그 자체에 대해 호출할 수 있는 메서드를 정의할 수도 있다. 이런 메서드를 타입 메서드라고 한다. 타입 메서드는 static 키워드를 메서드의 func 키워드 앞에 적어줌으로써 만들 수 있다. 클래스는 class키워드를 대신해서 사용함으로써 하위 클래스들이 상위 클래스의 메서드 구현을 오버라이딩하게 해줄 수 있다.

타입 메서드는 인스턴스 메서드와 같이 도트 구문에 의해 호출된다. 그러나 타입 메서드는 해당 타입의 인스턴스가 아니라 타입 그 자체에다 호출한다는 차이가 있다. SomeClass라는 클래스의 타입 메서드를 부르는 예시를 보자.

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

타입 메서드의 바디 안에 내재된 self 프라퍼티는 타입의 인스턴스가 아닌 타입 그 자체를 가리키는 것이다. 이가 의미하는 바는 인스턴스 프라퍼티와 인스턴스 메서드 매개변수에서와 같이 타입 프라퍼티와 타입 메서드 매개변수에 대해서도 서로를 구분하기 위해 self를 사용해줄 수 있다는 것이다. 

더 일반적으로 말하자면 타입 메서드의 바디 안에서 사용하는 메서드나 프라퍼티 이름 중 적절하지 않은 것은 다른 타입수준의 메서드와 프라퍼티를 나타낸다는 것이다. 타입 메서드는 타입 이름을 앞에 고정할 필요 없이 다른 메서드의 이름만으로 통해 해당 타입 메서드를 호출할 수 있다. 마찬가지로, 구조체나 에뉴머레이션의 타입 메서드도 타입 이름 접두어 없이 타입 프라퍼티의 이름만을 통해 타입 프라퍼티에 접근할 수 있다.

이하의 예시에서는 LevelTracker라는 구조체를 정의한다. 이는 게임의 서로 다른 레벨에서 플레이어의 진행 상황을 추적한다. 이는 싱글 플레이어 게임이지만 하나의 기기에 여러 명의 플레이어의 정보를 저장할 수 있다.

레벨1을 제외한 게임의 모든 레벨은 게임을 처음 플레이할 때는 잠겨있다. 플레이어가 한 단계를 끝낼 때마다 해당 기기에서는 해당 레벨이 모든 플레이어에게 오픈된다. LevelTracker 구조체는 프라퍼티와 메서드를 통해 게임의 어떤 레벨들이 언락되었는지를 추적한다. 또한 개개의 플레이어에 대해 현재의 레벨이 얼마나 되는지 또한 추적한다.

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

LevelTracker 구조체는 플레이어가 언락한 가장 높은 레벨을 추적한다. 이 값은 highestUnlockedLevel이라는 타입 구조체에 저장된다.

LevelTracker는 highestUnlockedLevel 프라퍼티에 잘 적용되는 두 개의 타입 함수를 정의한다. 첫 번째는 unlock(_:)이라고 불리는 타입 함수인데, 이는 highestUnlockedLevel의 값을 새 레벨이 언락될 때마다 업데이트한다. 두 번째는 isUnlocked(_:)라고 불리는 convenience 타입 함수인데, 이는 특정 레벨이 이미 언락되어 있으면 true를 리턴한다. 

LevelTracker는 타입 프라퍼티와 타입 메서드에 더해 각각의 플레이어의 진행을 추적한다. 이는 currentLevel이라는 인스턴스 프라퍼티를 사용해서 플레이어가 현재 플레이하고 있는 레벨을 추적한다.

currentLevel 프라퍼티를 관리하기 위해서 LevelTracker는 advance(to:)라고 불리는 인스턴스 메서드를 정의한다. currentLevel을 업데이트하기 전에 이 메서드는 요청된 세 레벨이 이미 언락되었는지 확인한다.

advance(to:) 메서드는 currentLevel을 설정할 수 있었는지 없었는지를 나타내는 불리언 값을 리턴한다. advance(to:) 메서드를 호출하는 코드가 리턴 값을 무시하는 것이 꼭 실수는 아니기 때문에 이 함수는 @discardableResult 어트리뷰트로 표시된다.

LevelTracker 구조체는 아래와 같이 Player 클래스와 함께 사용되는데, 이를 통해 각각의 플레이어들의 진행 상황을 추적하고 업데이트 한다.

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

Player 클래스는 플레이어의 진행 상황을 추적하기 위해 LevelTracker의 새로운 인스턴스를 만들어낸다. 또한 complete(level:)이라고 불리는 메서드를 제공하는데, 이는 플레이어가 특정 레벨을 완성할 때마다 호출된다. 이 메서드는 모든 플레이어들에게 다름 레벨을 언락해주고 플레이어의 진행 상황을 다음 레벨로 옮기도록 업데이트해준다.

새 플레이어를 위해 Player 클래스의 인스턴스를 새로 만들어준 후 해당 플레이어가 레벨 1을 끝내면 어떤 일이 일어나는지 볼 수 있다.

var player = Player(name: "Argyrios")
player.complete(level: 1)
print("highest unlocked level is now \(LevelTracker.highestUnlockedLevel)")
// Prints "highest unlocked level is now 2"

만약 게임에서 그 어느 플레이어에 의해서도 언락되지 않은 레벨로 옮기기 위해 두 번째 플레이어를 새로 생성해주면 플레이어의 현재 레벨을 설정하려는 시도는 실패한다.

player = Player(name: "Beto")
if player.tracker.advance(to: 6) {
    print("player is now on level 6")
} else {
    print("level 6 hasn't yet been unlocked")
}
// Prints "level 6 hasn't yet been unlocked"