[Discussion] Change my mind: using `class` in Swift may be a bad idea

Edit: the "stack vs heap" I mentioned in the following is actually "value semantics vs "reference semantics".

class vs. struct

In Swift, we use the keyword class to indicate the instance of this class type we define is allocated on the heap. We use the keyword struct to define an object (in this post, I extend the meaning of the term object) which is allocated on the stack.

It seems to be a concise solution for the Swift memory model. Programmers don’t have to deal with pointers explicitly, and everything works well. But I believe that the keyword class actually bring more drawbacks than benefits. I’d like to show the drawbacks of class as the following.

First, class allows mutation by default. For example, the first code snippets shown as follows is valid, and the second one is invalid:

class MyClass {
    var x: Int = 1
}

let instance1 = MyClass()
instance1.x = 2
struct MyStruct {
    var x: Int = 1
}

let instance2 = MyStruct()
instance2.x = 2 // ERROR: Cannot assign to property: 'instance2' is a 'let' constant

It means that even if we declare instance1 with let, we can still modify it. If we use class to define our abstract data structure, we give up the compiler’s protection of mutability. There are no way to prevent users to mutate instance1.

Moreover, struct requires you to add the keyword mutating to the methods that can modify the data structure. However, you can mutate a class through any methods without any additional marks. We know that an immutable data/object is almost always better than a mutable one from the perspective of the software engineering and optimization. The class keyword doesn’t play well with it.

Second, the definition of class requires us to decide whether the object should be allocated on the heap or the stack when defining it instead of using it. But in many situations, we want to postpone this decision until we actually use it. What’s worse, it’s actually not convenient to refactor a class to struct, since struct has some additional features that class doesn’t have (e.g., automatic member-wise constructor generation, mutating keyword…). We may also want two instances with the same structures, but one on the stack and the other on the heap. In this case, we have to write two data structures.

Solution?

Maybe we should use a Rust-like strategy, i.e., using an Arc<T> container to indicate that an object is allocated on the heap, and using Weak<T> to indicate a heap-allocated variable marked as weak. I create a prototype to mimic it: AtomicReferenceCell(It doesn't work actually...)

Of course, in UIKit/AppKit there are many classes, so if we write code for Apple’s platforms, we cannot avoid to use class. But in other situations, we may need to consider another to use struct.

Meet Crusty if you haven't yet.

8 Likes

You can easily enforce immutability on the members of a class…

class MyClass
{
  let x: Int = 1
}

    let instance1 = MyClass()
    
    instance1.x = 2 // Cannot assign to property: 'x' is a 'let' constant

In some situations, we don't want to restrict the mutability of the instance of MyClass when defining it, but want to restrict it when using it.

By your approach, we cannot define a method to mutate MyClass.x.

If you don't define a properly as immutable, then it will always be mutable in use.

I don't understand what you are trying to say here.

Oh and forget all that stack vs heap stuff, it really isn't relevant in Swift as allocation is opaque.

Here's another possibility…

class MyClass
{
  private(set) var x: Int = 1
  
  func doSomethingAndThenSetX(to value: Int)
  {
    // do something
    
    x = value
  }
}

This version makes x readonly but allows a method on MyClass to mutate it

It seems that this approach cannot meet the following use case:

struct MyStruct {
    var x: Int = 1
}

func f1() {
    let instance2 = MyStruct()
    instance2.x = 2 // ERROR!
}

func f2() {
    var instance2 = MyStruct()
    instance2.x = 2
}

In function f1, mutation is forbidden; but in function f2, mutation is allowed. In your code, we can always modify x by doSomethingAndThenSetX

It's not just historical cruft. ObservableObjects have to be classes or actors. Shared mutable state continues to be a better solution than injecting value types and closures to publish their changes.

1 Like

The difference is that f1() creates an immutable instance of MyStruct into a let, whilst f2() creates a mutable instance into a var.

With my code, the class is mutable but the property can be made immutable by defining it as a let.

It's a question of granularity.

If you are interested, you can read my implementation AtomicReferenceCell.

I basically use struct{ class { T }} to implement shared objects. With this implementation, we can have all the benefits struct brings without using class explicitly.

Unfortunately, I don't think most of us will be interested unless you can get it to work with SwiftUI and Core Data. But we were all hoping for a Core Data replacement this year, and didn't get it, so if you want to write one for Apple for next year, that would be excellent!

I'm sorry but I just con't see the benefit and what you are trying to achieve. You seem to be reinventing pointers and dereferencing when most of us are just glad to not have to bother with all that.

Please explain precisely what all this gives over what Swift already provides.

If you are after shared objects, why not just use the Singleton pattern?

final class MyClass
{
  var x: Int = 1
  
  private init() { }
  
  static let shared = MyClass()
}

Sorry for the misunderstanding, here by shared objects I just want to express the object that is allocated on the heap. So singleton pattern doesn't help.

It is many years since I last used heap vs stack allocation but you are making incorrect assumptions about their use in Swift.

A struct is not necessarily allocated on the stack. The "pointer" to the contents of the struct might be held on the stack but the bulk of its contents, e.g. a string, is allocated on the heap and may change dynamically.

1 Like

The Swift memory model does not forbid stack-allocated class instances, though I don't believe the compiler will do this now. As Joanna_Carter already wrote, structs may be allocated on the heap today.

You should never choose struct over class because of where you think it will be allocated. Use classes when you need reference semantics, and use structs for everything else.

3 Likes

I believe that I conflate two concepts: "stack vs heap" and "value semantics vs reference semantics" here.

What I really want to convey is that class is not the best choice to express reference semantics, since it sacrifices the control of mutability. The alternative way is to use something like struct { class {T }} as a wrapper to regain the control of mutability.

cc @Avi

I don't think there's any way around it. The language is being entirely consistent, but you get different outcomes due to the difference between value types and references.

class C { }

let c = C()

The variable c is immutable. The thing it references is mutable.

struct S {}

let s = S()

The variable s is immutable. It doesn't reference anything, therefore there is nothing that can be mutable.

In both cases, it's the variable which is immutable. The difference comes in what is contained in that variable. For a reference type, it's the reference itself, AKA a pointer(-like thingy). That remains immutable. For a value type, the bytes which make up the value are stored directly in the variable, so these bytes are immutable if the variable is.

In any event, your work-around doesn't work.

class C { var x = 0 }
struct S { let c = C() }

let s = S()
s.c.x = 34 // this is legal, even though s itself is immutable
3 Likes

:thinking:my workaround doesn't work, then are there any other solutions?

What exactly do you want to achieve?

Mutability of properties on a class, as in all code, is controlled by whether a property is declared as a let (immutable) or a var (mutable) - unless the "thing" held in a let is a reference type, in which case the contents of a reference type remain mutable.

1 Like