C# is statically typed: every variable, field and expression has a type known at compile time. The language has a set of built-in types with keywords of their own (int, double, string...). Each keyword is an alias for a type in the System namespace, so int and System.Int32 are the same type, and string and System.String are the same type.
The built-in types
| Type | .NET type | Size | Range or precision | Literal |
|---|---|---|---|---|
bool | Boolean | 1 byte | true or false | true |
byte | Byte | 1 byte | 0 to 255 | 200 |
sbyte | SByte | 1 byte | -128 to 127 | -5 |
short | Int16 | 2 bytes | -32,768 to 32,767 | 1200 |
ushort | UInt16 | 2 bytes | 0 to 65,535 | 60000 |
int | Int32 | 4 bytes | -2,147,483,648 to 2,147,483,647 | 42 |
uint | UInt32 | 4 bytes | 0 to 4,294,967,295 | 42u |
long | Int64 | 8 bytes | about ±9.2 × 10^18 | 42L |
ulong | UInt64 | 8 bytes | 0 to about 1.8 × 10^19 | 42ul |
float | Single | 4 bytes | about ±3.4 × 10^38, ~6 to 9 digits | 2.5f |
double | Double | 8 bytes | about ±1.8 × 10^308, ~15 to 17 digits | 2.5 or 2.5d |
decimal | Decimal | 16 bytes | about ±7.9 × 10^28, 28 to 29 digits | 2.5m |
char | Char | 2 bytes | one UTF-16 code unit, U+0000 to U+FFFF | 'A' |
string | String | reference | any length of text, immutable | "hello" |
object | Object | reference | the base of every type | new object() |
An integer literal without a suffix is an int, or the first of uint, long and ulong that can hold it if it is too big for an int, and a literal with a decimal point is a double. That is why float f = 2.5; and decimal d = 2.5; fail to compile: the literal is a double, and there is no implicit conversion from double to float or decimal. Add the suffix: 2.5f, 2.5m.
Every numeric type exposes its limits as constants:
Output:
byte: 0 to 255
short: -32768 to 32767
int: -2147483648 to 2147483647
long: -9223372036854775808 to 9223372036854775807
decimal max: 79228162514264337593543950335
double max: 1.798E+308
sizes: int 4, long 8, double 8, char 2
255 161 3000000000
In practice most code uses four of these: int for counts and indexes, long for IDs and large totals, double for measurements and math, and decimal for money.
decimal vs double
double (and float) store numbers in binary. Most decimal fractions, including 0.1, have no exact binary form, so they are stored as the nearest representable value and small errors accumulate. decimal stores a base-10 integer plus a scale, so 0.1 is exactly 0.1.
Output:
False
True
0.30000000000000004
0.3
double sum == 1.0: False
decimal sum == 1.0: True
Total: 59.97
Split three ways: 19.99
Use decimal for money, prices, tax and anything a person will add up by hand and compare. It keeps trailing zeros from its inputs (1.50m prints as 1.50) and has 28 to 29 significant digits.
Use double for physical measurements, statistics, geometry and games. It is several times faster, uses half the memory, and its range is enormous. Never compare two computed doubles with ==; compare the difference against a tolerance: Math.Abs(x - y) < 1e-9.
decimal is not magic: 1m / 3m is still 0.3333333333333333333333333333, and double arithmetic does not throw on division by zero (it produces infinity, or NaN for 0.0 / 0.0), while decimal and integer division by zero throw a DivideByZeroException.
Integer overflow and checked
Integer arithmetic that exceeds the type's range wraps around by default, silently. The compiler catches overflow in constant expressions, but not when the values come from variables:
Output:
-2147483648
OverflowException
-1294967296
2147483648
checked(...) or a checked { ... } block turns overflow into an OverflowException. You can also turn it on for the whole project with <CheckForOverflowUnderflow>true</CheckForOverflowUnderflow>. Writing int x = int.MaxValue + 1; with constants is a compile error (CS0220), because the compiler can see the overflow.
The last line shows the fix for most overflow bugs: convert to a wider type before the arithmetic. (long)(max + 1) would be wrong, since the addition has already wrapped by the time the cast runs.
Value types and reference types
Every C# type is either a value type or a reference type, and the difference decides what assignment does.
- Value types: all numeric types,
bool,char,DateTime, enums, and everystruct. The variable contains the data; assignment copies it. - Reference types:
string, arrays,object, and everyclass. The variable contains a reference to an object; assignment copies the reference, so both variables see the same object.
Output:
a=10 b=99
first[0]=99
s1=cat s2=cats
string is a reference type that behaves like a value in practice: it is immutable, and == compares the text, not the references. More on user-defined value types in structs.
char and bool
A char is one UTF-16 code unit, written in single quotes. It is a numeric type underneath, so it converts to int and supports arithmetic:
Output:
66
C
True
True
7
b
True
bool has exactly two values and does not convert to or from numbers: if (count) is a compile error, not a test for non-zero. Write if (count != 0). Characters outside the Basic Multilingual Plane, such as most emoji, take two char values (a surrogate pair), so a string's Length counts UTF-16 units, not visible characters.
Default values
Fields, array elements and default(T) get the type's default value: zero for numbers, false for bool, '\0' for char, and null for reference types. Local variables get no default and must be assigned before use.
Output:
0
0
False
True
0001-01-01
0,0,0
Since C# 7.1 you can write the default literal without the type when the compiler can infer it (int x = default;). A value type cannot be null, which is what int? (a nullable value type) is for; see nullable types.
Native-sized and big integers
Two more numeric types exist for special cases. nint and nuint (C# 9) are integers the size of a pointer, 32 or 64 bits depending on the platform, used in interop and low-level code. System.Numerics.BigInteger holds integers of any size, at the cost of speed:
using System.Numerics;
BigInteger factorial = 1;
for (int i = 2; i <= 30; i++) factorial *= i;
// 265252859812191058636308480000000
Frequently Asked Questions
What are the data types in C#?
The built-in types are the integers (sbyte, byte, short, ushort, int, uint, long, ulong), the floating-point types float and double, decimal for exact decimal arithmetic, char for a UTF-16 character, bool, and the reference types string and object. Each keyword is an alias for a .NET type: int is System.Int32, string is System.String.
Should I use decimal or double for money in C#?
Use decimal. A double stores binary fractions, so values like 0.1 cannot be represented exactly and sums drift (0.1 + 0.2 == 0.3 is false). A decimal stores base-10 digits, keeps 28 to 29 significant digits, and gives the results accountants expect. double is faster and has a far larger range, so use it for science, graphics and measurements.
What is the max value of int in C#?
int.MaxValue is 2,147,483,647 and int.MinValue is -2,147,483,648 (a 32-bit signed integer). Adding 1 to int.MaxValue silently wraps to int.MinValue unless the code runs in a checked context, where it throws an OverflowException. Use long (up to about 9.2 quintillion) for larger values.
What is the difference between float and double in C#?
float is 32 bits with about 6 to 9 significant digits; double is 64 bits with about 15 to 17. Floating-point literals are double by default, so a float needs the f suffix: float speed = 2.5f;. Use double unless memory or an API (such as Unity's Vector3) calls for float.
What is the difference between value types and reference types in C#?
A value type variable (int, double, bool, DateTime, any struct) holds the data itself, and assignment copies it. A reference type variable (string, arrays, any class) holds a reference to an object on the heap, and assignment copies the reference, so two variables can point at the same object.