While hunting down a bug in Camille (the iOS-Developers Slack bot), I ran across some interesting and unexpected behavior in the Swift debugger. Normally when you place a breakpoint on a line, you expect that line (and all those after it) to not be evaluated yet. It seems that in certain cases, this is not true!
You can consistently reproduce this when chaining function calls and breaking them out onto multiple lines. For example, say we have a Message
type that has a computed property for the users mentioned in it (we will hard code them for the example):
func printMarker(_ x: Int) -> Int {
print("marker")
return x
}
struct User {
let name: String
let age: Int
}
struct Message {
public var mentionedUsersComputed: [User] {
return [User(name: "Achilles", age: 32), User(name: "Tortoise", age: 105)]
}
}
let message = Message()
var ages: [Int]
ages = [1, 2, 3]
ages = message
.mentionedUsersComputed
.map { $0.age }
.map(printMarker)
print(ages)
If you put a breakpoint on the line ages = message
, it will print marker
twice before breaking. If you inspect the value of ages
at the breakpoint, it is still [1, 2, 3]
, which is what we would normally expect. This means that although the assignment itself hasn't occurred yet, it has already evaluated the right-hand side of the assignment, even though it should be breaking before any of those lines are executed.
It's interesting to note that placing the breakpoint on any other line in the chain results in normal behavior. Try putting it on the .mentionedUsersComputed
line, and you will see that nothing has printed yet. It's also worth noting that you also get normal behavior if you move any of the chained function calls onto the first line.
If you replace the computed property with a function, the behavior is the same. This, at least, makes sense: computed properties are functions.
struct Message {
func mentionedUsersF() -> [User] {
return [User(name: "Achilles", age: 32), User(name: "Tortoise", age: 105)]
}
}
let message = Message()
var ages: [Int]
ages = [1, 2, 3]
ages = message
.mentionedUsersF()
.map { $0.age }
.map(printMarker)
print(ages)
Now, if you repeat the process, except using a property, the breakpoint behaves as (originally) expected:
struct Message {
let mentionedUsers: [User]
}
let achilles = User(name: "Achilles", age: 32)
let tortoise = User(name: "Tortoise", age: 105)
let message = Message(mentionedUsers: [achilles, tortoise])
var ages: [Int]
ages = [1, 2, 3]
ages = message
.mentionedUsers
.map { $0.age }
.map(printMarker)
print(ages)
Put a breakpoint on the ages = message
line again, and this time nothing has been printed! Okay…that's at least normal. So it seems that this issue is only related to multi-line function chains.
Nope.
Just to make things extra weird, try this one out:
struct Message {
let mentionedUsers: [User]
func mentionedUsersF() -> [User] {
return [User(name: "Achilles", age: 32), User(name: "Tortoise", age: 105)]
}
}
let achilles = User(name: "Achilles", age: 32)
let tortoise = User(name: "Tortoise", age: 105)
let message = Message(mentionedUsers: [achilles, tortoise])
var ages: [Int]
ages = [1, 2, 3]
ages = message
.mentionedUsersF()
.map { $0.age }
.map(printMarker)
print(ages)
Breaking on ages = message
now occurs before anything is printed, even though all we have changed from the above function example is adding an unused property to the Message
struct. So it seems that it is not the usage of a property that leads to expected breakpoint behavior, but the presence of a property at all.
Final notes: this doesn't occur if you chain only functions that are not members of a struct, nor does it happen if you chain functions that are members of a class.
If you have any insight or explanation for this behavior, please share it! This is pretty strange, and the fact that it is inconsistent definitely makes it seem like a bug to me, so I've filed one here.