C# is statically typed. Every variable and expression has a type known at compile time
(except where dynamic defers that to run time). All
types belong to one unified type system: every type, value types included,
derives from object (C# specification clause 8).
Kinds of type
Types fall into value types, reference types, type parameters and, in unsafe code, pointer types. A variable of a value type holds its value directly. A variable of a reference type holds a reference to an object; several variables may refer to the same object.
| Kind | Includes | Clause |
|---|---|---|
| Value types | The simple types, enums, structs, tuples, nullable value types | 8.3 |
| Reference types | Classes, object, string, interfaces, arrays, delegates, dynamic | 8.2 |
| Type parameters | The T of a generic type or method | 8.5 |
| Pointer types | int*, void*, function pointers | 24.3 |
Value types are sealed (except for inheriting from System.ValueType),
cannot be null unless wrapped in Nullable, and are copied
on assignment. A reference type’s assignment copies only the reference, which is
what makes two variables refer to the same object.
Built-in types
Each keyword is an alias for a type in System. They are the same type:
int and System.Int32 can be used interchangeably.
| Keyword | .NET type | Size | Range or precision |
|---|---|---|---|
bool | Boolean | 1 byte | true or false; no conversion to or from numbers |
byte | Byte | 8 bits | 0 to 255 |
sbyte | SByte | 8 bits | −128 to 127 |
short | Int16 | 16 bits | −32,768 to 32,767 |
ushort | UInt16 | 16 bits | 0 to 65,535 |
int | Int32 | 32 bits | −2,147,483,648 to 2,147,483,647 |
uint | UInt32 | 32 bits | 0 to 4,294,967,295 |
long | Int64 | 64 bits | −9,223,372,036,854,775,808 to 9,223,372,036,854,775,807 |
ulong | UInt64 | 64 bits | 0 to 18,446,744,073,709,551,615 |
nint | IntPtr | 32 or 64 bits | Native-sized signed integer (C# 9) |
nuint | UIntPtr | 32 or 64 bits | Native-sized unsigned integer (C# 9) |
char | Char | 16 bits | A UTF-16 code unit, U+0000 to U+FFFF |
float | Single | 32 bits | IEEE 754 binary32, about ±1.5 × 10−45 to ±3.4 × 1038, 6 to 9 digits |
double | Double | 64 bits | IEEE 754 binary64, about ±5.0 × 10−324 to ±1.7 × 10308, 15 to 17 digits |
decimal | Decimal | 128 bits | About ±1.0 × 10−28 to ±7.9228 × 1028, 28 to 29 digits |
object | Object | reference | Base type of all types |
string | String | reference | An immutable sequence of UTF-16 code units |
- Types without a keyword, such as
Int128,UInt128,HalfandBigInteger, are library types. decimalis a base-10 type for money and the like. Its operations are exact where the result fits, unlikedouble.- Floating-point operations do not throw. They produce infinities and NaN. Integer operations can overflow; see checked and unchecked.
stringhas value equality with==(clause 8.2.5), but is a reference type.
Default values
Fields and array elements start at the default value of their type (clause 9.3). The
expression default
gives this value.
| Type | Default |
|---|---|
Numeric, char, enums | All bits zero: 0, '\0', the zero member |
bool | false |
| Reference types, nullable types | null |
| Structs | All fields set to their defaults |
Nullable value types
T?, where T is a non-nullable value type, is shorthand for
System.Nullable<T> (C# 2, clause 8.3.12). It holds a T or
no value.
int? a = 5;
int? b = null;
a.HasValue; // true
b.GetValueOrDefault(); // 0
b ?? -1; // -1
int c = a.Value; // InvalidOperationException if no value
if (a is int x) { } // pattern, x is an int
- Operators are lifted to the nullable type (clause 12.4.8): if either operand
of
+and other arithmetic is null, the result is null. - The relational operators
<,>,<=and>=returnfalseif either operand is null. bool?has three-valued&and|(clause 12.16.5):false & nullisfalse, andtrue | nullistrue.- Boxing a nullable value with no value gives a null reference; otherwise it boxes the
T(clause 8.3.13).
Nullable reference types
Nullable reference types (C# 8, clause 8.9) add warnings to help avoid null reference
errors. They change no behaviour at run time: string? and string
are both System.String. The context they apply in is set by
#nullable or the
project’s Nullable property.
#nullable enable
string name = "Ann"; // not nullable
string? middle = null; // nullable reference
int a = name.Length; // fine
int b = middle.Length; // warning: possible null dereference
int c = middle?.Length ?? 0;
int d = middle!.Length; // ! silences the warning only
The compiler tracks the null state of each expression through the code. A
? on a type says that null is expected; without it, null is unexpected. The
null-forgiving operator ! (clause 12.8.9) has no effect at run time. Library
attributes in System.Diagnostics.CodeAnalysis describe what a method does
about null; see Attributes.
Boxing and unboxing
Converting a value type to object, to System.ValueType, or to
an interface it implements boxes it: the value is copied into an object on the
heap (clause 8.3.13). Converting back unboxes it, and copies the value out.
int i = 42;
object box = i; // boxing
int j = (int)box; // unboxing
long l = (long)box; // InvalidCastException: the box holds an int
object n = null;
int k = (int)n; // NullReferenceException
Unboxing must name the exact value type, or its nullable form. A
ref struct
cannot be boxed.
Conversions
A conversion changes an expression from one type to another (clause 10). An
implicit conversion needs no syntax. An explicit conversion needs a
cast, (T)x. Conversions exist between built-in types and can be
user-defined.
Implicit numeric conversions
| From | To |
|---|---|
sbyte | short, int, long, float, double, decimal |
byte | short, ushort, int, uint, long, ulong, float, double, decimal |
short | int, long, float, double, decimal |
ushort | int, uint, long, ulong, float, double, decimal |
int | long, float, double, decimal |
uint | long, ulong, float, double, decimal |
long | float, double, decimal |
ulong | float, double, decimal |
char | ushort, int, uint, long, ulong, float, double, decimal |
float | double |
nint | long, float, double, decimal |
nuint | ulong, float, double, decimal |
sbyte, byte, short, ushort and
int also convert implicitly to nint; byte,
ushort and uint convert implicitly to
nuint.
Conversions from int, uint, long and
ulong to float, and from long and
ulong to double, may lose precision but never magnitude.
Conversions to decimal keep the value where it fits. There are no implicit
conversions to char.
A constant expression of type int converts implicitly to
sbyte, byte, short, ushort,
uint, ulong, nint or nuint if the value fits, and a constant
long converts to ulong (clause 10.2.11). That is why
byte b = 200; compiles.
Explicit conversions
An explicit numeric conversion may lose data. An integer conversion in an unchecked
context drops high bits, or in a
checked context throws
OverflowException. Conversions from floating point to an integer truncate;
if the value is out of range, a checked context throws, and the result is unspecified
in an unchecked one. Conversions to decimal throw
OverflowException when the value is too large, in any context.
All kinds of conversion
| Conversion | Example | Clause |
|---|---|---|
| Identity | int to int, object to dynamic | 10.2.2 |
| Implicit numeric | int to long | 10.2.3 |
| Implicit enum | The constant 0 to any enum | 10.2.4 |
| Interpolated string | $"…" to IFormattable | 10.2.5 |
| Implicit nullable | int to int? | 10.2.6 |
| Null literal | null to any reference or nullable type | 10.2.7 |
| Implicit reference | A class to a base class or interface; array covariance | 10.2.8 |
| Boxing | int to object | 10.2.9 |
| Implicit dynamic | Any expression of type dynamic to any type | 10.2.10 |
| Implicit constant | 200 to byte | 10.2.11 |
| Type parameter | T to its constraint type | 10.2.12 |
| Implicit tuple | (int, int) to (long, long) | 10.2.13 |
| User-defined implicit | implicit operator | 10.2.14 |
| Anonymous function, method group | A lambda to a delegate type | 10.2.15 |
| Default literal | default to any type | 10.2.16 |
| Throw expression | throw e to any type | 10.2.17 |
| Switch expression | A switch to the type each arm converts to | 10.2.18 |
| Object creation | new() to its target type | 10.2.19 |
| Conditional expression | c ? a : b to the target type | 10.2.20 |
| Collection expression | [1, 2] to int[] or a span | 10.2.22 |
| Inline array | An inline array to a span | 10.2.23 |
| Explicit numeric | (int)1.5 | 10.3.2 |
| Explicit enum | (Color)2 | 10.3.3 |
| Explicit nullable | (int)x for int? x | 10.3.4 |
| Explicit reference | Base class to derived class | 10.3.5 |
| Explicit tuple | ((int, int))(1L, 2L) | 10.3.6 |
| Unboxing | (int)o | 10.3.7 |
| User-defined explicit | explicit operator | 10.3.9 |
C# 14 adds implicit conversions between arrays and spans: from
T[] to Span<T> and ReadOnlySpan<T>, from
Span<T> to ReadOnlySpan<T>, from
string to ReadOnlySpan<char>, and from
ReadOnlySpan<U> to ReadOnlySpan<T> when U
has an implicit reference conversion to T (feature specification
First-class span types).
Dynamic
The dynamic type (C# 4, clauses 8.2.4 and 8.7) tells the compiler to bind
operations at run time. It is object in the compiled code, with an
attribute recording that it is dynamic. Any expression converts implicitly to it, and it
converts implicitly to any type. Errors appear at run time as exceptions.
dynamic d = "hello";
Console.WriteLine(d.Length); // 5, found at run time
d = 12;
int n = d + 1; // 13
d.NoSuchMember(); // RuntimeBinderException
Tuples
A tuple groups values in a lightweight value type (C# 7.0, clause 8.3.11). The compiler
maps it to System.ValueTuple, whose fields are mutable. Element names exist
only in source code.
(int Sum, int Count) t = (6, 3);
t.Sum; // 6
t.Item1; // 6, the field name
var pair = (1, "a");
var named = (x: 1, y: 2);
int a = 1, b = 2;
var inferred = (a, b); // named a and b, C# 7.1
(int sum, int count) = t; // deconstruction
(a, b) = (b, a); // swap
var same = (1, 2) == (1, 2); // true, C# 7.3
Tuples with more than seven elements nest the rest in a Rest field. Elements
can be matched with positional
patterns and split with
deconstruction.
Arrays
An array is a reference type holding elements of one type, indexed from zero (clause
17). Every array derives from System.Array. T[] also
implements IList<T>.
int[] a = new int[3]; // zeros
int[] b = new int[] { 1, 2, 3 };
int[] c = { 1, 2, 3 }; // initialiser in a declaration
int[] d = [1, 2, 3]; // collection expression, C# 12
int[,] grid = new int[2, 3]; // rectangular
int[][] jagged = new int[2][]; // array of arrays
a[^1]; // last element, C# 8
b[1..3]; // a copy of a range, C# 8
An out-of-range index throws IndexOutOfRangeException. Arrays of a
reference type are covariant: object[] o = new string[1]; compiles, but
storing a value of the wrong type throws ArrayTypeMismatchException (clause
17.6).
Anonymous types
An anonymous type (C# 3) is a class generated by the compiler from an object
initialiser: var p = new { Name = "Ann", Age = 30 };. Properties are
read-only and Equals compares them. Members of the same name and type, in
the same order, in the same assembly, share a type. They can be copied with
with (C# 10).
Union types
A union type (C# 15, in .NET 11) holds a value of one of a fixed list of case types. Unlike the rest of this page it has no clause number yet: it is described by the Unions feature specification. The .NET 11 SDK is still at the release candidate stage.
public record class Cat(string Name);
public record class Dog(string Name, string Breed);
public union Pet(Cat, Dog);
Pet pet = new Dog("Rex", "Labrador"); // union conversion from each case type
string name = pet switch
{
Dog d => d.Name,
Cat c => c.Name,
};
- The compiler turns a
uniondeclaration into astructmarked with[System.Runtime.CompilerServices.Union]that implementsIUnion. It has one public constructor per case type and aValueproperty of typeobject?, and value-type cases are boxed. - Any type that converts to
objectcan be a case type: classes, structs, interfaces, type parameters, nullable types and other unions. - A body may add members, except instance fields, auto-properties and field-like
events. Declared constructors must delegate to a generated constructor with
this(…). - A type or declaration pattern on a union tests its contents; see Patterns.
- A default union value contains null, so a null arm is advisable where default values can occur.
- Any class or struct with a
[Union]attribute that follows the union pattern is also a union type. It can use other storage, such as without boxing.
Implicit typing
var asks the compiler to infer a local variable’s type from its
initialiser. The variable is still statically typed. See
Variables. A
target-typed expression
such as new() takes its type from where it is used.