3

I am not able to modify my model class variable even using mutating func keyword in a method?

So basically I have wrapped my problem in a very easy way I have a class Car that has 3 variable id, start, and modelNo

After that initialize an empty Car model array and then I want to show 10 cars, creating a range for loop which iterates 1...10, initializing the Car model class and appending it to original cars array.

There is a check for first 5 cars id will be 0 and last 5 cars id will be 1

I want a filter in which same id's last car will start so I have created a method filtered and modifying the start variable but not able to modify it. Can you please help me what I am doing wrong?

Please see my complete code and copy paste it to the playground.

struct Car {
    var id = 0
    var start = false
    var modelNo: String

    init(start: Bool, id: Int, model: String) {
        self.start = start
        self.id = id
        self.modelNo = model
    }

    mutating func startCar(status: Bool) {
        start = status
    }
}

var arrCars:[Car] = [Car]()

for i in 1...10 {
    let md = Car(start: false, id: i <= 5 ? 0 : 1, model: "Model\(i)")
    arrCars.append(md)
}

private func filtered() {
    for item in arrCars {
        let filteredItems = arrCars.filter { $0.id == item.id }
        if filteredItems.count > 0 {
            if var lastItem = filteredItems.last {
                print("Will start \(lastItem.modelNo)")
                lastItem.startCar(status: true)
            }
        }
    }

    let cars = arrCars.filter { (car) -> Bool in
        car.start == true
    }

    print(cars.count)
}

filtered()

Any help will be appreciated.

Losiowaty
  • 7,911
  • 2
  • 32
  • 47
iamVishal16
  • 1,780
  • 18
  • 40
  • 2
    When you `mutate` structure instance's property you actually create new instance where all but mutated property are same, and mutated is set to new value. Old instances remain unchanged, such as ones in `arrCars` array. – user28434'mstep Sep 25 '18 at 11:29
  • but I want to modify `arrCars`? how can I resolve this issue? – iamVishal16 Sep 25 '18 at 11:30
  • You should either replace content of `arrCars` with mutated instances. Or just use `class` here. – user28434'mstep Sep 25 '18 at 11:33
  • Replacing content is not a good approach and I will go with class for this time. Expecting some other workarounds by other. Thanks. – iamVishal16 Sep 25 '18 at 11:37
  • @matt I think my problem is something different I am talking about `mutating function `. Why did you marked it duplicate? – iamVishal16 Sep 25 '18 at 12:05
  • 1
    I don't think it is different at all. What I'm telling you is that you are thinking about this wrong. You are mutating just fine! You are just not mutating the same Car as the one in the array. See my answer below. When you understand that a struct is a value type, this is dead obvious. Thus the duplicate. – matt Sep 25 '18 at 12:13

3 Answers3

9

Creating a mutating function on a struct doesn't change the value semantics of structs. As soon as a struct instance is mutated, a copy is made.

You say:

if var lastItem = filteredItems.last {
    print("Will start \(lastItem.modelNo)")
    lastItem.startCar(status: true)
}

So, lastItem contains an instance of a car you are going to start. Under the covers this is the same instance of the car that is in filteredItems, which is the same instance that is in arrCars. But, as soon as you mutate the Car in lastItem, a copy is made, so lastItem no longer has the same instance as the one in arrCars. filteredItems and arrCars still contain the original, unaltered Car instance.

You can change Car to be a class rather than a struct so that it has reference semantics to get the behaviour you want.

The other option is to modify the instance in place; something like arrCars[0].startCar(status: true). To do this you will need to get an array containing the indicies of the cars you want to start rather than the cars themselves:

private func filtered() {
    for item in arrCars {
        let matchingIndices = arrCars.enumerated().compactMap { (index,car) in
            return car.id == item.id ? index:nil
        }
        if matchingIndices.count > 0 {
            if let lastIndex = matchingIndices.last {
                print("Will start \(arrCars[lastIndex].modelNo)")
                arrCars[lastIndex].startCar(status: true)
            }
        }
    }

    let cars = arrCars.filter { (car) -> Bool in
        car.start == true
    }

    print(cars.count)
}

However, where a model object requires mutability or is stateful, a struct is probably not suitable.

Paulw11
  • 108,386
  • 14
  • 159
  • 186
4

The answers you've gotten so far are not wrong, but they seem a little confusing. Let's look at it a little more simply. The problem is this line:

if var lastItem = filteredItems.last

This yields a Car as lastItem. But it is not the same Car as the one in the filteredItems.

That is because a struct is a value type. Assignment copies the struct. Thus, what you do to lastItem has no effect on what is sitting inside filteredItems.

So, you are not having any trouble mutating a struct. You are mutating lastItem just fine! The problem is that the lastItem Car is not the same object as any Car inside filteredItems.

Peter Schorn
  • 916
  • 3
  • 10
  • 20
matt
  • 515,959
  • 87
  • 875
  • 1,141
  • Okay. I will take care of it. – iamVishal16 Sep 25 '18 at 12:18
  • While semantically you can think of the copy happening when the assignment is made, technically, the copy isn't made until the mutation occurs. – Paulw11 Sep 25 '18 at 20:29
  • @Paulw11 Yes, although it's important to make clear that that is an implementation detail of Array in particular. Copy-on-write is not a fact about structs, it's a fact about certain highly optimized library types. – matt Sep 25 '18 at 20:34
  • Probably. I guess I was too worried about someone correcting me regarding copy-on-write in the comments :). I should have just gone with the simplified explanation that describes the effect rather than the implementation. – Paulw11 Sep 25 '18 at 20:35
0

Firstly try to run this code and check if it solves the problem if we stick to Structures. Then I'll try to explain why this helps:

struct Car {
    var id = 0
    var start = false
    var modelNo: String

    init(start: Bool, id: Int, model: String) {
        self.start = start
        self.id = id
        self.modelNo = model
    }

    mutating func startCar(status: Bool) {
        start = status
    }
}

var arrCars: Array<Car> = [Car]()

for i in 1...10 {
    let md = Car(start: false, id: i <= 5 ? 0 : 1, model: "Model\(i)")
    arrCars.append(md)
}

private func filtered() {
    for item in arrCars {
        let filteredItems = arrCars.filter { $0.id == item.id }
        if filteredItems.count > 0 {
            // solves the problem
            arrCars[filteredItems.count-1].startCar(status: true)
            print("Will start \(arrCars[filteredItems.count-1].modelNo)")
        }
    }

    let cars = arrCars.filter { (car) -> Bool in
        car.start == true
    }

    print(cars.count)
}

filtered()
Igor B.
  • 2,219
  • 13
  • 17
  • No. Now there is only one car in `cars` array. I want two values last print statement will print 2 for correct output. – iamVishal16 Sep 25 '18 at 11:51
  • @Vishal16, sorry, I don't fully get the idea of code. Did you notice that before proposed changes the program prints 0. After changes it prints 1. It does necessary modification of structure. – Igor B. Sep 25 '18 at 11:56
  • I am just saying there are two cars which will start and In your solution only 1 car had a start. – iamVishal16 Sep 25 '18 at 12:01
  • @Vishal16, thanks for the reply. But I'm still confused. the code as I understand it is starting only last car or am I wrong? – Igor B. Sep 25 '18 at 12:05
  • If you run my code in Playground you will find this line `print("Will start \(lastItem.modelNo)") ` print two cars model. Please check it. – iamVishal16 Sep 25 '18 at 12:07
  • 2
    Ok, got it now. Thanks! Further answer modifications seems not needed as Paulw11 has already provided version of the code I intended to provide. The key thing per my understanding that an Array in Swift is structure by itself. And modification of it's items is possible thru the subscript `[]` operator usage. That's an interesting questions and IMHO does not duplicates attached one. Good luck! – Igor B. Sep 25 '18 at 12:30
  • Thanks for you are feeling It's an interesting question. :) – iamVishal16 Sep 25 '18 at 12:41