Signed and Unsigned Comparisons in C, C#, and T-SQL

As noted earlier, I’ve been doing a lot of firmware development recently (in C). A long-standing rule of C (and C++) is to convert signed int values to unsigned int values if both are used in a comparison; this is what the standard specifies. It’s also traditional to issue a warning (the famous comparison between signed and unsigned integer expressions warning) because such code is usually a mistake.

In C, the expression ( (unsigned)-1 == -1 ) will compile (with a warning), and have a value of 1 (true). C# and T-SQL both handle that situation very differently.

In C#, the expression ( unchecked((uint)-1) == -1 ) will compile without warning and have a value of false. What actually happens (webcite) is that the unsigned value of 0xFFFFFFFF and the signed value of -1 are both converted to long values and then compared.

The C# behavior does makes sense. C# continues the C/C++ tradition of implicitly promoting to int for any integral binary operation (e.g., two byte values added together are converted to ints before the addition). However, the additional implicit promotion to long was a bit of a surprise. This is probably due to 64-bit math becoming more commonplace - not common enough to have an implicit promotion to long for every binary operation, but enough to add the implicit promotion to long when necessary.

In SQL Server’s TSQL, the expression select ‘true’ where -1 = 4294967295; will return an empty result set (meaning -1 is not considered equivalent to 4294967295). Interestingly, it’s doing the same kind of promotion as C# (webcite). In this case, the value 4294967295 is typed as a bigint (a 64-bit signed integer).

The C# behavior can be seen when inspecting T-SQL statements generated by Linq to Entites. In my case, I was using code such as this to look up an item by serial number (an unsigned value):

using (var context = new MyEntities())
    uint serialNumber = 0xFFFFFFFF;
    var item = context.Items.Where(x => x.SerialNumber == serialNumber).SingleOrDefault();

The generated T-SQL statement (for SQL Server Compact Edition) was like this:

SELECT TOP (2) [Extent1].[ID] AS [ID], [Extent1].[SerialNumber] AS [SerialNumber] FROM [Items] AS [Extent1]  WHERE ( CAST( [Extent1].[SerialNumber] AS bigint)) = @p__linq__0

p__linq__0 had a DbType of Int64. Note that the generated T-SQL includes an explicit cast to bigint because the C# language implicitly inserted a cast to long in the expression x.SerialNumber == serialNumber.

Lesson learned: be careful of mixing signed and unsigned types. The warnings that existed in C/C++ are not necessarily present in C# (or T-SQL).