26

I recently thought about the use of unsigned integers in C# (and I guess similar argument can be said about other "high level languages")

When In need of an integer I am normally not faced with the dilemma of the size of an integer, an example would be an age property of a Person class (but the question is not limited to properties) . With that in mind there is, as far as I can see, only one advantage of using an unsigned integer ("uint") over a signed integer ("int") - readability. If I wish to express the idea that an age can only be positive I can achieve this by setting the age type to uint.

On the other hand, calculations on unsigned integers can lead to errors of all sorts and it makes it difficult to do operations such as subtracting two ages. (I read this is one of the reasons Java omitted unsigned integers)

In the case of C# I can also think that a guard clause on the setter would be a solution that gives the best of two worlds, but, this would not be applicable when I for example, an age would be passes to some method. A workaround would be to define a class called Age and have the property age be the only thing there, but this pattern would have Me create many classes and would be a source of confusion (other developers would not know when an object is just a wrapper and when it's something more sofisticaded).

What are some general best practices regarding this issue? How should I deal with this type of scenario?

6
  • 6
  • 1
    Additionally unsigned int is not CLS compliant, which means you can't call APIs that use them from other .NET languages.
    – Nathan
    Commented Jan 6, 2016 at 20:57
  • 2
    @NathanCooper: ... "can't call APIs that use them from some other languages". The metadata for them is standardized, so all .NET languages that DO support unsigned types will interoperate just fine.
    – Ben Voigt
    Commented Jan 6, 2016 at 21:39
  • 5
    To address your specific example, I would not have a property called Age in the first place. I'd have a property called Birthday or CreationTime or whatever, and compute the age from it. Commented Jan 6, 2016 at 22:25
  • 3
    "...but this pattern would have Me create many classes and would be a source of confusion" actually that's the correct thing to do. Just search for the infamous Primitive Obsession anti pattern.
    – Songo
    Commented Jan 7, 2016 at 1:50

3 Answers 3

28

The designers of the .NET Framework chose a 32 bit signed integer as their "general-purpose number" for several reasons:

  1. It can handle negative numbers, especially -1 (which the Framework uses to indicate an error condition; this is why a signed int is used everywhere indexing is required, even though negative numbers are not meaningful in an indexing context).
  2. It's large enough to serve most purposes, while being small enough to be used economically almost anywhere.

The reason to use unsigned ints is not readability; it is having the capability to get the math that only an unsigned int provides.

Guard clauses, validation and contract preconditions are perfectly acceptable ways to insure valid numeric ranges. Seldom does a real-world numeric range correspond exactly to a number between zero and 232-1 (or whatever the native numeric range is of the numeric type you chose), so using a uint to constrain your interface contract to positive numbers is kind of beside the point.

10
  • 2
    Nice answer! Also there might be some cases where an unsigned int may actually inadvertently produce more errors (though probably ones immediately spotted, but a bit confusing) -- imagine looping in reverse with an unsigned int counter because some size is an integer: for (uint j=some_size-1; j >= 0; --j) -- whoops (not sure if this is an issue in C#)! I found this issue in code before which tried to use unsigned int on the C side as much as possible -- and we ended up changing it to just favor int later on, and our lives were much easier with fewer compiler warnings too.
    – user204677
    Commented Jan 6, 2016 at 18:12
  • 16
    "Seldom does a real-world numeric range correspond to a number between zero and 2^32-1." In my experience, if you're going to need a number larger than 2^31, you're very likely to end up also needing numbers larger than 2^32, so you might as well just move up to (signed) int64 at that point. Commented Jan 6, 2016 at 18:23
  • 3
    @Panzercrisis: That's a bit severe. It would probably be more accurate to say "Use int most of the time because that is the established convention, and it's what most people are going to expect to see used routinely. Use uint when you require the special capabilites of a uint." Remember, the Framework designers decided to follow this convention extensively, so you can't even use uint in many Framework contexts (it is not type-compatible). Commented Jan 6, 2016 at 20:03
  • 3
    @Panzercrisis It might be an overly strong phrasing; but I'm not sure if I've ever used unsigned types in C# except when I was calling down to win32 apis (where the convention is that constants/flags/etc are unsigned). Commented Jan 6, 2016 at 20:36
  • 5
    It is indeed quite rare. The only time I ever use unsigned ints is in bit-twiddling scenarios. Commented Jan 6, 2016 at 20:37
9

You mainly need to be aware of two things: the data you're representing, and any intermediate steps in your calculations.

It certainly makes sense to have age be unsigned int, because we usually don't consider negative ages. But then you mention subtracting one age from another. If we just blindly subtract one integer from another, then it's definitely possible to end up with a negative number, even if we previously agreed that negative ages don't make sense. So in this case you would want your calculation done with a signed integer.

In regards to whether unsigned values are bad or not, I would say that it is a huge generalization to say unsigned values are bad. Java doesn't have unsigned values, as you mentioned, and it constantly annoys me. A byte can have a value from 0-255 or 0x00-0xFF. But if you want to instantiate a byte larger than 127(0x7F), you either have to write it as a negative number or cast an integer to a byte. You end up with code that looks like this:

byte a = 0x80; // Won't compile!
byte b = (byte) 0x80;
byte c = -128; // Equal to b

The above annoys me to no end. I'm not allowed to have a byte have a value of 197, even though that's a perfectly valid value for most sane people dealing with bytes. I can cast the integer or I can find the negative value (197 == -59 in this case). Also consider this:

byte a = 70;
byte b = 80;
byte c = a + b; // c == -106

So as you can see, adding two bytes with valid values, and ending up with a byte with a valid value, ends up changing the sign. Not only that but it's not immediately obvious that 70 + 80 == -106. Technically this is an overflow, but in my mind (as a human being) a byte shouldn't overflow for values under 0xFF. When I do bit arithmetic on paper, I don't consider the 8th bit being a sign bit.

I work with a lot of integers on the bit level, and having everything be signed usually makes everything less intuitive and harder to deal with, because you have to remember that right-shifting a negative number gives you new 1s in your number. Whereas right-shifting an unsigned integer never does that. For example:

signed byte b = 0b10000000;
b = b >> 1; // b == 0b1100 0000
b = b & 0x7F;// b == 0b0100 0000

unsigned byte b = 0b10000000;
b = b >> 1; // b == 0b0100 0000;

It just adds extra steps that I feel shouldn't be necessary.

While I used byte above, the same applies to 32-bit and 64-bit integers. Not having unsigned is crippling and it shocks me that there are high level languages like Java that don't allow them at all. But for most people this is a non-issue, because many programmers don't deal with bit-level arithmetic.

In the end, it's useful to use unsigned integers if you're thinking of them as bits, and it's useful to use signed integers when you're thinking of them as numbers.

3
  • 7
    I share your frustration about languages without unsigned integral types (especially for bytes) but I'm afraid that this isn't a direct answer to the question asked here. Maybe you could add a conclusion, which I believe, could be: “Use unsigned integers if you're thinking of their value as bits and signed integers if you're thinking about them as numbers.”
    – 5gon12eder
    Commented Jan 6, 2016 at 20:07
  • 1
    it's what i said in a comment above. glad to see someone else thinking the same way. Commented Jan 7, 2016 at 3:19
  • Is this answer using an example from Java? The question is tagged with C#, but the byte example in this answer is not correct for C# (bytes are unsigned in C# and have the values 0-255).
    – Trisped
    Commented Dec 16, 2021 at 20:26
9

Generally, you should always use the most specific data type for your data possible.

If, for example, you are using Entity Framework to pull data from a database, EF will automatically use the data type closest to the one used in the database.

There are two problems with this in C#.
First, most C# developers use only int, to represent whole numbers (unless there is a reason to use long). This means that other developers will not think to check the data type, so they will get the overflow errors mentioned above. The second, and more critical issue, is/was that .NET's original arithmetic operators only supported int, uint, long, ulong, float, double, and decimal*. This is still the case today (see section 7.8.4 in C# 5.0 language spec). You can test this yourself using the following code:

byte a, b;
a = 1;
b = 2;
var c = a - b;      //In visual studio, hover over "var" and the tip will indicate the data type, or you can get the value from cName below.
string cName = c.GetType().Namespace + '.' + c.GetType().Name;

The result of our byte-byte is an int (System.Int32).

These two issues gave rise to the "only use int for whole numbers" practice which is so common.

So to answer your question, in C# it is usually a good idea to stick to int unless:

  • An automated code generator used a different value (like Entity Framework).
  • All other developers on the project are aware that you are using the less common data types (include a comment pointing out that you used the data type and why).
  • The less common data types are commonly used in the project already.
  • The program requires the benefits of the less common data type (you have 100 million of these you need to keep in RAM, so the difference between a byte and an int or an int and a long is critical, or the arithmetic differences of unsigned already mentioned).

If you need to do math on the data, stick to the common types.
Remember, you can cast from one type to another. This can be less efficient from a CPU stand point, so you are probably better off with one of the 7 common types, but it is an option if needed.

Enumerations (enum) is one of my personal exceptions to the above guidelines. If I have only a few options, I will specify the enum to be a byte or a short. If I need that last bit in a flagged enum, I will specify the type to be uint so I can use hex to set the value for the flag.

If you do use a property with value restricting code, be sure to explain in the summary tag what restrictions are there and why.

*C# aliases are used instead of .NET names like System.Int32 since this is a C# question.

Note: there was a blog or article from the .NET developers (which I cannot find), which pointed out the limited number of arithmetic functions and some reasons why they did not worry about it. As I remember, they indicated that they had no plans for adding support for the other data types.

Note: Java does not support unsigned data types and previously had no support for 8 or 16 bit whole numbers. Since many C# developers came from a Java background or needed to work in both languages, the limitations of one language would sometimes be artificially imposed on the other.

2
  • 1
    My general rule of thumb is simply, "use int, unless you can't".
    – PerryC
    Commented Jan 7, 2016 at 18:07
  • @PerryC I believe that is the most common convention. The point of my answer was to provide a more complete convention which allows you to use the language features.
    – Trisped
    Commented Jan 7, 2016 at 18:39

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.