Continuing the discussion from Pitch: enable bounds-checking for BufferPointers:
It's important to note that (realistically) this is never going to change [in Swift]. This discussion is purely for edification.
Official justification
Per the language guide:
Use
UInt
only when you specifically need an unsigned integer type with the same size as the platform’s native word size. If this isn’t the case,Int
is preferred, even when the values to be stored are known to be nonnegative. A consistent use ofInt
for integer values aids code interoperability, avoids the need to convert between different number types, and matches integer type inference, as described in Type Safety and Type Inference.
And also:
Use the
Int
type for all general-purpose integer constants and variables in your code, even if they’re known to be nonnegative. Using the default integer type in everyday situations means that integer constants and variables are immediately interoperable in your code and will match the inferred type for integer literal values.Use other integer types only when they’re specifically needed for the task at hand, because of explicitly sized data from an external source, or for performance, memory usage, or other necessary optimization. Using explicitly sized types in these situations helps to catch any accidental value overflows and implicitly documents the nature of the data being used.
Though note how that last sentence contradicts the earlier text since it is implicitly acknowledging that a signed type may permit logically invalid values (e.g. negative indices).
The Highlander problem
Though it's not written anywhere formal, that I can find, I recall the other reason is that Swift can have only one preferred numeric type (i.e. what i
defaults to when you write let i = 5
). It was argued that a signed type is more flexible - since it supports negative values - so it'd ultimately require less explicit typing of variables.
e.g. consider if you had:
let a = 5
let b = -5
let result = a + b
Possible other reasons
Avoid crashes
I vaguely recall there was also an argument that it was to avoid unexpected crashes due to the very literal-minded nature of Swift's arithmetic evaluation, e.g.:
var index = 0
someArray[index - 5 + someArgument]
IIRC, that's basically guaranteed to crash even if someArgument
is ≥ 5, because the arithmetic is performed step by step and it underflows on the first operation (-5).
It's hard to fault Swift for this because it's hard for the compiler to know what the "correct" order of associative operations is (if there even is one). The only true solution would probably be something like a numeric model of unlimited-precision intermediaries, which isn't trivial for a compiler to implement. It was apparently considered in Swift's early days but alas was rejected. I suspect it'll be a hallmark of the next generation of languages (it's in practice pretty similar to how popular languages like Python already work, with their unlimited-precision numeric types).
Note also that in at least some languages which use signed indices (e.g. Julia) the compiler actually has to do extra work as a result, so that it can actually detect incorrect uses (by magically knowing that despite the signed type in use, the values are in principle not actually signed).
Convenience in loops
Similarly, a C-style for(;;) loop might "intentionally" underflow because it counts down and has a >= 0
check, requiring the index to go negative in order to actually escape the loop. But an unsigned index would crash (in Swift) due to the underflow.
However, Swift very deliberately avoids such "low-level" looping constructs - they're one of the only cases of syntax actually being removed - favouring unindexed enumeration (for object in collection
) and range-based enumeration (for i in 0..<5
). And for reverse order, for i in stride(from: 5, to: 0, step: -1)
works just fine even if the 'from' and 'to' arguments are unsigned.
Potential performance benefits
In some other languages (e.g. C++) there are performance benefits to using signed integers for e.g. loop counters, because overflow is officially undefined behaviour for signed integers but not for unsigned integers, giving the compiler's optimiser more flexibility for signed integers.
However, I don't think this applies to Swift since overflow is not undefined behaviour in the same sense (it crashes, which is still bad behaviour, but the Swift compiler is required to ensure it crashes, it can't just omit the crash even if nothing would go wrong in its absence).
Downsides
I understand the original rationale for using signed integers for everything in Swift, but I think in retrospect that was a bad decision. It creates more problems than it solves, trading essentially just some occasional Int(…)
/ UInt(…)
"noise" for a bunch of downsides:
- It's conceptually wrong, and confusing.
- It moves error diagnostics away from their source (e.g. crash only when you try to use an invalid - negative - index, rather than when the negative index is created).
- It's fundamentally at odds with Objective-C code, which does use unsigned integers for indices, creating subtle bridging errors and complicating the bridging since there has to be special behaviour in place to implicitly rewrite
NSUInteger
(and friends) toInt
. - It limits the size of containers. Though it's exceedingly rare that this is a practical issue when using
Int
on 64-bit architectures.
I make a point of using the conceptually correct type in my own code, such as UInt
whenever that applies, and I don't have any problem with it; in practice it adds very little explicit casting because it's unnatural (and suspicious) to be converting between signed and unsigned for a given application. I suspect the vast majority of the casts are merely to deal with 3rd party libraries (including Apple's) that use the wrong signedness.