Movatterモバイル変換


[0]ホーム

URL:


Skip to main content

This browser is no longer supported.

Upgrade to Microsoft Edge to take advantage of the latest features, security updates, and technical support.

Download Microsoft EdgeMore info about Internet Explorer and Microsoft Edge
Table of contentsExit focus mode

8 Types

  • 2025-04-12
Feedback

In this article

8.1 General

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.

8.2 Reference types

8.2.1 General

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.

8.2.2 Class types

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 typeDescription
System.ObjectThe ultimate base class of all other types. See§8.2.3.
System.StringThe string type of the C# language. See§8.2.5.
System.ValueTypeThe base class of all value types. See§8.3.2.
System.EnumThe base class of allenum types. See§19.5.
System.ArrayThe base class of all array types. See§17.2.2.
System.DelegateThe base class of alldelegate types. See§20.1.
System.ExceptionThe base class of all exception types. See§21.3.

8.2.3 The object type

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.

8.2.4 The dynamic type

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.

8.2.5 The string type

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.

8.2.6 Interface types

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.

8.2.7 Array types

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.

8.2.8 Delegate types

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.

8.3 Value types

8.3.1 General

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.

8.3.2 The System.ValueType type

All value types implicitly inherit from theclassSystem.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.

8.3.3 Default constructors

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:

  • For allsimple_types, the default value is the value produced by a bit pattern of all zeros:
    • Forsbyte,byte,short,ushort,int,uint,long, andulong, the default value is0.
    • Forchar, the default value is'\x0000'.
    • Forfloat, the default value is0.0f.
    • Fordouble, the default value is0.0d.
    • Fordecimal, the default value is0m (that is, value zero with scale 0).
    • Forbool, the default value isfalse.
    • For anenum_typeE, the default value is0, converted to the typeE.
  • For astruct_type, the default value is the value produced by setting all value type fields to their default value and all reference type fields tonull.
  • For anullable_value_type the default value is an instance for which theHasValue 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, variablesi,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).

8.3.4 Struct types

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.

8.3.5 Simple types

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.

KeywordAliased type
sbyteSystem.SByte
byteSystem.Byte
shortSystem.Int16
ushortSystem.UInt16
intSystem.Int32
uintSystem.UInt32
longSystem.Int64
ulongSystem.UInt64
charSystem.Char
floatSystem.Single
doubleSystem.Double
boolSystem.Boolean
decimalSystem.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
  • Throughconst 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.

8.3.6 Integral types

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:

  • Thesbyte type represents signed 8-bit integers with values from-128 to127, inclusive.
  • Thebyte type represents unsigned 8-bit integers with values from0 to255, inclusive.
  • Theshort type represents signed 16-bit integers with values from-32768 to32767, inclusive.
  • Theushort type represents unsigned 16-bit integers with values from0 to65535, inclusive.
  • Theint type represents signed 32-bit integers with values from-2147483648 to2147483647, inclusive.
  • Theuint type represents unsigned 32-bit integers with values from0 to4294967295, inclusive.
  • Thelong type represents signed 64-bit integers with values from-9223372036854775808 to9223372036854775807, inclusive.
  • Theulong type represents unsigned 64-bit integers with values from0 to18446744073709551615, inclusive.
  • Thechar 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: Althoughchar 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:

  • There are no predefined implicit conversions from other types to thechar 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.
  • Constants of thechar 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.

8.3.7 Floating-point types

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:

  • Positive zero and negative zero. In most situations, positive zero and negative zero behave identically as the simple value zero, but certain operations distinguish between the two (§12.10.3).
  • Positive infinity and negative infinity. Infinities are produced by such operations as dividing a non-zero number by zero.

    Example:1.0 / 0.0 yields positive infinity, and–1.0 / 0.0 yields negative infinity.end example

  • TheNot-a-Number value, often abbreviated NaN. NaNs are produced by invalid floating-point operations, such as dividing zero by zero.
  • The finite set of non-zero values of the forms ×m × 2, wheres is 1 or −1, andm ande are determined by the particular floating-point type: Forfloat, 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:

  • The result of a floating-point operation is rounded to the nearest representable value in the destination format.
  • If the magnitude of the result of a floating-point operation is too small for the destination format, the result of the operation becomes positive zero or negative zero.
  • If the magnitude of the result of a floating-point operation is too large for the destination format, the result of the operation becomes positive infinity or negative infinity.
  • If a floating-point operation is invalid, the result of the operation becomes NaN.
  • If one or both operands of a floating-point operation is NaN, the result of the operation becomes NaN.

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 thedouble 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

8.3.8 The Decimal type

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 thatEmineEmax, 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. Fordecimals with an absolute value less than1.0m, the value is exact to at least the 28th decimal place. Fordecimals 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.

8.3.9 The Bool type

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 valuefalse, 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

8.3.10 Enumeration types

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).

8.3.11 Tuple types

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 nestedSystem.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 forpair1,pair2, andpair3 are all valid, with names for no, some or all of the tuple type elements.

The tuple type forpair4 is valid because the namesItem1 andItem2 match their positions, whereas the tuple type forpair5 is disallowed, because the namesItem2 andItem123 do not.

The declarations forpair6 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 theItem name corresponding to their position, as well as by the corresponding tuple element name, if present in the type.end example

8.3.12 Nullable value types

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:

  • AHasValue property of typebool
  • AValue 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.

8.3.13 Boxing and unboxing

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.

8.4 Constructed types

8.4.1 General

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).

8.4.2 Type arguments

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.

8.4.3 Open and closed types

All types can be classified as eitheropen types orclosed types. An open type is a type that involves type parameters. More specifically:

  • A type parameter defines an open type.
  • An array type is an open type if and only if its element type is an open type.
  • A constructed type is an open type if and only if one or more of its type arguments is an open type. A constructed nested type is an open type if and only if one or more of its type arguments or the type arguments of one or more of its containing types is an open type.

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.

8.4.4 Bound and unbound types

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).

8.4.5 Satisfying constraints

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:

  • If the constraint is aclass 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:
    • An identity conversion (§10.2.2)
    • An implicit reference conversion (§10.2.8)
    • A boxing conversion (§10.2.9), provided that typeA is a non-nullable value type.
    • An implicit reference, boxing or type parameter conversion from a type parameterA toC.
  • If the constraint is the reference type constraint (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).
  • If the constraint is the value type constraint (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).
  • If the constraint is the constructor constraintnew(), 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 baseclassB<T>. In contrast,classE 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

8.5 Type parameters

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 literalnull 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.
  • Anew 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.

8.6 Expression tree types

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 toFunc<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 delegatedel 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

8.7 The dynamic type

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:

  • Operations on expressions of typedynamic can be dynamically bound (§12.3.3).
  • Type inference (§12.6.3) will preferdynamic overobject if both are candidates.
  • dynamic cannot be used as
    • the type in anobject_creation_expression (§12.8.17.2)
    • aclass_base (§15.2.4)
    • apredefined_type in amember_access (§12.8.7.1)
    • the operand of thetypeof operator
    • an attribute argument
    • a constraint
    • an extension method type
    • any part of a type argument withinstruct_interfaces (§16.2.5) orinterface_type_list (§15.2.4.1).

Because of this equivalence, the following holds:

  • There is an implicit identity conversion
    • betweenobject anddynamic
    • between constructed types that are the same when replacingdynamic withobject
    • between tuple types that are the same when replacingdynamic withobject
  • Implicit and explicit conversions to and fromobject also apply to and fromdynamic.
  • Signatures that are the same when replacingdynamic withobject are considered the same signature.
  • The typedynamic is indistinguishable from the typeobject at run-time.
  • An expression of the typedynamic is referred to as adynamic expression.

8.8 Unmanaged types

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.
  • Anyenum_type.
  • Any user-definedstruct_type that contains instance fields ofunmanaged_types only.
  • Any type parameter which is constrained to be unmanaged.
  • Anypointer_type (§23.3).

8.9 Reference Types and nullability

8.9.1 General

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:

  • nullable: Anullable-reference-type can be assignednull. Its default null state ismaybe-null.
  • non-nullable: Anon-nullable reference should not be assigned anull value. Its default null state isnot-null.

Note: The typesR 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.

8.9.2 Non-nullable reference types

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.

8.9.3 Nullable reference types

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.

8.9.4 Nullable context

8.9.4.1 General

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

8.9.4.2 Nullable disable

When both the warning and annotations flags are disabled, the nullable context isdisabled.

When the nullable context isdisabled:

  • No warning shall be generated when a variable of an unannotated reference type is initialized with, or assigned a value of,null.
  • No warning shall be generated when a variable of a reference type that possibly has the null value.
  • For any reference typeT, the annotation? inT? generates a message and the typeT? is the same asT.
  • For any type parameter constraintwhere T : C?, the annotation? inC? generates a message and the typeC? is the same asC.
  • For any type parameter constraintwhere T : U?, the annotation? inU? generates a message and the typeU? is the same asU.
  • The generic constraintclass? 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

  • The null-forgiving operator! (§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

8.9.4.3 Nullable annotations

When the warning flag is disabled and the annotations flag is enabled, the nullable context isannotations.

When the nullable context isannotations:

  • For any reference typeT, the annotation? inT? indicates thatT? a nullable type, whereas the unannotatedT is non-nullable.
  • No diagnostic warnings related to nullability are generated.
  • The null-forgiving operator! (§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

8.9.4.4 Nullable warnings

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:

  • A reference variable that has been determined to bemaybe null, is dereferenced.
  • A reference variable of a non-nullable type is assigned to an expression that ismaybe null.
  • The? is used to note a nullable reference type.
  • The null-forgiving operator! (§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

8.9.4.5 Nullable enable

When both the warning flag and the annotations flag are enabled, the nullable context isenabled.

When the nullable context isenabled:

  • For any reference typeT, the annotation? inT? makesT? a nullable type, whereas the unannotatedT is non-nullable.
  • A compiler can use static flow analysis to determine the null state of any reference variable. When nullable warnings are enabled, a reference variable’s null state (§8.9.5) is eithernot null,maybe null, ormaybe default and
  • The null-forgiving operator! (§12.8.9) sets the null state of its operand tonot null.
  • A compiler can issue a warning if the nullability of a type parameter doesn’t match the nullability of its corresponding type argument.

8.9.5 Nullabilities and null states

8.9.5.1 General

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.

8.9.5.2 Flow analysis

A compiler that generates diagnostic warnings conforms to these rules.

Every expression has one of threenull states:

  • maybe null: The value of the expression may evaluate to null.
  • maybe default: The value of the expression may evaluate to the default value for that type.
  • not null: The value of the expression isn’t null.

Thedefault null state of an expression is determined by its type, and the state of the annotations flag when it is declared:

  • The default null state of a nullable reference type is:
    • Maybe null when its declaration is in text where the annotations flag is enabled.
    • Not null when its declaration is in text where the annotations flag is disabled.
  • The default null state of a non-nullable reference type is not null.

Note: Themaybe default state is used with unconstrained type parameters when the type is a non-nullable type, such asstring 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 statementint 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 ofo 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 setP 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 theDisappearingProperty 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

8.9.5.3 Type conversions

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

Collaborate with us on GitHub
The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, seeour contributor guide.

Feedback

Was this page helpful?

YesNo

In this article

Was this page helpful?

YesNo