1

In Swift, the Struct is a value type, while the Class is a reference type. So, two different variables cannot point to the same underlying struct instance, while they can point to the same instance of a class. This means that making a "shallow copy" of a class instance by simply assigning it to a new variable means that changing the instance via the second variable is reflected when accessing the same instance via the first variable. This is exactly what you'd expect, and works the same as in other programming languages I'm familiar with (like the Object in JavaScript).

But, since Struct is not a reference type, you'd also expect that there would be no such thing as a "shallow copy" of a struct instance - every time you assign a new variable to a struct, you should get a separate, new instance with the same value. Changing one of a struct's properties, unlike for an instance of a class, discards the original struct and replaces it with a new one, since structs are also immutable. Presumably, this means that every time you copy or change a struct, a new instance is created at that time. When a new instance of the struct is created, I would expect the init method to run. This doesn't actually happen, though!

In the code sample below, I copy the same kind of struct both with the "shallow copy" method and with the "deep copy" technique which I would use on instances of a class. My belief at the time of writing the code was that the result of both methods of copying a struct would be exactly the same. However, only the "deep copy" method causes the struct's init method to run, while the "shallow copy" does not. This is evident from inspecting the length of the static array property inside the struct after both attempts at copying instances. The length increases after making "deep copies," but not after making shallow copies!

Making changes to the "shallow copy" still does not cause the changes to be reflected in the original struct-containing variable though. This does reflect what I would expect to happen based on structs being an immutable value type.

So, my question is: How exactly are new structs created when an existing struct is copied (vs. when the constructor is explicitly called), and why doesn't it cause the init method to run?

MCVE demonstrating the unexpected outcome:

import Foundation

struct TestStruct {
    var name: String
    var value: Int

    init(name: String, value: Int) {
        self.name = name
        self.value = value
        TestStruct.allInstances.append(self)
    }
    
    static var allInstances: [TestStruct] = []
}

and

import UIKit

class ViewController: UIViewController {
    
    var structs = [TestStruct(name: "First", value: 1), TestStruct(name: "Second", value: 2)]
    
    override func viewDidLoad() {
        super.viewDidLoad()
        print("We started with \(TestStruct.allInstances.count) instances in the static array")
        
        var structsCopy: [TestStruct] = []
        for s in structs {
            structsCopy.append(s)
        }
        print("Now there are \(TestStruct.allInstances.count) instances in the static array")
        
        var structsDeepCopy: [TestStruct] = []
        for s in structs {
            structsDeepCopy.append(TestStruct(name: s.name, value: s.value))
        }
        print("Now there are \(TestStruct.allInstances.count) instances in the static array")
        
        print("But, structsCopy isn't a shallow copy, because changing it doesn't change the original array:")
        structsCopy[0].name = "NEW NAME"
        print("structs[0] still has .name \(structs[0].name), while structsCopy[0].name is now \(structsCopy[0].name)")
        
        print("After making changes to a struct in the first copy, there are still just \(TestStruct.allInstances.count) instances in the static array, not 5")
    }
}

1 Answer 1

1

"Copying" a struct, by assigning it to a variable or passing it to a function, doesn't call a user-written init. Instead, it copies each member individually, which can be a literal copy (e.g. for the Int-typed property) or a retain operation (e.g. for the String-typed property).

That kind of "allInstances" list only makes sense for reference types such as classes. For value types, such as structs, it's impossible for that array to contain the same value as the function you created it from -- you're copying self to pass it to TestStruct.allInstances.append(self). (Because of this, copying can't call a user-written initializer in the general case. It's very easy to accidentally end up with infinite recursion.) If you need an allInstances list like this, you must use a reference type -- and, to prevent memory leaks, it should probably be a list of weak references rather than a literal [MyClass].

If you want value semantics -- you can't mutate a reference someone else holds -- while still being a reference type to support the allInstances list, you need to make it an immutable class like NSString or NSArray (but not NSMutableString or NSMutableArray).

1
  • Thank you for your excellent answer: that was exactly what I wanted to know! If you're wondering, the question only came up in the context of writing a type-agnostic function that works with either classes or structs - otherwise I would never write something like the initializer in my example. If you're curious, here is the post that inspired this question: stackoverflow.com/questions/76262624/… Commented Jun 28, 2023 at 21:43

Not the answer you're looking for? Browse other questions tagged or ask your own question.