Search code examples
swiftswift3slice

Data <-> MutableRandomAccessSlice


I am really struggling with the fact that someData[start...stop] returns a MutableRandomAccessSlice. My someData was a let to begin with, so why would I want a Mutable thing? Why don't I get just a RandomAccessSlice. What's really frustrating though, is that it returns a thing that is pretty API incompatible with the original source. With a Data, I can use .withUnsafeBytes, but not so with this offspring. And how you turn the Slice back into a Data isn't clear either. There is no init that takes one of those.

I could use the subdata(in:) method instead of subscripting, but then, what's the point of the subscript if I only ever want a sub collection representation that behaves like the original collection. Furthermore, the subdata method can only do open subranges, why the subscript can do both closed and open. Is this just something they haven't quite finished up for Swift3 final yet?


Solution

  • Remember that the MutableRandomAccessSlice you get back is a value type, not a reference type. It just means you can modify it if you like, but it has nothing to do with the thing you sliced it out of:

    let x = Data(bytes: [1,2,3]) // <010203>
    var y = x[0...1]
    y[0] = 2
    x // <010203>
    

    If you look in the code, you'll note that the intent is to return a custom slice type:

    public subscript(bounds: Range<Index>) -> MutableRandomAccessSlice<Data> {
        get {
            return MutableRandomAccessSlice(base: self, bounds: bounds)
        }
        set {
            // Ideally this would be:
            //   replaceBytes(in: bounds, with: newValue._base)
            // but we do not have access to _base due to 'internal' protection
            // TODO: Use a custom Slice type so we have access to the underlying data
            let arrayOfBytes = newValue.map { $0 }
            arrayOfBytes.withUnsafeBufferPointer {
                let otherData = Data(buffer: $0)
                replaceBytes(in: bounds, with: otherData)
            }
        }
    }
    

    That said, a custom slice will still not be acceptable to a function that takes a Data. That is consistent with other types, though, like Array, which slices to an ArraySlice which cannot be passed where an Array is expected. This is by design (and likely is for Data as well for the same reasons). The concern is that a slice "pins" all of the memory that backs it. So if you took a 3 byte slice out of a megabyte Data and stored it in an ivar, the entire megabyte has to hang around. The theory (according to Swift devs I spoke with) is that Arrays could be massive, so you need to be careful with slicing them, while Strings are usually much smaller, so it's ok for a String to slice to a String.

    In my experience so far, you generally want subdata(in:). My experimentation with it is that it's very similar in speed to slicing, so I believe it's still copy on write (but it doesn't seem to pin the memory either in my initial tests). I've only tested on Mac so far, though. It's possible that there are more significant performance differences on iOS devices.