Skip to content

Instantly share code, notes, and snippets.

@okstring
Created August 24, 2021 12:55
Show Gist options
  • Save okstring/62a74299e32b7102c250c87b3c8a6ec2 to your computer and use it in GitHub Desktop.
Save okstring/62a74299e32b7102c250c87b3c8a6ec2 to your computer and use it in GitHub Desktop.

Mutating

  • 값 유형을 사용할 때 모든 mutations은 (기본적으로) 작업 중인 값의 로컬 복사본에만 수행되고 실제로 mutations을 수행하는 API는 mutations으로. 명확하게 표시되어야 합니다.

  • Mutating으로 표시된 함수는 포함하는 값 내의 모든 속성을 변경할 수 있습니다. Swift의 structured mutations 개념은 참조 유형이 아니라 값 유형에만 적용되기 때문에 "값"이라는 단어가 여기서 매우 중요합니다.

  • Mutating을 켜야 하는 경우 값 유형의 변경이 어디에서 오는지 더 쉽게. 찾아낼 수 있고 값에 대한 우발적인 변경으로 인해 발생하는 오류를 줄이는 데 도움이 됩니다. 또한 우리는 어떤 메소드가 변경을 할 수 있는지 정확히 알고 있고 어떤 조건/상황에서 돌연변이 메소드가 실행될지 알기 때문에 값에 대해 추론하고 예측하는 것이 여전히 상대적으로 간단합니다.

  • 단, 값 내에서 참조유형을 사용하면 쓰레드 세이프를 보장하지 못함

여러 사용 예시

속성을 수정하는 것 외에도 컨텍스트를 변경하면 self에 완전히 새로운 값을 할당할 수 있습니다. 이는 enum에 변경 메서드를 추가할 때 정말 유용할 수 있습니다. 예를 들어, 여기에서 하나의 작업을 다른 작업에 쉽게 추가할 수 있도록 API를 만들고 있습니다.

enum Operation {
    case add(Item)
    case remove(Item)
    case update(Item)
    case group([Operation])
}

extension Operation {
    mutating func append(_ operation: Operation) {
        self = .group([self, operation])
    }
}

기본 속성 세트로 다시 재설정하려는 경우

struct Canvas {
    var backgroundColor: Color?
    var foregroundColor: Color?
    var shapes = [Shape]()
    var images = [Image]()

    mutating func reset() {
        self = Canvas()
    }
}

재설정하려는 값의 유형을 몰라도 아래와 같은 코드가 가능하다

protocol Resettable {
    init()
    mutating func reset()
}

extension Resettable {
    mutating func reset() {
        self = Self()
    }
}

struct Canvas: Resettable {
    var backgroundColor: Color?
    var foregroundColor: Color?
    var shapes = [Shape]()
    var images = [Image]()
}

이니셜라이저는 유형의 속성에 초기 값을 할당하는 것 외에도 작업을 수행하기 위해 변형 메서드를 호출할 수도 있습니다(self가 사전에 완전히 초기화된 경우).

struct ProductGroup {
    var name: String
    private(set) var products = [Product]()
    private(set) var totalPrice = 0
    
    init(name: String, products: [Product]) {
        self.name = name // 초기화 하고 나서
        products.forEach { add($0) }
    }

    mutating func add(_ product: Product) {
        products.append(product)
        totalPrice += product.price
    }
}

동작

  • mutating을 선언한 메서드는 메서드 내에서 property를 변경할 수 있고, 메서드가 종료될 때 변경한 모든 내용을 원래 struct에 다시 기록한다.
  • swift 벨류 타입의 핵심은 불변성이라서 그걸 통채로 갈아 엎습니다. 그렇게 되면 다중 쓰레드에서 세이프하게 작동할 수 있습니다. (?????)

COW

  • 표준 라이브러리의 모든 가변 크기 collection과 마찬가지로 배열은 copy-on-write(COW) 최적화를 사용한다.
  • Array의 여러 복사본은 복사본 중 하나를 수정할 때까지 동일한 저장소를 공유한다.
  • 수정되는 Array는 원본의 복사본으로 교체한 다음 제자리에서 수정된다.
  • 복사량을 줄이기 위해 최적화가 적용되는 경우가 있다.
  • 즉, Array가 다른 복사본과 저장소를 공유하는 경우 해당 Array의 첫번째 mutating으로 Array 복사가 발생한다.
var numbers = [1, 2, 3, 4, 5]
var firstCopy = numbers
var secondCopy = numbers

// The storage for 'numbers' is copied here
numbers[0] = 100
numbers[1] = 200
numbers[2] = 300
// 'numbers' is [100, 200, 300, 4, 5]
// 'firstCopy' and 'secondCopy' are [1, 2, 3, 4, 5]

위의 경우에서도 numbers, firstCopy, secondCopy가 처음에는 같은 저장소를 공유하고 있다. numbers의 값이 바뀔 때, 저장소가 복사되고 numbers는 복사된 저장소를 가지고 있게 된다.

그렇다면 mutating이란?

위에서 살펴본 것처럼 Swift는 값이 변경될 때 복사를 하는 COW로 최적화를 진행한다. structproperty 값이 변경될 때에는 Swift가 직관적으로 알 수 있다. (이는 let, var 키워드를 통해 컴파일러가 이미 알고 있다.) mutating 키워드 없이 메서드가 property를 변경하게 되면 언제 실제로 복사를 해야하는지 알 수가 없다. 따라서, mutating 키워드는 해당 메서드가 호출된다면 실제 복사를 해야한다고 알려주는 역할이다.

let queue = DispatchQueue.global(qos: .default)

class Bird {}
var single = Bird()

queue.async {
  while true { single = Bird() }
}
while true { single = Bird() }

https://velog.io/@wonhee010/mutating

https://jercy.tistory.com/2

https://www.swiftbysundell.com/articles/mutating-and-nonmutating-swift-contexts/

https://medium.com/commencis/stop-using-structs-e1be9a86376f

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment