This browser is no longer supported.
Upgrade to Microsoft Edge to take advantage of the latest features, security updates, and technical support.
Note
Access to this page requires authorization. You can trysigning in orchanging directories.
Access to this page requires authorization. You can trychanging directories.
The types of the C# language are divided into two main categories:reference types andvalue types. Both value types and reference types may begeneric types, which take one or moretype parameters. Type parameters can designate both value types and reference types.
type : reference_type | value_type | type_parameter | pointer_type // unsafe code support ;
pointer_type (§23.3) is available only in unsafe code (§23).
Value types differ from reference types in that variables of the value types directly contain their data, whereas variables of the reference types storereferences to their data, the latter being known asobjects. With reference types, it is possible for two variables to reference the same object, and thus possible for operations on one variable to affect the object referenced by the other variable. With value types, the variables each have their own copy of the data, and it is not possible for operations on one to affect the other.
Note: When a variable is a reference or output parameter, it does not have its own storage but references the storage of another variable. In this case, the ref or out variable is effectively an alias for another variable and not a distinct variable.end note
C#’s type system is unified such thata value of any type can be treated as an object. Every type in C# directly or indirectly derives from theobject
class type, andobject
is the ultimate base class of all types. Values of reference types are treated as objects simply by viewing the values as typeobject
. Values of value types are treated as objects by performing boxing and unboxing operations (§8.3.13).
For convenience, throughout this specification, some library type names are written without using their full name qualification. Refer to§C.5 for more information.
A reference type is a class type, an interface type, an array type, a delegate type, or thedynamic
type. For each non-nullable reference type, there is a corresponding nullable reference type noted by appending the?
to the type name.
reference_type : non_nullable_reference_type | nullable_reference_type ;non_nullable_reference_type : class_type | interface_type | array_type | delegate_type | 'dynamic' ;class_type : type_name | 'object' | 'string' ;interface_type : type_name ;array_type : non_array_type rank_specifier+ ;non_array_type : value_type | class_type | interface_type | delegate_type | 'dynamic' | type_parameter | pointer_type // unsafe code support ;rank_specifier : '[' ','* ']' ;delegate_type : type_name ;nullable_reference_type : non_nullable_reference_type nullable_type_annotation ;nullable_type_annotation : '?' ;
pointer_type is available only in unsafe code (§23.3).nullable_reference_type is discussed further in§8.9.
A reference type value is a reference to aninstance of the type, the latter known as an object. The special valuenull
is compatible with all reference types and indicates the absence of an instance.
A class type defines a data structure that containsdata members (constants and fields),function members (methods, properties, events, indexers, operators, instance constructors, finalizers, and static constructors), and nested types. Class types support inheritance, a mechanism whereby derived classes can extend and specialize base classes. Instances of class types are created usingobject_creation_expressions (§12.8.17.2).
Class types are described in§15.
Certain predefined class types have special meaning in the C# language, as described in the table below.
Class type | Description |
---|---|
System.Object | The ultimate base class of all other types. See§8.2.3. |
System.String | The string type of the C# language. See§8.2.5. |
System.ValueType | The base class of all value types. See§8.3.2. |
System.Enum | The base class of allenum types. See§19.5. |
System.Array | The base class of all array types. See§17.2.2. |
System.Delegate | The base class of alldelegate types. See§20.1. |
System.Exception | The base class of all exception types. See§21.3. |
Theobject
class type is the ultimate base class of all other types. Every type in C# directly or indirectly derives from theobject
class type.
The keywordobject
is simply an alias for the predefined classSystem.Object
.
Thedynamic
type, likeobject
, can reference any object. When operations are applied to expressions of typedynamic
, their resolution is deferred until the program is run. Thus, if the operation cannot legitimately be applied to the referenced object, no error is given during compilation. Instead, an exception will be thrown when resolution of the operation fails at run-time.
Thedynamic
type is further described in§8.7, and dynamic binding in§12.3.1.
Thestring
type is a sealed class type that inherits directly fromobject
. Instances of thestring
class represent Unicode character strings.
Values of thestring
type can be written as string literals (§6.4.5.6).
The keywordstring
is simply an alias for the predefined classSystem.String
.
An interface defines a contract. A class or struct that implements an interface shall adhere to its contract. An interface may inherit from multiple base interfaces, and a class or struct may implement multiple interfaces.
Interface types are described in§18.
An array is a data structure that contains zero or more variables, which are accessed through computed indices. The variables contained in an array, also called the elements of the array, are all of the same type, and this type is called the element type of the array.
Array types are described in§17.
A delegate is a data structure that refers to one or more methods. For instance methods, it also refers to their corresponding object instances.
Note: The closest equivalent of a delegate in C or C++ is a function pointer, but whereas a function pointer can only reference static functions, a delegate can reference both static and instance methods. In the latter case, the delegate stores not only a reference to the method’s entry point, but also a reference to the object instance on which to invoke the method.end note
Delegate types are described in§20.
A value type is either a struct type or an enumeration type. C# provides a set of predefined struct types called thesimple types. The simple types are identified through keywords.
value_type : non_nullable_value_type | nullable_value_type ;non_nullable_value_type : struct_type | enum_type ;struct_type : type_name | simple_type | tuple_type ;simple_type : numeric_type | 'bool' ;numeric_type : integral_type | floating_point_type | 'decimal' ;integral_type : 'sbyte' | 'byte' | 'short' | 'ushort' | 'int' | 'uint' | 'long' | 'ulong' | 'char' ;floating_point_type : 'float' | 'double' ;tuple_type : '(' tuple_type_element (',' tuple_type_element)+ ')' ; tuple_type_element : type identifier? ; enum_type : type_name ;nullable_value_type : non_nullable_value_type nullable_type_annotation ;
Unlike a variable of a reference type, a variable of a value type can contain the valuenull
only if the value type is a nullable value type (§8.3.12). For every non-nullable value type there is a corresponding nullable value type denoting the same set of values plus the valuenull
.
Assignment to a variable of a value type creates acopy of the value being assigned. This differs from assignment to a variable of a reference type, which copies the reference but not the object identified by the reference.
All value types implicitly inherit from theclass
System.ValueType
, which, in turn, inherits from classobject
. It is not possible for any type to derive from a value type, and value types are thus implicitly sealed (§15.2.2.3).
Note thatSystem.ValueType
is not itself avalue_type. Rather, it is aclass_type from which allvalue_types are automatically derived.
All value types implicitly declare a public parameterless instance constructor called thedefault constructor. The default constructor returns a zero-initialized instance known as thedefault value for the value type:
sbyte
,byte
,short
,ushort
,int
,uint
,long
, andulong
, the default value is0
.char
, the default value is'\x0000'
.float
, the default value is0.0f
.double
, the default value is0.0d
.decimal
, the default value is0m
(that is, value zero with scale 0).bool
, the default value isfalse
.E
, the default value is0
, converted to the typeE
.null
.HasValue
property is false. The default value is also known as thenull value of the nullable value type. Attempting to read theValue
property of such a value causes an exception of typeSystem.InvalidOperationException
to be thrown (§8.3.12).Like any other instance constructor, the default constructor of a value type is invoked using thenew
operator.
Note: For efficiency reasons, this requirement is not intended to actually have the implementation generate a constructor call. For value types, the default value expression (§12.8.21) produces the same result as using the default constructor.end note
Example: In the code below, variables
i
,j
andk
are all initialized to zero.class A{ void F() { int i = 0; int j = new int(); int k = default(int); }}
end example
Because every value type implicitly has a public parameterless instance constructor, it is not possible for a struct type to contain an explicit declaration of a parameterless constructor. A struct type is however permitted to declare parameterized instance constructors (§16.4.9).
A struct type is a value type that can declare constants, fields, methods, properties, events, indexers, operators, instance constructors, static constructors, and nested types. The declaration of struct types is described in§16.
C# provides a set of predefinedstruct
types called the simple types. The simple types are identified through keywords, but these keywords are simply aliases for predefinedstruct
types in theSystem
namespace, as described in the table below.
Keyword | Aliased type |
---|---|
sbyte | System.SByte |
byte | System.Byte |
short | System.Int16 |
ushort | System.UInt16 |
int | System.Int32 |
uint | System.UInt32 |
long | System.Int64 |
ulong | System.UInt64 |
char | System.Char |
float | System.Single |
double | System.Double |
bool | System.Boolean |
decimal | System.Decimal |
Because a simple type aliases a struct type, every simple type has members.
Example:
int
has the members declared inSystem.Int32
and the members inherited fromSystem.Object
, and the following statements are permitted:int i = int.MaxValue; // System.Int32.MaxValue constantstring s = i.ToString(); // System.Int32.ToString() instance methodstring t = 123.ToString(); // System.Int32.ToString() instance method
end example
Note: The simple types differ from other struct types in that they permit certain additional operations:
- Most simple types permit values to be created by writingliterals (§6.4.5), although C# makes no provision for literals of struct types in general.Example:
123
is a literal of typeint
and'a'
is a literal of typechar
.end example- When the operands of an expression are all simple type constants, it is possible for a compiler to evaluate the expression at compile-time. Such an expression is known as aconstant_expression (§12.23). Expressions involving operators defined by other struct types are not considered to be constant expressions
- Through
const
declarations, it is possible to declare constants of the simple types (§15.4). It is not possible to have constants of other struct types, but a similar effect is provided by static readonly fields.- Conversions involving simple types can participate in evaluation of conversion operators defined by other struct types, but a user-defined conversion operator can never participate in evaluation of another user-defined conversion operator (§10.5.3).
end note.
C# supports nine integral types:sbyte
,byte
,short
,ushort
,int
,uint
,long
,ulong
, andchar
. The integral types have the following sizes and ranges of values:
sbyte
type represents signed 8-bit integers with values from-128
to127
, inclusive.byte
type represents unsigned 8-bit integers with values from0
to255
, inclusive.short
type represents signed 16-bit integers with values from-32768
to32767
, inclusive.ushort
type represents unsigned 16-bit integers with values from0
to65535
, inclusive.int
type represents signed 32-bit integers with values from-2147483648
to2147483647
, inclusive.uint
type represents unsigned 32-bit integers with values from0
to4294967295
, inclusive.long
type represents signed 64-bit integers with values from-9223372036854775808
to9223372036854775807
, inclusive.ulong
type represents unsigned 64-bit integers with values from0
to18446744073709551615
, inclusive.char
type represents unsigned 16-bit integers with values from0
to65535
, inclusive. The set of possible values for thechar
type corresponds to the Unicode character set.Note: Although
char
has the same representation asushort
, not all operations permitted on one type are permitted on the other.end note
All signed integral types are represented using two’s complement format.
Theintegral_type unary and binary operators always operate with signed 32-bit precision, unsigned 32-bit precision, signed 64-bit precision, or unsigned 64-bit precision, as detailed in§12.4.7.
Thechar
type is classified as an integral type, but it differs from the other integral types in two ways:
char
type. In particular, even though thebyte
andushort
types have ranges of values that are fully representable using thechar
type, implicit conversions from sbyte, byte, orushort
tochar
do not exist.char
type shall be written ascharacter_literals or asinteger_literals in combination with a cast to type char.Example:
(char)10
is the same as'\x000A'
.end example
Thechecked
andunchecked
operators and statements are used to control overflow checking for integral-type arithmetic operations and conversions (§12.8.20). In achecked
context, an overflow produces a compile-time error or causes aSystem.OverflowException
to be thrown. In anunchecked
context, overflows are ignored and any high-order bits that do not fit in the destination type are discarded.
C# supports two floating-point types:float
anddouble
. Thefloat
anddouble
types are represented using the 32-bit single-precision and 64-bit double-precision IEC 60559 formats, which provide the following sets of values:
Example:
1.0 / 0.0
yields positive infinity, and–1.0 / 0.0
yields negative infinity.end example
float
, 0 <m < 2²⁴ and −149 ≤e ≤ 104, and fordouble
, 0 <m < 2⁵³ and −1075 ≤e ≤ 970. Denormalized floating-point numbers are considered valid non-zero values. C# neither requires nor forbids that a conforming implementation support denormalized floating-point numbers.Thefloat
type can represent values ranging from approximately 1.5 × 10⁻⁴⁵ to 3.4 × 10³⁸ with a precision of 7 digits.
Thedouble
type can represent values ranging from approximately 5.0 × 10⁻³²⁴ to 1.7 × 10³⁰⁸ with a precision of 15-16 digits.
If either operand of a binary operator is a floating-point type then standard numeric promotions are applied, as detailed in§12.4.7, and the operation is performed withfloat
ordouble
precision.
The floating-point operators, including the assignment operators, never produce exceptions. Instead, in exceptional situations, floating-point operations produce zero, infinity, or NaN, as described below:
Floating-point operations may be performed with higher precision than the result type of the operation. To force a value of a floating-point type to the exact precision of its type, an explicit cast (§12.9.7) can be used.
Example: Some hardware architectures support an “extended” or “long double” floating-point type with greater range and precision than the
double
type, and implicitly perform all floating-point operations using this higher precision type. Only at excessive cost in performance can such hardware architectures be made to perform floating-point operations withless precision, and rather than require an implementation to forfeit both performance and precision, C# allows a higher precision type to be used for all floating-point operations. Other than delivering more precise results, this rarely has any measurable effects. However, in expressions of the formx * y / z
, where the multiplication produces a result that is outside thedouble
range, but the subsequent division brings the temporary result back into thedouble
range, the fact that the expression is evaluated in a higher range format can cause a finite result to be produced instead of an infinity.end example
Thedecimal
type is a 128-bit data type suitable for financial and monetary calculations. Thedecimal
type can represent values including those in the range at least -7.9 × 10⁻²⁸ to 7.9 × 10²⁸, with at least 28-digit precision.
The finite set of values of typedecimal
are of the form (–1)ᵛ ×c × 10⁻ᵉ, where the signv is 0 or 1, the coefficientc is given by 0 ≤c <Cmax, and the scalee is such thatEmin ≤e ≤Emax, whereCmax is at least 1 × 10²⁸,Emin ≤ 0, andEmax ≥ 28. Thedecimal
type does not necessarily support signed zeros, infinities, or NaN’s.
Adecimal
is represented as an integer scaled by a power of ten. Fordecimal
s with an absolute value less than1.0m
, the value is exact to at least the 28th decimal place. Fordecimal
s with an absolute value greater than or equal to1.0m
, the value is exact to at least 28 digits. Contrary to thefloat
anddouble
data types, decimal fractional numbers such as0.1
can be represented exactly in the decimal representation. In thefloat
anddouble
representations, such numbers often have non-terminating binary expansions, making those representations more prone to round-off errors.
If either operand of a binary operator is ofdecimal
type then standard numeric promotions are applied, as detailed in§12.4.7, and the operation is performed withdouble
precision.
The result of an operation on values of typedecimal
is that which would result from calculating an exact result (preserving scale, as defined for each operator) and then rounding to fit the representation. Results are rounded to the nearest representable value, and, when a result is equally close to two representable values, to the value that has an even number in the least significant digit position (this is known as “banker’s rounding”). That is, results are exact to at least the 28th decimal place. Note that rounding may produce a zero value from a non-zero value.
If adecimal
arithmetic operation produces a result whose magnitude is too large for thedecimal
format, aSystem.OverflowException
is thrown.
Thedecimal
type has greater precision but may have a smaller range than the floating-point types. Thus, conversions from the floating-point types todecimal
might produce overflow exceptions, and conversions fromdecimal
to the floating-point types might cause loss of precision or overflow exceptions. For these reasons, no implicit conversions exist between the floating-point types anddecimal
, and without explicit casts, a compile-time error occurs when floating-point anddecimal
operands are directly mixed in the same expression.
Thebool
type represents Boolean logical quantities. The possible values of typebool
aretrue
andfalse
. The representation offalse
is described in§8.3.3. Although the representation oftrue
is unspecified, it shall be different from that offalse
.
No standard conversions exist betweenbool
and other value types. In particular, thebool
type is distinct and separate from the integral types, abool
value cannot be used in place of an integral value, and vice versa.
Note: In the C and C++ languages, a zero integral or floating-point value, or a null pointer can be converted to the Boolean value
false
, and a non-zero integral or floating-point value, or a non-null pointer can be converted to the Boolean valuetrue
. In C#, such conversions are accomplished by explicitly comparing an integral or floating-point value to zero, or by explicitly comparing an object reference tonull
.end note
An enumeration type is a distinct type with named constants. Every enumeration type has an underlying type, which shall bebyte
,sbyte
,short
,ushort
,int
,uint
,long
orulong
. The set of values of the enumeration type is the same as the set of values of the underlying type. Values of the enumeration type are not restricted to the values of the named constants. Enumeration types are defined through enumeration declarations (§19.2).
A tuple type represents an ordered, fixed-length sequence of values with optional names and individual types. The number of elements in a tuple type is referred to as itsarity. A tuple type is written(T1 I1, ..., Tn In)
with n ≥ 2, where the identifiersI1...In
are optionaltuple element names.
This syntax is shorthand for a type constructed with the typesT1...Tn
fromSystem.ValueTuple<...>
, which shall be a set of generic struct types capable of directly expressing tuple types of any arity between two and seven inclusive.There does not need to exist aSystem.ValueTuple<...>
declaration that directly matches the arity of any tuple type with a corresponding number of type parameters. Instead, tuples with an arity greater than seven are represented with a generic struct typeSystem.ValueTuple<T1, ..., T7, TRest>
that in addition to tuple elements has aRest
field containing a nested value of the remaining elements, using anotherSystem.ValueTuple<...>
type. Such nesting may be observable in various ways, e.g. via the presence of aRest
field. Where only a single additional field is required, the generic struct typeSystem.ValueTuple<T1>
is used; this type is not considered a tuple type in itself. Where more than seven additional fields are required,System.ValueTuple<T1, ..., T7, TRest>
is used recursively.
Element names within a tuple type shall be distinct. A tuple element name of the formItemX
, whereX
is any sequence of non-0
-initiated decimal digits that could represent the position of a tuple element, is only permitted at the position denoted byX
.
The optional element names are not represented in theValueTuple<...>
types, and are not stored in the runtime representation of a tuple value. Identity conversions (§10.2.2) exist between tuples with identity-convertible sequences of element types.
Thenew
operator§12.8.17.2 cannot be applied with the tuple type syntaxnew (T1, ..., Tn)
. Tuple values can be created from tuple expressions (§12.8.6), or by applying thenew
operator directly to a type constructed fromValueTuple<...>
.
Tuple elements are public fields with the namesItem1
,Item2
, etc., and can be accessed via a member access on a tuple value (§12.8.7. Additionally, if the tuple type has a name for a given element, that name can be used to access the element in question.
Note: Even when large tuples are represented with nested
System.ValueTuple<...>
values, each tuple element can still be accessed directly with theItem...
name corresponding to its position.end note
Example: Given the following examples:
(int, string) pair1 = (1, "One");(int, string word) pair2 = (2, "Two");(int number, string word) pair3 = (3, "Three");(int Item1, string Item2) pair4 = (4, "Four");// Error: "Item" names do not match their position(int Item2, string Item123) pair5 = (5, "Five");(int, string) pair6 = new ValueTuple<int, string>(6, "Six");ValueTuple<int, string> pair7 = (7, "Seven");Console.WriteLine($"{pair2.Item1}, {pair2.Item2}, {pair2.word}");
The tuple types for
pair1
,pair2
, andpair3
are all valid, with names for no, some or all of the tuple type elements.The tuple type for
pair4
is valid because the namesItem1
andItem2
match their positions, whereas the tuple type forpair5
is disallowed, because the namesItem2
andItem123
do not.The declarations for
pair6
andpair7
demonstrate that tuple types are interchangeable with constructed types of the formValueTuple<...>
, and that thenew
operator is allowed with the latter syntax.The last line shows that tuple elements can be accessed by the
Item
name corresponding to their position, as well as by the corresponding tuple element name, if present in the type.end example
Anullable value type can represent all values of its underlying type plus an additional null value. A nullable value type is writtenT?
, whereT
is the underlying type. This syntax is shorthand forSystem.Nullable<T>
, and the two forms can be used interchangeably.
Conversely, anon-nullable value type is any value type other thanSystem.Nullable<T>
and its shorthandT?
(for anyT
), plus any type parameter that is constrained to be a non-nullable value type (that is, any type parameter with a value type constraint (§15.2.5)). TheSystem.Nullable<T>
type specifies the value type constraint forT
, which means that the underlying type of a nullable value type can be any non-nullable value type. The underlying type of a nullable value type cannot be a nullable value type or a reference type. For example,int??
is an invalid type. Nullable reference types are covered in§8.9.
An instance of a nullable value typeT?
has two public read-only properties:
HasValue
property of typebool
Value
property of typeT
An instance for whichHasValue
istrue
is said to be non-null. A non-null instance contains a known value andValue
returns that value.
An instance for whichHasValue
isfalse
is said to be null. A null instance has an undefined value. Attempting to read theValue
of a null instance causes aSystem.InvalidOperationException
to be thrown. The process of accessing the Value property of a nullable instance is referred to asunwrapping.
In addition to the default constructor, every nullable value typeT?
has a public constructor with a single parameter of typeT
. Given a valuex
of typeT
, a constructor invocation of the form
new T?(x)
creates a non-null instance ofT?
for which theValue
property isx
. The process of creating a non-null instance of a nullable value type for a given value is referred to aswrapping.
Implicit conversions are available from thenull
literal toT?
(§10.2.7) and fromT
toT?
(§10.2.6).
The nullable value typeT?
implements no interfaces (§18). In particular, this means it does not implement any interface that the underlying typeT
does.
The concept of boxing and unboxing provide a bridge betweenvalue_types andreference_types by permitting any value of avalue_type to be converted to and from typeobject
. Boxing and unboxing enables a unified view of the type system wherein a value of any type can ultimately be treated as anobject
.
Boxing is described in more detail in§10.2.9 and unboxing is described in§10.3.7.
A generic type declaration, by itself, denotes anunbound generic type that is used as a “blueprint” to form many different types, by way of applyingtype arguments. The type arguments are written within angle brackets (<
and>
) immediately following the name of the generic type. A type that includes at least one type argument is called aconstructed type. A constructed type can be used in most places in the language in which a type name can appear. An unbound generic type can only be used within atypeof_expression (§12.8.18).
Constructed types can also be used in expressions as simple names (§12.8.4) or when accessing a member (§12.8.7).
When anamespace_or_type_name is evaluated, only generic types with the correct number of type parameters are considered. Thus, it is possible to use the same identifier to identify different types, as long as the types have different numbers of type parameters. This is useful when mixing generic and non-generic classes in the same program.
Example:
namespace Widgets{ class Queue {...} class Queue<TElement> {...}}namespace MyApplication{ using Widgets; class X { Queue q1; // Non-generic Widgets.Queue Queue<int> q2; // Generic Widgets.Queue }}
end example
The detailed rules for name lookup in thenamespace_or_type_name productions is described in§7.8. The resolution of ambiguities in these productions is described in§6.2.5. Atype_name might identify a constructed type even though it doesn’t specify type parameters directly. This can occur where a type is nested within a genericclass
declaration, and the instance type of the containing declaration is implicitly used for name lookup (§15.3.9.7).
Example:
class Outer<T>{ public class Inner {...} public Inner i; // Type of i is Outer<T>.Inner}
end example
A non-enum constructed type shall not be used as anunmanaged_type (§8.8).
Each argument in a type argument list is simply atype.
type_argument_list : '<' type_argument (',' type_argument)* '>' ;type_argument : type | type_parameter nullable_type_annotation? ;
Each type argument shall satisfy any constraints on the corresponding type parameter (§15.2.5). A reference type argument whose nullability doesn’t match the nullability of the type parameter satisfies the constraint; however a warning may be issued.
All types can be classified as eitheropen types orclosed types. An open type is a type that involves type parameters. More specifically:
A closed type is a type that is not an open type.
At run-time, all of the code within a generic type declaration is executed in the context of a closed constructed type that was created by applying type arguments to the generic declaration. Each type parameter within the generic type is bound to a particular run-time type. The run-time processing of all statements and expressions always occurs with closed types, and open types occur only during compile-time processing.
Two closed constructed types are identity convertible (§10.2.2) if they are constructed from the same unbound generic type, and an identity conversion exists between each of their corresponding type arguments. The corresponding type arguments may themselves be closed constructed types or tuples that are identity convertible. Closed constructed types that are identity convertible share a single set of static variables. Otherwise, each closed constructed type has its own set of static variables. Since an open type does not exist at run-time, there are no static variables associated with an open type.
The termunbound type refers to a non-generic type or an unbound generic type. The termbound type refers to a non-generic type or a constructed type.
An unbound type refers to the entity declared by a type declaration. An unbound generic type is not itself a type, and cannot be used as the type of a variable, argument or return value, or as a base type. The only construct in which an unbound generic type can be referenced is thetypeof
expression (§12.8.18).
Whenever a constructed type or generic method is referenced, the supplied type arguments are checked against the type parameter constraints declared on the generic type or method (§15.2.5). For eachwhere
clause, the type argumentA
that corresponds to the named type parameter is checked against each constraint as follows:
class
type, an interface type, or a type parameter, letC
represent that constraint with the supplied type arguments substituted for any type parameters that appear in the constraint. To satisfy the constraint, it shall be the case that typeA
is convertible to typeC
by one of the following:class
), the typeA
shall satisfy one of the following:A
is an interface type, class type, delegate type, array type or the dynamic type.Note:
System.ValueType
andSystem.Enum
are reference types that satisfy this constraint.end note
A
is a type parameter that is known to be a reference type (§8.2).struct
), the typeA
shall satisfy one of the following:A
is astruct
type orenum
type, but not a nullable value type.Note:
System.ValueType
andSystem.Enum
are reference types that do not satisfy this constraint.end note
A
is a type parameter having the value type constraint (§15.2.5).new()
, the typeA
shall not beabstract
and shall have a public parameterless constructor. This is satisfied if one of the following is true:A
is a value type, since all value types have a public default constructor (§8.3.3).A
is a type parameter having the constructor constraint (§15.2.5).A
is a type parameter having the value type constraint (§15.2.5).A
is aclass
that is not abstract and contains an explicitly declared public constructor with no parameters.A
is notabstract
and has a default constructor (§15.11.5).A compile-time error occurs if one or more of a type parameter’s constraints are not satisfied by the given type arguments.
Since type parameters are not inherited, constraints are never inherited either.
Example: In the following,
D
needs to specify the constraint on its type parameterT
so thatT
satisfies the constraint imposed by the baseclass
B<T>
. In contrast,class
E
need not specify a constraint, becauseList<T>
implementsIEnumerable
for anyT
.class B<T> where T: IEnumerable {...}class D<T> : B<T> where T: IEnumerable {...}class E<T> : B<List<T>> {...}
end example
A type parameter is an identifier designating a value type or reference type that the parameter is bound to at run-time.
type_parameter : identifier ;
Since a type parameter can be instantiated with many different type arguments, type parameters have slightly different operations and restrictions than other types.
Note: These include:
- A type parameter cannot be used directly to declare a base class (§15.2.4.2) or interface (§18.2.4).
- The rules for member lookup on type parameters depend on the constraints, if any, applied to the type parameter. They are detailed in§12.5.
- The available conversions for a type parameter depend on the constraints, if any, applied to the type parameter. They are detailed in§10.2.12 and§10.3.8.
- The literal
null
cannot be converted to a type given by a type parameter, except if the type parameter is known to be a reference type (§10.2.12). However, a default expression (§12.8.21) can be used instead. In addition, a value with a type given by a type parametercan be compared with null using==
and!=
(§12.12.7) unless the type parameter has the value type constraint.- A
new
expression (§12.8.17.2) can only be used with a type parameter if the type parameter is constrained by aconstructor_constraint or the value type constraint (§15.2.5).- A type parameter cannot be used anywhere within an attribute.
- A type parameter cannot be used in a member access (§12.8.7) or type name (§7.8) to identify a static member or a nested type.
- A type parameter cannot be used as anunmanaged_type (§8.8).
end note
As a type, type parameters are purely a compile-time construct. At run-time, each type parameter is bound to a run-time type that was specified by supplying a type argument to the generic type declaration. Thus, the type of a variable declared with a type parameter will, at run-time, be a closed constructed type§8.4.3. The run-time execution of all statements and expressions involving type parameters uses the type that was supplied as the type argument for that parameter.
Expression trees permit lambda expressions to be represented as data structures instead of executable code. Expression trees are values ofexpression tree types of the formSystem.Linq.Expressions.Expression<TDelegate>
, whereTDelegate
is any delegate type. For the remainder of this specification these types will be referred to using the shorthandExpression<TDelegate>
.
If a conversion exists from a lambda expression to a delegate typeD
, a conversion also exists to the expression tree typeExpression<TDelegate>
. Whereas the conversion of a lambda expression to a delegate type generates a delegate that references executable code for the lambda expression, conversion to an expression tree type creates an expression tree representation of the lambda expression. More details of this conversion are provided in§10.7.3.
Example: The following program represents a lambda expression both as executable code and as an expression tree. Because a conversion exists to
Func<int,int>
, a conversion also exists toExpression<Func<int,int>>
:Func<int,int> del = x => x + 1; // CodeExpression<Func<int,int>> exp = x => x + 1; // Data
Following these assignments, the delegate
del
references a method that returnsx + 1
, and the expression tree exp references a data structure that describes the expressionx => x + 1
.end example
Expression<TDelegate>
provides an instance methodCompile
which produces a delegate of typeTDelegate
:
Func<int,int> del2 = exp.Compile();
Invoking this delegate causes the code represented by the expression tree to be executed. Thus, given the definitions above,del
anddel2
are equivalent, and the following two statements will have the same effect:
int i1 = del(1);int i2 = del2(1);
After executing this code,i1
andi2
will both have the value2
.
The API surface provided byExpression<TDelegate>
is implementation-defined beyond the requirement for aCompile
method described above.
Note: While the details of the API provided for expression trees are implementation-defined, it is expected that an implementation will:
- Enable code to inspect and respond to the structure of an expression tree created as the result of a conversion from a lambda expression
- Enable expression trees to be created programatically within user code
end note
The typedynamic
uses dynamic binding, as described in detail in§12.3.2, as opposed to static binding which is used by all other types.
The typedynamic
is considered identical toobject
except in the following respects:
dynamic
can be dynamically bound (§12.3.3).dynamic
overobject
if both are candidates.dynamic
cannot be used astypeof
operatorBecause of this equivalence, the following holds:
object
anddynamic
dynamic
withobject
dynamic
withobject
object
also apply to and fromdynamic
.dynamic
withobject
are considered the same signature.dynamic
is indistinguishable from the typeobject
at run-time.dynamic
is referred to as adynamic expression.unmanaged_type : value_type | pointer_type // unsafe code support ;
Anunmanaged_type is any type that is neither areference_type nor atype_parameter that is not constrained to be unmanaged, and contains no instance fields whose type is not anunmanaged_type. In other words, anunmanaged_type is one of the following:
sbyte
,byte
,short
,ushort
,int
,uint
,long
,ulong
,char
,float
,double
,decimal
, orbool
.Anullable reference type is denoted by appending anullable_type_annotation (?
) to a non-nullable reference type. There is no semantic difference between a non-nullable reference type and its corresponding nullable type, both can either be a reference to an object ornull
. The presence or absence of thenullable_type_annotation declares whether an expression is intended to permit null values or not. A compiler may provide diagnostics when an expression is not used according to that intent. The null state of an expression is defined in§8.9.5. An identity conversion exists among a nullable reference type and its corresponding non-nullable reference type (§10.2.2).
There are two forms of nullability for reference types:
null
. Its default null state ismaybe-null.null
value. Its default null state isnot-null.Note: The types
R
andR?
are represented by the same underlying type,R
. A variable of that underlying type can either contain a reference to an object or be the valuenull
, which indicates “no reference.”end note
The syntactic distinction between anullable reference type and its correspondingnon-nullable reference type enables a compiler to generate diagnostics. A compiler must allow thenullable_type_annotation as defined in§8.2.1. The diagnostics must be limited to warnings. Neither the presence or absence of nullable annotations, nor the state of the nullable context can change the compile time or runtime behavior of a program except for changes in any diagnostic messages generated at compile time.
Anon-nullable reference type is a reference type of the formT
, whereT
is the name of the type. The default null-state of a non-nullable variable isnot-null. Warnings may be generated when an expression that ismaybe-null is used where anot-null value is required.
A reference type of the formT?
(such asstring?
) is anullable reference type. The default null-state of a nullable variable ismaybe null. The annotation?
indicates the intent that variables of this type are nullable. A compiler can recognize these intents to issue warnings. When the nullable annotation context is disabled, using this annotation can generate a warning.
Every line of source code has anullable context. The annotations and warnings flags for the nullable context control nullable annotations (§8.9.4.3) and nullable warnings (§8.9.4.4), respectively. Each flag can beenabled ordisabled. A compiler can use static flow analysis to determine the null state of any reference variable. A reference variable’s null state (§8.9.5) is eithernot null,maybe null, ormaybe default.
The nullable context may be specified within source code via nullable directives (§6.5.9) and/or via some implementation-specific mechanism external to the source code. If both approaches are used, nullable directives supersede the settings made via an external mechanism.
The default state of the nullable context is implementation defined.
Throughout this specification, all C# code that does not contain nullable directives, or about which no statement is made regarding the current nullable context state, shall be assumed to have been compiled using a nullable context where both annotations and warnings are enabled.
Note: A nullable context where both flags are disabled matches the previous standard behavior for reference types.end note
When both the warning and annotations flags are disabled, the nullable context isdisabled.
When the nullable context isdisabled:
null
.T
, the annotation?
inT?
generates a message and the typeT?
is the same asT
.where T : C?
, the annotation?
inC?
generates a message and the typeC?
is the same asC
.where T : U?
, the annotation?
inU?
generates a message and the typeU?
is the same asU
.class?
generates a warning message. The type parameter must be a reference type.Note: This message is characterized as “informational” rather than “warning,” so as not to confuse it with the state of the nullable warning setting, which is unrelated.end note
!
(§12.8.9) has no effect.Example:
#nullable disable annotationsstring? s1 = null; // Informational message; ? is ignoredstring s2 = null; // OK; null initialization of a references2 = null; // OK; null assignment to a referencechar c1 = s2[1]; // OK; no warning on dereference of a possible null; // throws NullReferenceExceptionc1 = s2![1]; // OK; ! is ignored
end example
When the warning flag is disabled and the annotations flag is enabled, the nullable context isannotations.
When the nullable context isannotations:
T
, the annotation?
inT?
indicates thatT?
a nullable type, whereas the unannotatedT
is non-nullable.!
(§12.8.9) may alter the analyzed null state of its operand and what compile time diagnostic warnings are produced.Example:
#nullable disable warnings#nullable enable annotationsstring? s1 = null; // OK; ? makes s2 nullablestring s2 = null; // OK; warnings are disableds2 = null; // OK; warnings are disabledchar c1 = s2[1]; // OK; warnings are disabled; throws NullReferenceExceptionc1 = s2![1]; // No warnings
end example
When the warning flag is enabled and the annotations flag is disabled, the nullable context iswarnings.
When the nullable context iswarnings, a compiler can generate diagnostics in the following cases:
?
is used to note a nullable reference type.!
(§12.8.9) is used to set the null state of its operand tonot null.Example:
#nullable disable annotations#nullable enable warningsstring? s1 = null; // OK; ? makes s2 nullablestring s2 = null; // OK; null-state of s2 is "maybe null"s2 = null; // OK; null-state of s2 is "maybe null"char c1 = s2[1]; // Warning; dereference of a possible null; // throws NullReferenceExceptionc1 = s2![1]; // The warning is suppressed
end example
When both the warning flag and the annotations flag are enabled, the nullable context isenabled.
When the nullable context isenabled:
T
, the annotation?
inT?
makesT?
a nullable type, whereas the unannotatedT
is non-nullable.!
(§12.8.9) sets the null state of its operand tonot null.A compiler is not required to perform any static analysis nor is it required to generate any diagnostic warnings related to nullability.
The remainder of this subclause is conditionally normative.
A compiler that generates diagnostic warnings conforms to these rules.
Every expression has one of threenull states:
Thedefault null state of an expression is determined by its type, and the state of the annotations flag when it is declared:
Note: Themaybe default state is used with unconstrained type parameters when the type is a non-nullable type, such as
string
and the expressiondefault(T)
is the null value. Because null is not in the domain for the non-nullable type, the state is maybe default.end note
A diagnostic can be produced when a variable (§9.2.1) of a non-nullable reference type is initialized or assigned to an expression that is maybe null when that variable is declared in text where the annotation flag is enabled.
Example: Consider the following method where a parameter is nullable and that value is assigned to a non-nullable type:
#nullable enablepublic class C{ public void M(string? p) { // Warning: Assignment of maybe null value to non-nullable variable string s = p; }}
A compiler may issue a warning where the parameter that might be null is assigned to a variable that should not be null. If the parameter is null-checked before assignment, a compiler may use that in its nullable state analysis and not issue a warning:
#nullable enablepublic class C{ public void M(string? p) { if (p != null) { string s = p; // No warning // Use s } }}
end example
A compiler can update the null state of a variable as part of its analysis.
Example: A compiler may choose to update the state based on any statements in your program:
#nullable enablepublic void M(string? p){ int length = p.Length; // Warning: p is maybe null string s = p; // No warning. p is not null if (s != null) { int l2 = s.Length; // No warning. s is not null } int l3 = s.Length; // Warning. s is maybe null}
In the previous example, a compiler may decide that after the statement
int length = p.Length;
, the null-state ofp
is not-null. If it were null, that statement would have thrown aNullReferenceException
. This is similar to the behavior if the code had been preceded byif (p == null) throw NullReferenceException();
except that the code as written may produce a warning, the purpose of which is to warn that an exception may be thrown implicitly.end example
Later in the method, the code checks thats
is not a null reference. The null-state ofs
can change to maybe null after the null-checked block closes. A compiler can infer thats
is maybe null because the code was written to assume that it might have been null. Generally, when the code contains a null check, a compiler may infer that the value might have been null:
Example: Each of the following expressions include some form of a null check. The null-state of
o
can change from not null to maybe null after each of these statements:#nullable enablepublic void M(string s){ int length = s.Length; // No warning. s is not null _ = s == null; // Null check by testing equality. The null state of s is maybe null length = s.Length; // Warning, and changes the null state of s to not null _ = s?.Length; // The ?. is a null check and changes the null state of s to maybe null if (s.Length > 4) // Warning. Changes null state of s to not null { _ = s?[4]; // ?[] is a null check and changes the null state of s to maybe null _ = s.Length; // Warning. s is maybe null }}
Both auto-property and field-like event declarations make use of a compiler-generated backing field. Null state analysis may infer that assignment to the event or property is an assignment to a compiler generated backing field.
Example: A compiler can determine that writing an auto-property or field-like event writes the corresponding compiler generated backing field. The null state of the property matches that of the backing field.
class Test{ public string P { get; set; } public Test() {} // Warning. "P" not set to a non-null value. static void Main() { var t = new Test(); int len = t.P.Length; // No warning. Null state is not null. }}
In the previous example, the constructor doesn’t set
P
to a not null value, and a compiler may issue a warning. There’s no warning when theP
property is accessed, because the type of the property is a non nullable reference type.end example
A compiler can treat a property (§15.7) as either a variable with state, or as independent get and set accessors (§15.7.3).
Example: A compiler can choose whether writing to a property changes the null state of reading the property, or if reading a property changes the null state of that property.
class Test{ private string? _field; public string? DisappearingProperty { get { string tmp = _field; _field = null; return tmp; } set { _field = value; } } static void Main() { var t = new Test(); if (t.DisappearingProperty != null) { int len = t.DisappearingProperty.Length; // No warning. A compiler can assume property is stateful } }}
In the previous example, the backing field for the
DisappearingProperty
is set to null when it is read. However, a compiler may assume that reading a property doesn’t change the null state of that expression.end example
A compiler may use any expression that dereferences a variable, property, or event to set the null state to not null. If it were null, the dereference expression would have thrown aNullReferenceException
:
Example:
public class C{ private C? child; public void M() { _ = child.child.child; // Warning. Dereference possible null value var greatGrandChild = child.child.child; // No warning. }}
end example
A compiler that generates diagnostic warnings conforms to these rules.
Note: Differences in top-level or nested nullability annotations in types do not affect whether conversion between the types is permitted, since there is no semantic difference between a non-nullable reference type and its corresponding nullable type (§8.9.1).end note
A compiler may issue a warning when nullability annotations differ between two types, either top-level or nested, when the conversion is narrowing.
Example: Types differing in top-level annotations
#nullable enablepublic class C{ public void M1(string p) { _ = (string?)p; // No warning, widening } public void M2(string? p) { _ = (string)p; // Warning, narrowing _ = (string)p!; // No warning, suppressed }}
end example
Example: Types differing in nested nullability annotations
#nullable enablepublic class C{ public void M1((string, string) p) { _ = ((string?, string?))p; // No warning, widening } public void M2((string?, string?) p) { _ = ((string, string))p; // Warning, narrowing _ = ((string, string))p!; // No warning, suppressed }}
end example
A compiler may follow rules for interface variance (§18.2.3.3), delegate variance (§20.4), and array covariance (§17.6) in determining whether to issue a warning for type conversions.
#nullable enablepublic class C{ public void M1(IEnumerable<string> p) { IEnumerable<string?> v1 = p; // No warning } public void M2(IEnumerable<string?> p) { IEnumerable<string> v1 = p; // Warning IEnumerable<string> v2 = p!; // No warning } public void M3(Action<string?> p) { Action<string> v1 = p; // No warning } public void M4(Action<string> p) { Action<string?> v1 = p; // Warning Action<string?> v2 = p!; // No warning } public void M5(string[] p) { string?[] v1 = p; // No warning } public void M6(string?[] p) { string[] v1 = p; // Warning string[] v2 = p!; // No warning }}
end example
A compiler may issue a warning when nullability differs in either direction in types which do not permit a variant conversion.
#nullable enablepublic class C{ public void M1(List<string> p) { List<string?> v1 = p; // Warning List<string?> v2 = p!; // No warning } public void M2(List<string?> p) { List<string> v1 = p; // Warning List<string> v2 = p!; // No warning }}
end example
End of conditionally normative text
Was this page helpful?
Was this page helpful?