-->

how to define a custom subscripting array operator

2019-05-11 19:36发布

问题:

was it possible to add operator func to Swift class subscript method

var x = ["dkfkd", "dkff"]
x[2] ??=  "mmmm" // equal to x[2] = x[2] ?? "mmmm"

回答1:

This isn’t related to the subscript operator, but more a question of how to define a ??= operator. Which you can do, but it might not work quite the way you expect.

Here’s a possible implementation:

// first define the ??= operator
infix operator ??= { }

// then some pretty standard logic for an assignment
// version of ??
func ??=<T>(inout lhs: T?, rhs: T) {
    lhs = lhs ?? rhs
}

This compiles, and works as you might be expecting:

var i: Int? = 1

i ??= 2   // i unchanged

var j: Int? = nil

j ??= 2  // j is now Some(2)

It will also work in combination with subscripts:

var a: [Int?] = [1, nil]

a[1] ??= 2

a[1]  // is now Some(2)

I say this might not work completely as expected because of the types. a ?? b takes an optional a, and if it’s nil, returns a default of b. However it returns a non-optional value. That’s kinda the point of ??.

On the other hand, ??= cannot do this. Because the left-hand side is already determined to be optional, and an assignment operator cannot change the type only the value. So while it will substitute the value inside the optional in the case of nil, it won’t change the type to be non-optional.

PS the reason the ??= function compiles at all is because non-optional values (i.e. what you will get back from lhs ?? rhs) are implicitly upgraded to optional values if necessary, hence lhs ?? rhs, of type T, can be assigned to lhs, which is of type T?.



回答2:

If you don't have a requirement that this "extending subscript" operation be on Array itself, you might find it more straightforward to create your own type that wraps an Array (or other buffer) than to wedge this behavior into an Array extension.

First, you were probably just thinking about using array[array.count] = item as a synonym for appending, right? (Beyond that, it gets more complicated, as @AirspeedVelocity notes, because there's the question of what to put in between the existing array elements and the new item.) Here's a version that does just that:

struct ExtendingArray<T>: ArrayLiteralConvertible {
    typealias Element = T
    var array: [T] = []

    init(arrayLiteral elements: Element...) {
        self.array = elements
    }

    subscript(index: Int) -> T {
        get {
            return array[index]
        }
        set(newValue) {
            if index < array.count {
                array[index] = newValue
            } else if index == array.count {
                array.append(newValue)
            } else {
                fatalError("can't assign subscript beyond append range")
            }
        }
    }
}

// playground example
var foo: ExtendingArray = ["a", "b"]
foo[2] = "c"
foo // {["a", "b", "c"]}

If you want to be able to create a sparse collection, where you can assign an element at any numeric index without having to fill in placeholder elements in between nonconsecutive indices... you don't actually want an Array — you want a Dictionary whose keys are integers. You can do that with just a Dictionary if you like:

var sparse = [Int:String]()
sparse[0] = "a"
sparse[1] = "b"
sparse[26] = "z"
sparse // [0: "a", 1: "b", 26: "z"]

Or, if you want slightly more array-like semantics, you can create your own type that wraps a Dictionary (and adopts ArrayLiteralConvertible, forwards subscript to the underlying dictionary, etc).



回答3:

Since @MartinR points out I’ve probably misintepreted the question, here’s an answer to the other possible interpretation – that you want a subscript that adds a value to a collection if it isn’t present.

You won’t be able to do this with an operator. And you also won’t be able to change the behaviour of the standard subscript operator on an existing collection. But you can add a new subscript operator, that takes a custom type. So you could try this:

// custom type that is used to indicate behaviour
struct IfAbsent<I> {
    let idx: I
    init(_ idx: I) { self.idx = idx }
}

// add an array extension that defines a subscript that takes an IfAbsent type
extension Array {
    subscript(idx: IfAbsent<Int>) -> T{
        get {
            return self[idx.idx]
        }
        set(newValue) {
            if idx.idx < self.count {
                self[idx] = newValue
            }
            else {
                self.extend(Repeat(count: idx.idx - self.count + 1, repeatedValue: newValue))
            }
        }
    }
}
var x = ["dkfkd", "dkff"]
x[IfAbsent(1)] = "does nothing"
x[IfAbsent(2)] = "will insert this”
// x will be equal to ["dkfkd", "dkff", "will insert this"]

The downside is arrays have to have values at every position, so you have to put this new value in every entry between the current last and the target subscript:

var x = [1,2,3]
x[10] = 1
// x is now [1, 2, 3, 1, 1, 1]

Works a lot better for dictionaries:

extension Dictionary {
    subscript(idx: IfAbsent<Key>) -> Value? {
        get {
            return self[idx.idx]
        }
        set(newValue) {
            if self[idx.idx] == nil {
                self[idx.idx] = newValue
            }
        }
    }
}

You could also do a version that took a default to pad the array, separate from the value to assign at the specific subscript:

struct WithDefault<T> {
    let idx: Int
    let dflt: T
    init(_ idx: Int, _ dflt: T) {
        self.idx = idx
        self.dflt = dflt
    }
}
extension Array {
    subscript(idx: WithDefault<T>) -> T {
        get {
            return idx.idx < self.count
                ? self[idx.idx]
                : idx.dflt
        }
        set(newValue) {
            if idx.idx < self.count {
                self[idx] = newValue
            }
            else {
                self.extend(Repeat(count: idx.idx - self.count, repeatedValue: idx.dflt))
                self.append(newValue)
            }
        }
    }
}

This has the benefit of a sensible default for the subscript get, too:

var x = [1,2,3]
x[WithDefault(5, 99)]  // returns 99
x[WithDefault(5, 0)] = 5
// x is now [1, 2, 3, 0, 0, 5]

(You could even get fancy and do a version that took a closure for the default that mapped the index).



回答4:

If I may join the party :) Love the responses from rickster and Airspeed Velocity... extending sequence types and default value types ...

The way it is posed, the question essentially describes an ordered set behaviour (only, provided by the operator rather than the type). So we can play with that idea in some very straightforward ways:

infix operator ??= { }

func ??= <T: Hashable> (inout lhs: [T], rhs: T) {
    if Set(lhs).contains(rhs) {
        lhs += [rhs]
    }
}

We could, however, seek to preserve the syntactic direction Apple is taking with Swift and build on their += array concatenation operator:

infix operator +== { }

func +== <T: Hashable> (inout lhs: [T], rhs: [T]) {
    lhs += Set(rhs).subtract(lhs) 
}

However, this is not preserving the relative order of elements in rhs that are not in lhs. To do this we could:

func +== <T: Hashable> (inout lhs: [T], rhs: [T]) {
    let dif = Set(rhs).subtract(lhs)
    lhs += filter(rhs) { dif.contains($0) } // preserves order (fun version - not efficient!)
}

Which can be used like so:

var x = ["dkfkd", "dkff"]
x +== ["mmmm"]                              //--> ["dkfkd", "dkff", "mmmm"]
x +== ["and", "dkff", "so", "dkfkd", "on"]  //--> ["dkfkd", "dkff", "mmmm", "and", "so", "on"]