Swift: Mutating
사전지식
기본적으로 Swift의 type은 value type(값 타입), reference type(참조 타입)으로 두 종류로 나뉩니다.
값 타입은 변수나 상수에 할당되거나 혹은 함수에 전달될 때 값이 복사되는 유형입니다.
구조체와 열거형이 포함되고 숫자, 문자, 문자열과 같은 데이터 타입은 Swift 표준 라이브러리에서 구조체를 사용하여 정의 및 구현됩니다.
참조 타입은 값 타입과 달리 변수나 상수에 할당되거나 혹은 함수에 전달될 때 복사되지 않아요.
대신 동일한 기존 인스턴스에 대한 참조가 사용됩니다.
Mutating
기본적으로 값 타입의 속성은 immutable(불변적)으로 인스턴스 메서드 내에서 수정할 수 없어요.
하지만 특정 메서드 안에서 구조체나 열거형의 속성을 수정해야 하는 경우, 해당 메서드에 mutating 키워드를 추가함으로써 메서드가 메서드 내에서 mutable(가변적)하게, 즉 변경 가능하게 만들어 줄 수 있습니다.
메서드의 변경 사항은 메서드가 끝날 때 원래 구조체로 다시 쓰입니다.
또한 self 속성을 통해 새로운 인스턴스를 할당할 수 있으며, 새로운 인스턴스는 메서드가 끝날 때 기존의 인스턴스를 대체하게 합니다.
아래 예는 mutating 메서드 moveBy를 통해 위치를 이동하는 함수입니다.
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))")
// The point is now at (3.0, 4.0)
let fixedPoint = Point(x: 3.0, y: 3.0)
fixedPoint.moveBy(x: 2.0, y: 3.0)
// Cannot use mutating member on immutable value: 'fixedPoint' is a 'let' constant
하지만 상수 타입의 구조체에서는 메서드를 mutating 하게 만들 수 없어요.
이는 구조체가 값 타입이기 때문으로, 값 타입의 인스턴스가 상수로 표시된다면 모든 속성들 또한 변수가 아닌 상수로 표시되기 때문입니다.
추가로 참조 타입인 클래스는 해당되지 않고, 상수로 인스턴스를 할당하여도 해당 인스턴스의 변수 속성을 변경할 수 있어요.
Mutating self
mutating func는 암시적 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 = .high
ovenLight.next()
// ovenLight = .off
Mutating extension
extension 또한 mutating을 통해 인스턴스 메서드를 mutable 하게 변경할 수 있습니다.
extension Int {
mutating func square() {
self = self * self
}
}
var someInt = 3
someInt.square()
// someInt = 9
Memory Safety
mutating에 대한 이야기를 하기 앞서 기본적으로 스위프트는 코드에서 안전하지 않은 행동이 발생하는 것을 방지해요.
예를 들어 스위프트는 변수가 사용되기 전에 초기화되고, 할당이 해제된 후 메모리에 접근하지 않으며, 배열 인덱스가 범위를 벗어나는지 오류를 확인하도록 해요.
스위프트는 또한 메모리의 위치를 수정하는 코드를 요구해서 동일한 메모리 영역에 대한 여러 접근이 충돌하지 않도록 해요.
메모리를 자동으로 관리하기 때문에, 대부분의 경우 메모리에 액세스 하는 것에 대해 생각할 필요가 없는 이유죠.
하지만 잠재적인 충돌이 발생할 수 있는 것을 이해하는 것이 중요합니다.
메모리에 대한 접근이 상충되는 코드를 작성한다면 충돌이 발생할 것이고 컴파일 시간 또는 런타임 오류가 발생하겠죠.
충돌하는 접근에 대해 고려해야 할 메모리 접근의 세 가지 특성이 있어요.
- 메모리 액세스가 읽기인지 쓰기인지
- 메모리 액세스 기간
- 메모리 액세스되는 메모리의 위치
특히 액세스 권한이 있는 두 개의 코드가 다음 조건을 모두 충족하는 경우 충돌이 발생합니다.
- 하나 이상 write 접근 또는 nonatomic 접근을 보유
- 같은 위치의 메모리에 접근하는 경우
- 겹치는 액세스 기간을 갖는 경우 (주로 겹치는 액세스 기간을 갖는 경우는 inout 매개변수 또는 mutating 메서드를 사용하는 코드)
Mutating과 충돌
구조체의 mutating 메서드는 메서드 호출 기간 동안 스스로에 대한 쓰기 권한을 가지고 있어요.
예를 들어, 각 플레이어가 피해를 받을 때 감소하는 health, 특수 능력을 사용할 때 감소하는 energy를 가진 게임이 있을 때 restoreHealth() 메서드에서 쓰기 권한은 메서드의 시작부터 메서드가 반환될 때까지 지속됩니다.
struct Player {
var name: String
var health: Int
var energy: Int
static let maxHealth = 10
mutating func restoreHealth() {
health = Player.maxHealth
}
}
지금 같은 경우에는 resotreHealth() 내부에 Player 인스턴스의 속성에 겹치는 다른 코드가 없죠.
하지만 아래의 shareHealth(with:) 메서드가 추가될 경우 다른 Player 인스턴스를 inout 매개 변수로 사용해서 액세스가 겹칠 수 있는 가능성이 생깁니다.
extension Player {
mutating func shareHealth(with teammate: inout Player) {
balance(&teammate.health, &health)
}
}
var oscar = Player(name: "Oscar", health: 10, energy: 10)
var maria = Player(name: "Maria", health: 5, energy: 10)
oscar.shareHealth(with: &maria) // OK
위 코드에서 oscar가 maria의 health를 공유할 수 있도록 shareHealth(with:) 메서드를 호출하는 것은 충돌을 일으키지 않아요.
oscar는 mutating 메서드에서 스스로의 값이기 때문에 메서드 호출 중에 oscar에 대한 쓰기 권한이 있고, maria에게 inout 매개 변수로 전달되었기 때문에 같은 기간 동안 maria 또한 쓰기 권한이 생깁니다.
oscar와 maria는 같은 기간 동안 maria의 health에 대한 쓰기 권한이 있지만 아직까지는 서로 다른 위치의 메모리에서 접근하기 때문에 두 쓰기 권한이 겹치더라도 충돌이 발생하지는 않아요.
하지만 만약 oscar가 shareHealth(with:)를 호출하게 된다면 충돌이 발생합니다.
oscar.shareHealth(with: &oscar)
mutating 메서드는 메서드가 호출된 동안 스스로에 대한 쓰기 권한이 필요하고, inout 매개 변수는 같은 기간 동안 teammate에게 쓰기 권한이 필요하죠.
이 메서드 내에서, 본인과 teammate 모두 위 그림과 같이 메모리의 동일한 위치를 참조하고, 두 쓰기 권한은 겹치는 메모리 참조로 인해 충돌이 발생합니다.
정리
Swift의 구조체는 memory safety를 위해서 기본적으로 immutable 해요.
mutating 메서드는 이를 mutable 하게 만들어주고, 호출의 결과로 스스로 새로운 인스턴스를 할당할 수 있게 해주는 키워드입니다.
참고
Stored Properties of Constant Structure Instances