I don't think value semantics imply that two values that are equal from the Equatable
perspective are actually the same value. If that was the case though, it'd imply that only types with an Equatable
conformance can be values.
You say this last, but this is the key question. Let me first address the Array note:
I think we all agree that pointers are reference types. Any method on a value type that exposes a pointer necessarily vends you a reference type from that value type. Because of the way Swift uses scoped accessors, these with
functions temporarily transform a value type into a reference type for the duration of their usage. That means that this example is similar in kind to the question of collection indices.
This is a necessary and desirable behaviour of their usage. For example, the small-String optimization makes a pointer to the storage of a String literally unusable outside the scope of the call, meaning that escaping the bit-value of that pointer can't possibly provide any notion of identity that's useful. However, within the scope of the call that identity is absolutely useful. Put another way, all of these with
functions pin a value into a location such that they can expose a reference type derived from a specific value.
So I agree with you: those functions don't prevent Array
from being a value type. But they do that by only being useful as identifiers for the duration of the with
call, where they temporarily expose a reference to a specific instance of the type, changing the semantics.
Let's be clear: generically, collection indices are reference types. Indices are the canonical example of a reference. Indeed, a pointer is simply an index into memory space, and they're the go-to example of a reference type.
There's some mental gymastics to go through here though, because of the fact that the type of many indices is a value type. Array.Index
is Int
, for example, and Int
is unquestionably a value type. However, my argument is that Array.Index
is still a reference type, just as pointers and file descriptors are. The rules on Collections are quite clear: an Index
may be used with the original collection, or a derived slice, and nothing else.
So let's return to your incisive question:
There is no contradiction here. One can always derive a value with reference semantics from a variable by asking for a pointer to that variable. This is true for all variables in Swift: withUnsafePointer(to:)
will give you a pointer, and pointers are always reference types.
Importantly though, the pointer is the reference type, not the thing it points to. The thing it points to still has value semantics, and while you can mutate through that reference all day, when you hold the bare type you are holding something with value semantics. Put another way, one can always "refer" to a value: it doesn't eliminate the semantic of the thing being referred to.
Certainly Dave Abrahams agrees with you: ValueSemantic protocol - #3 by dabrahams. However, even on that post he notes that classes make life very tricky here. Regardless, I think this is now a disagreement on what the words mean, rather than a disagreement on any technical aspect, so for the sake of not further derailing this thread I'll withdraw.
This is like saying that String
Array
isn't a value type because its capacity
is observable. Pointer identity is always a property of class types, but it's not always a saliant property of class types.
Back to OP's question. To change from:
struct Task {
let id: Int // in a global table
// methods
}
to:
class Task {
let id: Int // in a global table
// methods
}
is possible but undesirable: for example there would be an unneeded ARC traffic when you assign one task variable to another and there is no good reason for that. Nothing will really happen on deinit as the underlying task's lifetime won't suddenly end.
To change it to something like:
class Task {
// actual properties of the task, no global table is used here
// methods
}
would be impossible. Try to do it with a very similar thing - thread to see the issue:
class ThreadClass {
...
deinit {
...
}
}
Would you have "pthread_kill" in this class deinit? pthread_kill just sends a signal to the thread, which might not necessarily kill the thread (aside from killing a thread is generally a very bad idea). And the thread itself might finish/exit - but what would that mean to ThreadClass instances that are still alive? Here thread's lifetime is independent of class instances lifetime.