-2

Having programmed with identity types for years, I find mutating value types very stressful to use due to the constant risk of accidentally assigning (and thus copying) to a new variable and then mutating that copy and expecting to see those changes reflected in the original struct (example given at the end).

My actual question: What approaches/coding styles/practices are there to prevent this sort of thing from happening?


What I'm thinking: I can't imagine the answer to this question simply being "just remember that you're dealing with a struct and not a class", because that's extremely error prone especially since class-ness/struct-ness is entirely opaque to code using any particular data structure.

I also see a lot of talk around using "purely functional style", which I'm all for except:

  • seems very awkward to do in cases where the mutating method also needs to return a value (as in my example below). Of course I could return a tuple like (newStructInstance, returnValue) but that can't be The Way.
  • I imagine that instantiating new copies of structs whenever you want mutations can't be in any way efficient (unlike reassignment of structs which, as I understand it, creates a new instance without incurring any of the costs).

Some possible answers to my question could be:

  • your example is contrived and this sort of thing rarely, if ever, happens in practice
  • you're using the iterator (anti)pattern (see code below), so that's your original sin right there
  • ease up off of mutating and just use use structs for immutable types and classes for mutable ones

Is any of this in the right direction?


Example: Say we have a struct that simply wraps an integer which it increments by 1 with a mutating method next().

struct Counter {
    var i: Int = 0

    mutating func next() -> {
        self.i += 1
        return self.i
    }
}

In some initializer somewhere we instantiate like this

self.propertyWithLongAndAnnoyingName = Counter()

and later in the same scope, having forgotten that this property holds a struct, we assign it to the succinctly named local variable p and increment that.

var p = self.propertyWithLongAndAnnoyingName
d.next() // actually increments a copy
pseudosudo
  • 6,270
  • 9
  • 40
  • 53
  • Do you also experience this problem when dealing with types like integers & floats? – deej May 06 '19 at 16:36

1 Answers1

3

Your question, if it isn't just an elaborate troll, seems more psychological than practical, and as such is probably not a good fit for Stack Overflow: it's a matter of opinion. However, taking you seriously and assuming that the question is sincere, I can tell you as a matter of experience that, as someone who programmed for years in Objective-C and then learned Swift when it was first released to the public, the existence of value type objects such as a structs comes as a huge relief, not as a problem as you seem to posit.

In Swift, a struct is the object type par excellence. When you simply need an object, you use a struct. Using classes is quite exceptional, confined to cases such as the following:

  • You're using Cocoa (it's written in Objective-C and you just have to accept that its objects are classes)

  • You need a superclass / subclass relationship (very rare in pure Swift programming; usually this happens only because you are using Cocoa)

  • The object is to represent external reality where individual identity is crucial (for example, a UIView would need to be a reference type, not a value type, because there really are individual views sitting in the interface and we need to be able to distinguish them)

  • True mutability in place is a desideratum

In general, therefore, most programmers come to feel just the opposite of the state of mind you hypothesize: they are surprised when an object unexpectedly turns out to be a reference type (a class) and mutation affects a remote reference. In the code example that you give:

var p = self.propertyWithLongAndAnnoyingName
p.next()

...it would be a complete shock if self.propertyWithLongAndAnnoyingName were also mutated. Value types represent a form of safety. The assignment to p and mutation of p would be precisely to insulate self.propertyWithLongAndAnnoyingName from being changed.

In fact, Cocoa itself goes to great lengths to guard against this sort of thing. That is why, for example, the pattern is that propertyWithLongAndAnnoyingName would never be (as seen from the outside) an NSMutableString — it would be an NSString, which puts up a fence of immutability around the object even though it is a reference type. Thus the use of structs in Swifts solves a problem which Cocoa has to solve through much more elaborate and (to many beginners) arcane measures.

However, cases like this are relatively rare in actual fact, because another aspect of your hypothesis is also false:

I can't imagine the answer to this question simply being "just remember that you're dealing with a struct and not a class", because that's extremely error prone especially since class-ness/struct-ness is entirely opaque to code using any particular data structure

My own experience is that in any situation where it matters, I always know whether I'm dealing with a struct or a class. Simply, if it's a Swift library object (String, Array, Int, etc.), it's a struct. (The library defines basically no classes at all.) If it's a Cocoa object (UIView, UIViewController), it's a class.

Indeed, the naming convention usually helps you. The Foundation overlay is a case in point. NSData is a class; Data is a struct.

Note too that you cannot mutate a let reference to a struct, but you can mutate a let reference to class. Thus in practice you very quickly experience which is which, because you always use let if you can.

Finally, if it is really important to you hand an object off to some third party for mutation-in-place, that is what inout is for. And in that case, you know it, because you have to pass a reference (address) explicitly. If the parameter you are passing to is not inout, you will assume that your original object is safe from mutation, and you'll be glad of it.

matt
  • 515,959
  • 87
  • 875
  • 1,141
  • You might want to read http://www.apeth.com/swiftBook/ch04.html#SECreferenceTypes if you're having difficulty wrapping your head around the distinction between reference types and value types. – matt Dec 02 '18 at 06:30