C#: Numeric Data Types In A Nutshell
In C#, there is full scale availability of all the data types that can be used to represent anything inside a computer.
In this article, we are particularly concerned about numeric data types in C#. So without further ado, lets get started.
Numeric Data Types
Numeric data is of two types:
Integer Types
To represent integers we have 8 data types in C# that represent varying ranges of integers.
First we have byte that allows all positive integers from 0 to 255.
The byte type also has a signed variant named sbyte that represents integers from -128 to positive 127.
byte b = 255 // 0 to 255
b = -45 // gives compile time error
sbyte sb = -78 // -128 to 127
Note: It is a compile time error to assign negative values to unsigned data types.
Next we have short, int and long with unsigned variants named as ushort, uint, ulong.
short is a 16 bit integer type that allows integers from -32768 to 32767. Its unsigned variant named ushort ranges from 0 to 65535.
int is a 32 bit integer type that allows integers from -2147483648 to 2147483647. Its unsigned variant named uint ranges from 0 to 4294967295.
long is a 64 bit integer type that allows integers from -9223372036854775808 to 9223372036854775807. Its unsigned variant named ulong ranges from 0 to 18446744073709551615.
Note: When you declare the variables of long type using var keyword, you must suffix the long literal with l or L. For unsigned long (ulong), you have the suffix ul or UL. Although capital L and UL are preffered for the sake of clarity.
var l = -90L; // long variable
Console.WriteLine(l.GetType()); // prints System.Int64 (alias for long)
var ul = 90UL; // unsigned long (ulong) variable
Console.WriteLine(ul.GetType()); // prints System.UInt64 (alias for ulong)
The table below summarizes the range of values for the above integer types.
By the way, you can use MinValue and MaxValue methods to get the minimum and maximum allowable values for any integer type.
See the example below
// prints 0 to 255
Console.WriteLine($"{byte.MinValue} to {byte.MaxValue}");
// prints -128 to 127
Console.WriteLine($"{sbyte.MinValue} to {sbyte.MaxValue}");
// prints -32768 to 32767
Console.WriteLine($"{short.MinValue} to {short.MaxValue}");
// prints 0 to 65535
Console.WriteLine($"{ushort.MinValue} to {ushort.MaxValue}");
// prints -2147483648 to 2147483647
Console.WriteLine($"{int.MinValue} to {int.MaxValue}");
// prints 0 to 4294967295
Console.WriteLine($"{uint.MinValue} to {uint.MaxValue}");
// prints -9223372036854775808 to 9223372036854775807
Console.WriteLine($"{long.MinValue} to {long.MaxValue}");
// prints 0 to 18446744073709551615
Console.WriteLine($"{ulong.MinValue} to {ulong.MaxValue}");
Integer types allow all the basic arithmetic operations i.e. addition, multiplication, subtraction and division.
Beside standard number literal, integer literals can also be written in other forms.
Digits Separator: You can write integer literals with separator for clarity like
byte b = 1_23;
short s = -123_45;
int i = 123_456_789;
long l = 123_456_789_34;
Console.WriteLine(b); // prints 123
Console.WriteLine(s); // prints -12345
Console.WriteLine(i); // prints 123456789
Console.WriteLine(l); // prints 12345678934
These digits separators can be placed any where in the literal.
Binary Literals: You can write integer literals in binary as well. Binary literals starts with 0b/0B. Beginning with C# 7.2, you can also start the binary literal with an underscore (_) as well like 0b_/0B_.
See the example below
byte b = 0b_101; // 5
short s = -0b111; // -7
int i = 0b_110; // 6
long l = 0B_10001111; // 143
Hexadecimal Literals: You can write integer literals in hexadecimal as well. Hex literals starts with 0x/0X. Beginning with C# 7.2, you can also start the binary literal with an underscore (_) as well like 0x_/0X_.
Hexadecimal literals can have digits:
- 0 to 9
- a/A to f/F (a/A = 10, b/B = 11, …, f/F = 15)
See the example below
byte b = 0X_7F; // 127
short s = -0x111A; // -4378
int i = 0X010; // 16
ulong l = 0x_FFFFFFFFFFFFFFFF; // 18446744073709551615
Earlier I mentioned there are 8 data types for representing integers. Sorry that was not the whole truth. There is also a special integer type char that is used to represent characters like a, b, A, *, 9 etc. The char type also store integers internally but outputs the character representation when printed. For example when we store the letter j, it basically stores 106 internally to represent the letter. Character literals are enclosed in single quotation marks. Char literals can also be represented using hex or unicode notation as can be seen in the example below
char ch = 'j'; // stores 106 internally
char ch1 = '\x006A'; // hex notaion for j
char ch2 = '\u006A'; // unicode code point notation for j
Cosole.WriteLine(ch); // prints j
Cosole.WriteLine(ch1); // prints j
Cosole.WriteLine(ch2); // prints j
char type is a 16 bit unsigned type that allows values from 0 to 65535. The underlying .NET type is System.Char. Char is basically capable of representing unicode code points.
Real Number Types
In C#, real numbers having digits after the decimal point, can be represented using 3 data types: float, double and decimal.
float is a 32 bit real number type that allows values from 1.5E-45 to 3.4E+38. The underlying .NET type is System.Single. It is a single precision floating point type that confirms to the IEEE-754 standard for floating point representation and allows up to 7 digits after the decimal point.
double is a 64 bit real number type that allows values from 5E-324 to 1.7E+308. The underlying .NET type is System.Double. It is a double precision floating point type that confirms to the IEEE-754 standard for floating point representation and has maximum precision of 17 digits.
float f = 3.2F;
Console.WriteLine(f); // prints 3.2
f = 7.8923738839F;
Console.WriteLine(f); // prints 7.892374
double d = 2.43403930233209;
Console.WriteLine(d); // prints 2.43403930233209
Note: You must use suffix F or f with float literals because by default the real number literals are considered double by the compiler.
Notice that the last result in above example was rounded to 7.892374. That is the expected behavior when the precision of the number exceeds the maximum allowed digits.
Operations with Float and Double
All the primitive arithmetic operations (addition, multiplication, subtraction and division) that are allowed with integer types are also permitted with real numbers.
But there are some limitations that can be seen with examples below
double d = 0.1;
Console.WriteLine(d * 3); // prints 0.30000000000000004
The example doesn’t make sense right! But why the result is not just 0.3 and instead represented with unnecessary digits after the actual result. This is because float and double types are handled using floating point unit inside the computer hardware and since the binary representation of digits after the decimal point is inaccurate, the result is also inaccurate.
The float and double types are not suitable for monetary calculations (calculations involving money) because of their inaccuracy.
To overcome this limitation, the framework designers came up with the type decimal. The decimal type is a 128 bit real number type that allows values from 1E-28 to 7.9E+28. The underlying .NET type is System.Decimal. It has maximum precision of 29 digits.
Let’s see the above example with decimal type
decimal d = 0.1M;
Console.WriteLine(d * 3); // prints 0.3
Note: You must use suffix M or m with decimal literals because by default the real number literals are considered double by the compiler.
To compare the real numbers (float, double and decimal), there is a method CompareTo that comes handy. This method is preffered because real numbers don’t have exact binary representations. The example below prints false because of this limitation.
For example:
Console.WriteLine(0.1 + 0.1 + 0.1 == 0.3) // false
Using CompareTo can save us here. This method is available on all float, double and decimal types.
It returns:
- 0 when both the numbers are equal.
- 1 when first number is greater than the second number
- -1 when second number is greater than the first number
We will demonstrate it below
float f = 0.1F;
float f1 = 0.1F;
float f2 = 0.1F;
float f3 = 0.1F;
Console.WriteLine(f.CompareTo(f1 + f2 + f3)); // 0
double d1 = 2.3;
double d2 = 2.7;
Console.WriteLine(d2.CompareTo(d1)); // 1
decimal m1 = 7.18M;
decimal m2 = 8.9M;
Console.WriteLine(m1.CompareTo(m2)); // -1
Special Values For Float/Double
While the division by 0 for integer types raises exception, the division by zero for float/double results in infinity. It is a a set of special values defined as:
- PositiveInfinity
- NegativeInfinity
These values are available on both float and double types. Consider the following cases:
- When a positive float/double divides by 0, PositiveInfinity is the result. You can check if a number is positive infinity by using IsPositiveInfinity.
- When a negative float/double divides by 0, NegativeInfinity is the result. You can check if a number is negative infinity by using IsNegativeInfinity.
See the example below
double d = 20E3;
Console.WriteLine(double.IsPositiveInfinity(d / 0)); // prints true
Console.WriteLine(double.IsNegativeInfinity(d / 0)); // prints false
Console.WriteLine(double.IsInfinity(d / 0)); // prints true
float f = -20E3F;
Console.WriteLine(float.IsPositiveInfinity(f / 0)); // prints false
Console.WriteLine(float.IsNegativeInfinity(f / 0)); // prints true
Console.WriteLine(float.IsInfinity(f / 0)); // prints true
Notice that the method IsInfinity returns true for either positive or negative infinity. If you perform any operation with the variable whose value is infinity, you will get infinity as the result.
double d1 = 20E3;
double d2 = d1 / 0;
Console.WriteLine(double.IsPositiveInfinity(d2 + 90)); // prints true
There is also another special value NaN. When you divide a 0 by 0, you get NaN. It is also available on both float and double types. There is also a method available to check if a value is NaN. This method is IsNaN.
See the example below
double d1 = 0;
double d2 = d1 / 0;
double d3 = d1 / 0;
Console.WriteLine(double.IsNaN(d2)); // prints true
Console.WriteLine(d2 == d3); // prints false
Notice that you can’t compare NaN with equal (==) operator.
Numeric Type Conversions
Type conversions are of two types:
Widening Conversions
A widening conversion happens whenever a type conversion doesn’t make the data loss (but sometimes it can! See below).
Following are the rules regarding widening conversion without any data loss:
- byte can be converted to ushort, short, uint, int, ulong, long, float, double, decimal
- sbyte can be converted to short, int, long, float, double, decimal
- short can be converted to int, long, float, double, decimal
- ushort can be converted to uint, int, ulong, long, float, double, decimal
- char can be converted to ushort, uint, int, ulong, long, float, double, decimal
- int can be converted to long, double, decimal
- uint can be converted to ulong, long, double, decimal
- long, ulong can be converted to decimal
- float can be converted to double
A widening conversion is done automatically and that is why it is also called as implicit conversion.
See the example below
// All assignments below are valid
int i = 27;
long l = i;
byte b = 200;
short s = b;
float f = l;
double d = s;
But there are some widening conversions that can cause loss of precision.
They are:
- int, uint to float
- long, ulong to float, double
- decimal to float, double
Be careful when these happen as they can cause loss of precision.
Narrowing Conversions
Whenever a data of one type is converted to another type and there is a possible data loss, narrowing conversion is required. For example, long can’t be assigned to int because long has much wider range of values in its domain. Narrowing conversion is done explicitly and is also referred to as explicit conversion.
Type casting is a mechanic used to perform explicit conversion. The syntax of type casting is (target type)variable.
See the example below
int i = int.MaxValue;
// int is casted to short but data is lost
short s1 = (short)i;
i = 12;
// type cast needed although the value is within the range of short
s1 = (short)i;
One more caveat: When you perform arithmetic operations on integer type the operands (variables/values involved in arithmetic expression) are automatically promoted to either int or long types signed/unsigned (if not already an int or long type) and then the operation is performed.
byte b1 = 1;
byte b2 = 2;
byte b = b1 + b2; // gives error
In above example the sum of two bytes can’t be assigned back to a byte variable without a cast. So, a type cast will do the work here like
byte b = (byte)(b1 + b2);
This is because for integer type, the arithmetic operators are only defined for int and long types.
Now it is not specifically that all the integer types convert to an int.
In general, the following rules are in place for binary operations involving numeric types:
- If one of the operand is of decimal type, the other operand is converted to type decimal. It raises exception if the other operand is of type float/double.
- Else if either operand is of type double, the other operand is converted to type double.
- Else if either operand is of type float, the other operand is converted to type float.
- Else if either operand is of type ulong, the other operand is converted to type ulong. It raises exception if the other operand is of type sbyte, short, int, or long.
- Else if either operand is of type long, the other operand is converted to type long.
- Else if either operand is of type uint and the other operand is of type sbyte, short, or int, both operands are converted to type long.
- Else if either operand is of type uint, the other operand is converted to type uint.
- Else both operands are converted to type int.
Refer to the following links for more in depth look:
The Overflow Problem
Below is the program that attempts to sum two bytes and assign the result back to a byte.
byte b1 = byte.MaxValue;
byte b2 = 50;
byte b = (byte)(b1 + b2);
Console.WriteLine(b); // prints 49
You can enter the above code in the editor and get surprised that the result is not what is expected to be. The byte max value is 255 and we add 50 to it so the expected result is 255 + 50 = 305. But we get 49 here. Think why?
The reason is simple, a byte can only hold 256 total values (from 0 to 255). So, the sum now contains the overflow value 305 – 256 = 49.
This is the default behavior. When there is an overflow/underflow in numeric operations, it goes unnoticed silently. There is fortunately a mechanism provided by C# to handle this situation. It is the keyword checked. Lets see this keyword in action.
Checked Keyword
When there is an arithmetic expression that you suspect can cause overflow/underflow, just enclose it in the scope of checked keyword like
try
{
byte b1 = byte.MaxValue;
byte b2 = 50;
byte b = checked((byte)(b1 + b2)); // this will throw overflow exception
Console.WriteLine(b);
}
catch (OverflowException ex)
{
Console.WriteLine(ex.Message);
}
The checked keyword will throw an exception if overflow/underflow happens.
When you need to check the whole block of code for overflow/underflow, you can wrap the block in the scope of checked keyword like
try
{
checked
{
byte b1 = byte.MaxValue;
byte b2 = 50;
byte b = (byte)(b1 + b2); // this will throw overflow exception
Console.WriteLine(b);
}
}
catch (OverflowException ex)
{
Console.WriteLine(ex.Message);
}
You can set overflow/underflow checking project wide by entering this into your csproj file.
<PropertyGroup>
<CheckForOverflowUnderflow>true</CheckForOverflowUnderflow>
</PropertyGroup>
The above setting will enable the overflow/underflow checking project wide. But sometimes the data loss is acceptable. To disable overflow exception for a statement/block, C# provides unchecked keyword.
See the example below (project wide overflow/underflow checking is assumed)
unchecked
{
byte b1 = byte.MaxValue;
byte b2 = 50;
byte b = (byte)(b1 + b2); // this will execute silently
Console.WriteLine(b);
}
So this was my effort to look into the basic numeric data types in C#, I hope you have gained some insights into how numeric types in C# work. But programming is a technical discipline that can’t be mastered without deep study along with practice. So have a habit of coding daily for a few hours to really become the master of the trade.
Make sure to follow me for more interesting posts and leave a clap on this post if you liked it. You can also ask any question you might have in the comment section.