Makaronodentro
Makaronodentro

Reputation: 957

Why is NSArray mutable when used from Swift?

I have an objective-c header with the following property

@property (nullable, nonatomic, strong) NSArray<CustomObject *> *customObjects;

If I create a swift extension of that class I can now remove objects from the NSArray:

self.customObjects?.remove(at: 0)

Also if I do

print(type(of: self.customObjects))

I get:

Array<CustomObject>

Aren't NSArrays immutable ? Does Swift create a shallow copy whenever we edit it?

Upvotes: 3

Views: 294

Answers (1)

rickster
rickster

Reputation: 126137

Your property is (implicitly) declared readwrite in ObjC. This means you can change the property writing a new NSArray instance that replaces the old (in which case the new instance's constants might be derived by first reading the other NSArray instance that's the existing value of the property):

NSArray *currentObjects = self.customObjects;
// one of many ways to derive one immutable array from another:
NSArray *newArray = [currentObjects subarrayWithRange:NSMakeRange(1, currentObjects.count - 1)];
self.customObjects = newArray;

In Swift, your property comes across as a Swift.Array (that is, the Array type from the Swift standard library), which is a value type. Every assignment semantically creates a copy. (The expensive work of performing the copy can be deferred, using a "copy on write" pattern. Arrays of reference types, like objects, copy references instead of storage, so it's essentially a "shallow copy".)

Mutating operations do this, too:

let currentObjects1 = self.customObjects
currentObjects1.remove(0) // compile error
// currentObjects1 is a `let` constant so you can't mutate it

var currentObjects = self.customObjects
currentObjects.remove(0) // ok

print(self.customObjects.count - currentObjects.count) 
// this is 1, because currentObjects is a copy of customObjects
// we mutated the former but not the latter so their count is different

self.customObjects = currentObjects
// now we've replaced the original with the mutated copy just as in the ObjC example

When you have a readwrite property in Swift, and the type of that property is a value type like Array (or is an ObjC type that's bridged to a value type, like NSArray), you can use mutating methods directly on the property. That's because calling a mutating method is semantically equivalent to reading (and copying) the existing value, mutating the copy, and then writing back the changed copy.

// all equivalent
self.customObjects.remove(0)
self.customObjects = self.customObjects.dropFirst(1)
var objects = self.customObjects; objects.remove(0); self.customObjects = objects

BTW: If you're designing the API for the ObjC class in question here, you might consider making your customObjects property nonnull — unless there's a meaningful semantic difference between an empty array and a missing array, your Swift clients will find it cumbersome needing to distinguish the two.

Upvotes: 1

Related Questions