C# has the following predefined numeric types:
C# type | System type | Suffix | Size | Range |
---|---|---|---|---|
Integral—signed | ||||
|
| 8 bits | ‒27 to 27‒1 | |
|
| 16 bits | ‒215 to 215‒1 | |
|
| 32 bits | ‒231 to 231‒1 | |
|
|
| 64 bits | ‒263 to 263‒1 |
Integral—unsigned | ||||
---|---|---|---|---|
|
| 8 bits | 0 to 28‒1 | |
|
| 16 bits | 0 to 216‒1 | |
|
|
| 32 bits | 0 to 232‒1 |
|
|
| 64 bits | 0 to 264‒1 |
Real | ||||
---|---|---|---|---|
|
|
| 32 bits | ± (~10‒45 to 1038) |
|
|
| 64 bits | ± (~10‒324 to 10308) |
|
|
| 128 bits | ± (~10‒28 to 1028) |
Of the integral types, int
and long
are first-class citizens and are favored by both C# and the runtime. The
other integral types are typically used for interoperability or when space
efficiency is paramount.
Of the real number types, float
and double
are called floating-point
types and are typically used for scientific calculations. The
decimal
type is typically used for
financial calculations, where base-10-accurate arithmetic and high
precision are required. (Technically, decimal
is a floating-point type too, although
it’s not generally referred to as such.)
Integral literals can use decimal or hexadecimal notation; hexadecimal is denoted with the 0x
prefix (for example, 0x7f
is equivalent to 127
). Real
literals may use decimal or exponential notation, such as 1E06
.
By default, the compiler infers a numeric
literal to be either double
or an
integral type:
If the literal contains a decimal point or the exponential
symbol (E
), it is a double
.
Otherwise, the literal’s type is the first type in this list
that can fit the literal’s value: int
, uint
, long
, and ulong
.
For example:
Console.Write ( 1.0.GetType()); // Double(double)
Console.Write ( 1E06.GetType()); // Double(double)
Console.Write ( 1.GetType()); // Int32(int)
Console.Write (0xF0000000.GetType()); // UInt32(uint)
The numeric suffixes listed in the preceding table explicitly define the type of a literal:
decimal d = 3.5M
; // M = decim
al (case-insensitive)
The suffixes U
and L
are rarely necessary, because the uint
, long
, and ulong
types can nearly always be either
inferred or implicitly
converted from int
:
long i = 5; // Implicit conversion from int to long
The D
suffix is
technically redundant, in that all literals with a decimal point are
inferred to be double (and you can always add a decimal point to a
numeric literal). The F
and M
suffixes are the most useful and are
mandatory when specifying fractional float
or decimal
literals. Without suffixes, the
following would not compile, because 4.5 would be inferred to be of
type double
, which has no implicit
conversion to float
or decimal
:
float f = 4.5F; // Won't compile without suffix decimal d = −1.23M; // Won't compile without suffix
Integral conversions are implicit when the destination type can represent every possible value of the source type. Otherwise, an explicit conversion is required. For example:
int x = 12345; // int is a 32-bit integral long y = x; // Implicit conversion to 64-bit int short z = (short)x; // Explicit conversion to 16-bit int
A float
can be implicitly converted to a double
, since a double
can represent every possible
float
value. The reverse conversion
must be explicit.
Conversions between decimal
and other real types must be explicit.
Conversions from integral types to real types are implicit, whereas
the reverse must be explicit. Converting from a floating-point to an integral truncates
any fractional portion; to perform rounding conversions, use the
static System.Convert
class.
A caveat is that implicitly converting a large integral type to a floating-point type preserves magnitude but may occasionally lose precision:
int i1 = 100000001; float f = i1; // Magnitude preserved, precision lost int i2 = (int)f; // 100000000
The arithmetic operators (+
, −
,
*
, /
, %
) are
defined for all numeric types
except the 8- and 16-bit integral types. The %
operator evaluates
the remainder after division.
The increment and decrement operators (++
, −−
)
increment or decrement numeric types by 1. The operator can either
precede or follow the variable, depending on whether you want the
variable to be updated before or
after the expression is evaluated. For
example:
int x = 0; Console.WriteLine (x++); // Outputs 0; x is now 1 Console.WriteLine (++x); // Outputs 2; x is now 2 Console.WriteLine (−−x); // Outputs 1; x is now 1
Division operations on integral types always truncate remainders.
Dividing by a variable whose value is zero generates a runtime
error (a DivideByZeroException
). Dividing by the
literal 0 generates a compile-time error.
At runtime, arithmetic operations on integral types can overflow. By default, this happens
silently—no exception is thrown. While the C# specification is
agnostic as to the result of an overflow, the CLR always causes
wraparound behavior. For example, decrementing the minimum possible
int
value results in the maximum
possible int
value:
int a = int.MinValue; a−−; Console.WriteLine (a == int.MaxValue); // True
The checked
operator tells the runtime to generate an Over
flow
Exception
rather than failing silently when an integral expression
or statement exceeds the arithmetic limits of that type. The checked
operator affects expressions with
the ++
, −−
, (unary) −
, +
,
−
, *
, /
, and
explicit conversion operators between integral types.
checked
can be used around
either an expression or a statement block. For example:
int a = 1000000, int b = 1000000; int c =checked
(a * b); // Checks just the expressionchecked
// Checks all expressions{
// in statement block. c = a * b; ... }
You can make arithmetic overflow checking the default for all
expressions in a program by compiling with the /checked+
command-line switch (in Visual
Studio, go to Advanced Build Settings). If you then need to disable
overflow checking just for specific expressions or statements, you can
do so with the unchecked
operator.
The 8- and 16-bit integral types are byte
, sbyte
, short
, and ushort
. These types lack their own arithmetic
operators, so C# implicitly converts them to larger types as required. This can cause
a compilation error when trying to assign the result back to a small
integral type:
short x = 1, y = 1; short z = x + y; // Compile-time error
In this case, x
and y
are implicitly converted to int
so that the addition can be performed.
This means the result is also an int
,
which cannot be implicitly cast back to a short
(because it could cause loss of data).
To make this compile, we must add an explicit cast:
short z = (short) (x + y); // OK
Unlike integral types, floating-point types have values that
certain operations treat specially. These special values are NaN (Not a Number), +∞, −∞, and −0. The float
and
double
classes have constants for
NaN, +∞, and −∞ (as well as other values, including MaxValue
, MinValue
, and Epsilon
). For example:
Console.Write (double.NegativeInfinity); // -Infinity
Dividing a nonzero number by zero results in an infinite value:
Console.WriteLine ( 1.0 / 0.0); // Infinity Console.WriteLine (−1.0 / 0.0); // -Infinity Console.WriteLine ( 1.0 / −0.0); // -Infinity Console.WriteLine (−1.0 / −0.0); // Infinity
Dividing zero by zero, or subtracting infinity from infinity, results in a NaN:
Console.Write ( 0.0 / 0.0); // NaN Console.Write ((1.0 / 0.0) − (1.0 / 0.0)); // NaN
When using ==
, a NaN value is
never equal to another value, even another NaN value. To test whether a
value is NaN, you must use the float.IsNaN
or double.IsNaN
method:
Console.WriteLine (0.0 / 0.0 == double.NaN); // False Console.WriteLine (double.IsNaN (0.0 / 0.0)); // True
When using object.Equals
,
however, two NaN values are equal:
bool isTrue = object.Equals (0.0/0.0, double.NaN);
double
is useful for scientific computations (such as computing
spatial coordinates). decimal
is useful for financial computations and values that are
“man-made” rather than the result of real-world measurements. Here’s a summary of the
differences:
Feature | double | decimal |
---|---|---|
Internal representation | Base 2 | Base 10 |
Precision | 15-16 significant figures | 28–29 significant figures |
Range | ±(~10‒324 to ~10308) | ±(~10‒28 to ~1028) |
Special values | +0, ‒0, +∞, ‒∞, and NaN | None |
Speed | Native to processor | Nonnative to processor (about 10 times slower than double) |
float
and double
internally represent numbers in base-2. For this reason,
most literals with a fractional component (which are in base-10) will
not be represented precisely:
float tenth = 0.1f; // Not quite 0.1 float one = 1f; Console.WriteLine (one - tenth * 10f); // −1.490116E-08
This is why float
and double
are bad for financial calculations. In
contrast, decimal
works in base-10
and can precisely represent fractional numbers such as 0.1 (whose
base-10 representation is nonrecurring).