12

I have a very simple problem with Swift. I created this function:

var dictAges : [String: Int] = ["John":40, "Michael":20, "Bob": -16]

func correctAges(dict:[String:Int]) {
     for (name, age) in dict {
          guard age >= 0 else {
          dict[name] = 0
          continue
          }
     }
}
correctAges(dict:dictAges)

But I don't understand the error:

"cannot assign through subscript: 'dict' is a 'let' constant, dict[name] = 0"

How can I solve it?

Dávid Pásztor
  • 51,403
  • 9
  • 85
  • 116
Anna565
  • 735
  • 2
  • 10
  • 21
  • 2
    https://stackoverflow.com/a/36165146/5496433 – BallpointBen Nov 15 '17 at 15:13
  • Unrelated, but I wouldn't use a `guard` here – a simple `if` would suffice. Or even a `where` clause on the loop: `for (name, age) in dict where age < 0 { ... }` – Hamish Nov 15 '17 at 15:38

3 Answers3

21

Input arguments to functions are immutable inside the function body and a Dictionary is a value type, so the dict you see inside the function is actually a copy of the original dictAges. When you call a function with a value type as its input argument, that input argument is passed by value, not passed by reference, hence inside the function you cannot access the original variable.

Either declare the dict input argument as inout or if you'd prefer the more functional approach, return a mutated version of the dictionary from the function.

Functional approach:

var dictAges : [String: Int] = ["John":40, "Michael":20, "Bob": -16]

func correctAges(dict:[String:Int])->[String:Int] {
     var mutatedDict = dict
     for (name, age) in mutatedDict {
          guard age >= 0 else {
              mutatedDict[name] = 0
              continue
          }
     }
     return mutatedDict
}
let newDict = correctAges(dict:dictAges) //["Michael": 20, "Bob": 0, "John": 40]

Inout version:

func correctAges(dict: inout [String:Int]){
    for (name,age) in dict {
        guard age >= 0 else {
            dict[name] = 0
            continue
        }
    }
}

correctAges(dict: &dictAges) //["Michael": 20, "Bob": 0, "John": 40]
Dávid Pásztor
  • 51,403
  • 9
  • 85
  • 116
  • 5
    Do not use the `inout` version. Functions have a return value for a reason. – rmaddy Nov 15 '17 at 15:19
  • 2
    there is golden-rule in Swift: _if the answer is using `inout`, you raised the wrong question_ – holex Nov 15 '17 at 15:20
  • 1
    @rmaddy I agree, the version with the return value is preferred, but I wanted to show both versions to OP. – Dávid Pásztor Nov 15 '17 at 15:20
  • The question specifically asks how to use the modified array, which is what inout is used for –  Nov 15 '17 at 15:21
  • 1
    "Input arguments to functions are immutable inside the function body" Not if they are class instances. A poor explanation. – matt Nov 15 '17 at 15:53
  • @matt, that's not entirely true. Even for reference types, you cannot change the reference itself. You can change a property of the instance, but you cannot change the reference, hence the input argument is immutable. However, I have updated my answer to make it reflect the difference between value and reference types. – Dávid Pásztor Nov 15 '17 at 16:01
0

Your expectation that you can mutate a dictionary passed into a function is wrong, for the simple reason that a dictionary in Swift is a value type. And the types inside the dictionary, String and Int, are values too. This means that, in effect, the parameter dict is a copy of the original dictAges. Nothing that you do to dict can have any effect on dictAges.

This is a sign that you should rethink your entire architecture. If it was wrong to enter a negative number as an age, you should have caught that up front, as it was being entered. In effect, your entire use of a dictionary of Ints as the model here is probably incorrect. You should have used a dictionary of Person, where Person is a struct with an age and a setter that prevents an age from being negative in the first place.

If you must have a function that runs through an arbitrary dictionary and fixes the values to be nonnegative, make that a feature of dictionaries themselves:

var dictAges : [String: Int] = ["John":40, "Michael":20, "Bob": -16]

extension Dictionary where Value : SignedInteger {
    mutating func fixAges() {
        for (k,v) in self {
            if v < 0 {
                self[k] = 0
            }
        }
    }
}

dictAges.fixAges()
matt
  • 515,959
  • 87
  • 875
  • 1,141
0

In general, it's not good to mutate a sequence as you're looping through it, which is why you only get an immutable copy of the dictionary in your code.

Swift 4

I think a nicer approach could be to use map, and to take a more functional approach:

var dictAges : [String: Int] = ["John":40, "Michael":20, "Bob": -16]

func correctAges(dict:[String:Int]) -> [String:Int]
{
    let corrected = dict.map { (name, age) in age > 0 ? (name, age) : (name, 0) }
    return Dictionary(uniqueKeysWithValues: corrected)
}

dictAges = correctAges(dict: dictAges)
print(dictAges) // ["Michael": 20, "Bob": 0, "John": 40]

This way you can reuse this method for any [String:Int] dictionary.

XmasRights
  • 1,427
  • 13
  • 21