Crash During Int to UInt Converting | Swift
In this post, I want to discuss a crash, that we encounter in production code. The bug was related to a wrong expectation of API. I believe, everybody can face the same bug, even if the code looks right.
Our problem
In our source code, we needed to save a file on disk. So we had to check that a user had enough space. We use the URL
and URLResourceValues
for it. And the logic was wrapped into a special provider. The simplified version is listed below.
struct SpaceInfo {
let total: UInt64
let available: UInt64
}
final class AvailableSpaceProvider {
func getSpaceInfo() -> SpaceInfo? {
guard
let url = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).last,
let values = try? url.resourceValues(forKeys: [.volumeTotalCapacityKey, .volumeAvailableCapacityKey]),
let total = values.volumeTotalCapacity,
let available = values.volumeAvailableCapacity
else {
return nil
}
return SpaceInfo(total: UInt64(total), available: UInt64(available))
}
}
When we started getting crash logs, they looked something like that
EXC_BREAKPOINT ...
0 AvailableSpaceProvider.getSpaceInfo()
....
Talked to other developers, we understand that volumeTotalCapacityKey
and volumeAvailableCapacityKey
may return negative values. It’s unusual, how available and total space can be negative?
The crash was happening during Int to UInt converting. However, I expected that UInt from Int initializer returns something like zero, when Int is negative. Which turned out to be wrong. It’s also describe in the documentation.
Use this initializer to convert from another integer type when you know the value is within the bounds of this type. Passing a value that can’t be represented in this type results in a runtime error.
Solution
From the Swift perspective, you should explicitly specify how you want to convert values. The foundation provides several initializers for that. Let’s discuss it.
init?(exactly:)
Use the init?(exactly:)
initializer to create a new instance after checking whether the passed value is representable. Instead of trapping on out-of-range values, using the failable init?(exactly:)
initializer results in nil.
let x = Int16(exactly: 500)
// x == Optional(500)
let y = Int8(exactly: 500)
// y == nil
init(clamping:)
Use the init(clamping:)
initializer to create a new instance of a binary integer type where out-of-range values are clamped to the representable range of the type. For a type T, the resulting value is in the range T.min...T.max
.
let x = Int16(clamping: 500)
// x == 500
let y = Int8(clamping: 500)
// y == 127
let z = UInt8(clamping: -500)
// z == 0
init(truncatingIfNeeded:)
Use the init(truncatingIfNeeded:)
initializer to create a new instance with the same bit pattern as the passed value, extending or truncating the value's representation as necessary.
let q: Int16 = 850
// q == 0b00000011_01010010
let r = Int8(truncatingIfNeeded: q) // truncate 'q' to fit in 8 bits
// r == 82
// == 0b01010010
let s = Int16(truncatingIfNeeded: r) // extend 'r' to fill 16 bits
// s == 82
// == 0b00000000_01010010
Note that when negative integers are extended, the result is padded with ones. In my opinion, you’ll rarely need it, but it’s good to know about. Most of the time you’ll use exactly or clamping initializers.
Final thoughts
This information is not only about Int to UInt, but for all convertings for numeric types. If you are absolutely sure, that one type fits into another, use the default initializer (since it does not perform any checks, it is faster). But if you are not sure, use the initializers described above.