The way your question is formulated with the accompanying code is not really about copying but rather about reference vs value semantics.
A shallow or a deep copy is related to types adopting reference semantics (i.e. classes).
In Swift there is the NSCopying
protocol that is usually adopted and conformed to in order to define how a class type shall perform a copy.
For example this class always performs a deep copy of its properties:
final class Bar: NSCopying {
private(set) var value: Int
func increment() {
value += 1
}
init(value: Int = 0) { self.value = value }
func copy(with zone: NSZone? = nil) -> Any {
Self(value: self.value)
}
}
Bar
always performs a deep copy: it returns a new instance initialised with the same value of the original instance property. Such property is of type Int
, which adopts value semantics, hence:
let original = Bar(value: 0)
let clone = original.copy() as! Bar
if original !== clone { print("Different instances") } else { print("Same instance") }
// prints: "Different instances"
clone.increment()
print(original.value)
// prints: 0
print(clone.value)
// prints: 1
What happens if we create another class which has one of its properties of type Bar
? How do we define the way such property is gonna be copied?
Here comes into play shallow vs deep copy strategy:
final class Foo: NSCopying {
let name: String
let bar: Bar
init(name: String, initialValue: Int) {
self.name = name
self.bar = Bar(value: initialValue)
}
private init(name: String, bar: Bar) {
self.name = name
// we just assign the bar reference here…
self.bar = bar
}
func copy(with zone: NSZone?) -> Any {
// …we won't get a deep copy but a shallow copy instead!!!
Self(name: self.name, bar: self.bar)
}
}
In this case we performed a shallow copy of the property bar
. That's cause in the copy method we create a new instance of Foo
, but we initialise it with the same reference to the Bar
instance stored at bar
property:
let original = Foo(name: "George", initalValue: 0)
let clone = original.copy() as! Foo
if original !== clone { print("Different instances") } else { print("Same instance") }
// prints: "Different instances"
if original.bar !== clone.bar { print("Different bar instances") } else { print("Shared bar instance") }
// prints: "Shared bar instance"
original.bar.increment()
print(original.bar.value)
// prints: 1
print(clone.bar.value)
// prints: 1
As you may see here the clone instance of type Foo
got the value of its bar property also mutated as side effect of mutating the original Foo
instance.
To avoid this behaviour we could have leveraged on Bar
NSCopying
implementation so to obtain a copy of its bar
property:
final class Deep: NSCopying {
let name: String
let bar: Bar
init(name: String, initialValue: Int) {
self.name = name
self.bar = Bar(value: initialValue)
}
private init(name: String, bar: Bar) {
self.name = name
// We are assigning a copy of the bar parameter…
self.bar = bar.copy() as! Bar
}
func copy(with zone: NSZone?) -> Any {
// …Therefore we really perform a deep copy!
Self(name: self.name, bar: self.bar)
}
}
Now if we were to mutate a copy, we wouldn't get the side effect of also mutating the original and vice-versa:
let original = Deep(name: "George", initialValue: 0)
let clone = original.copy() as! Deep
clone.bar.increment()
print(original.bar.value)
// prints: 0
print(clone.bar.value)
// prints: 1
Here if Bar
was immutable, it didn't matter if we made a shallow copy of a property of this type, because there wouldn't be side effects: indeed it would have been better for the memory footprint of our application.
On the other hand, since Bar
was implemented as a mutable type, then we had to take into account possible side effects of its mutability in another reference having an internal property of this type.