8

I have an array of objects like this:

var myArr = [
  MyObject(name: "Abc", description: "Lorem ipsum 1."),
  MyObject(name: "Def", description: "Lorem ipsum 2."),
  MyObject(name: "Xyz", description: "Lorem ipsum 3.")
]

I know I can find the matched item like this:

var temp = myArr.filter { $0.name == "Def" }.first

But now how do I remove it from the original myArr? I was hoping the filter.first can return an index somehow so I can use removeAtIndex. Or better yet, I would like to do something like this:

myArr.removeAll { $0.name == "Def" } // Pseudo

Any ideas?

4
  • 2
    What's wrong with myArr = myArr.filter { $0.name != "Def" } ? Commented Apr 16, 2015 at 14:45
  • Because that forces me to create a new variable. I want to modify the existing myArr variable. Commented Apr 16, 2015 at 14:46
  • No it doesn't. myArr is still myArr. And this is a value type; you cannot mutate it in place! You will always be creating a new array, even if you write a mutating removeAll method. Commented Apr 16, 2015 at 14:47
  • 1
    A removeAll() implementation is discussed here: codereview.stackexchange.com/questions/86581/…. Commented Apr 16, 2015 at 14:52

5 Answers 5

25

What you are not grasping is that Array is a struct and therefore is a value type. It cannot be mutated in place the way a class instance can be. Thus, you will always be creating a new array behind the scenes, even if you extend Array to write a mutating removeIf method.

There is thus no disadvantage nor loss of generality in using filter and the logical negative of your closure condition:

myArr = myArr.filter { $0.name != "Def" }

For example, you could write removeIf like this:

extension Array {
    mutating func removeIf(closure:(T -> Bool)) {
        for (var ix = self.count - 1; ix >= 0; ix--) {
            if closure(self[ix]) {
                self.removeAtIndex(ix)
            }
        }
    }
}

And you could then use it like this:

myArr.removeIf {$0.name == "Def"}

But in fact this is a big fat waste of your time. You are doing nothing here that filter is not already doing. It may appear from the myArr.removeIf syntax that you are mutating myArr in place, but you are not; you are replacing it with another array. Indeed, every call to removeAtIndex in that loop creates another array! So you might as well use filter and be happy.

Sign up to request clarification or add additional context in comments.

5 Comments

Does mutating func removeAtIndex() really create a new array?
@MartinR Sure. Try this: var arr = [1,2,3] { didSet { println("did") } }; arr.removeAtIndex(1). The didSet is called, proving that the array has been replaced. Think of the variable arr as a shoebox; we've pulled the array out of the shoebox and put a different one in. That is how a value type works. A call to a mutating func of a value type instance replaces that instance with a new instance.
@matt: arr[0] = 2 triggers didSet as well. It's not that I don't believe you, but if each assignment to an array element creates an entire new array then that would make Array quite unusable for any serious work (imagine a linear equation solver with 1000 equations and 1000 unknowns).
@MartinR No, there can be efficiencies of element storage we don't know about (and there is every reason to believe that there are). It may be a new array without making any new elements. Nevertheless, the array reference itself cannot be mutated in place. It is replaced every time the array is mutated through it in any way.
@MartinR "arr[0] = 2 triggers didSet as well". Exactly so. That's not an argument against what I'm saying, it is what I'm saying. Try the same sort of thing with a reference type and you will see that this is not so. Replace the name of a MyObject instance and the reference is not replaced (and didSet is not called). - You might want to look over my book's discussion of this topic: apeth.com/swiftBook/ch04.html#SECreferenceTypes
5

Apple had added what you want in Swift 4:

var phrase = "The rain in Spain stays mainly in the plain."
let vowels: Set<Character> = ["a", "e", "i", "o", "u"]
phrase.removeAll(where: { vowels.contains($0) })
// phrase == "Th rn n Spn stys mnly n th pln."

Comments

1

Get the objects using filter then loop through the array and use myArr.removeAtIndex(index) to remove each object. Using filter is doing exactly that. To understand what is happening read below. Matts answer is a much cleaner way to accomplish this since you're testing for the opposite match therefore each object is preserved unless it matches your value.

Loop through your temp filter array

if let index = find(temp, note) {
   myArr.removeAtIndex(index)
}

6 Comments

For-loop the only way to find the index?.. Is there an elegant closure for this?
find only works for simple arrays, not array of objects right? Couldn't get find to work with array of objects.
You're already getting all of the objects using filter then you can use find in a loop to remove each from the other array.
filter is the "elegant closure" (and also is a loop, by the way)
Exactly, there may be confusion as to how each iteration is used and if he is wanting to preserve the original array? Simply testing for the negative gives the "deleting from array" affect if that's all he needs.
|
1

if you want to remove an object from an array that is being iterated, you should always iterate backwards as otherwise you will at some point work on indices that aren't valid anymore.

var myArr = [
    ["key":"value1"],
    ["key":"value2"],
    ["key":"value3"]
]

for index in stride(from: myArr.count - 1 , to: 0, by: -1){
    let x = myArr[index]
    if x["key"] == "value2"{
        myArr.removeAtIndex(index)
    }
}

Comments

0

Older and thouroughly answered post and all, but as an anecdote to Matt's answer above, for the simpler case of removing a single element, you could also do something like:

extension Array {
    public func dropFirst(matching: (Element) -> Bool) -> ArraySlice<Element> {
        guard let at = self.firstIndex(where: matching) else { return ArraySlice(self) }
        return prefix(at) + suffix(from: at).dropFirst()
    }
}

Which short circuits as soon as it finds element to drop

where:

[1, 2, 3, 4, 5].dropFirst(matching: { $0 == 3 }) // -> [1, 2, 4, 5] (ArraySlice)

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.