Compartir a través de


8 tipos

8.1 General

Los tipos del lenguaje C# se dividen en dos categorías principales: tipos de referencia y tipos de valor. Los tipos de valor y los tipos de referencia pueden ser tipos genéricos, que toman uno o varios parámetros de tipo. Los parámetros de tipo pueden designar los tipos de valor y los tipos de referencia.

type
    : reference_type
    | value_type
    | type_parameter
    | pointer_type     // unsafe code support
    ;

pointer_type (§23.3) solo está disponible en código no seguro (§23).

Los tipos de valor difieren de los tipos de referencia en que las variables de los tipos de valor contienen directamente sus datos, mientras que las variables de los tipos de referencia almacenan referencias a sus datos, lo último que se conoce como objetos. Con los tipos de referencia, es posible que dos variables hagan referencia al mismo objeto y, por tanto, es posible que las operaciones en una variable afecten al objeto al que hace referencia la otra variable. Con los tipos de valor, las variables tienen su propia copia de los datos y no es posible que las operaciones de una afecten a la otra.

Nota: Cuando una variable es un parámetro de referencia o salida, no tiene su propio almacenamiento, sino que hace referencia al almacenamiento de otra variable. En este caso, la variable ref o out es efectivamente un alias para otra variable y no una variable distinta. nota final

El sistema de tipos de C#está unificado de modo que un valor de cualquier tipo se pueda tratar como un objeto. Todos los tipos de C# directa o indirectamente se derivan del tipo de clase object, y object es la clase base definitiva de todos los tipos. Los valores de tipos de referencia se tratan como objetos mediante la visualización de los valores como tipo object. Los valores de los tipos de valor se tratan como objetos realizando operaciones de conversión boxing y unboxing (§8.3.13).

Por comodidad, en esta especificación, algunos nombres de tipo de biblioteca se escriben sin usar su calificación de nombre completo. Consulte §C.5 para obtener más información.

8.2 Tipos de referencia

8.2.1 General

Un tipo de referencia es un tipo de clase, un tipo de interfaz, un tipo de matriz, un tipo delegado o el dynamic tipo . Para cada tipo de referencia que no acepta valores NULL, hay un tipo de referencia que acepta valores NULL correspondiente indicado anexando al ? nombre de tipo.

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 solo está disponible en código no seguro (§23.3). nullable_reference_type se analiza más adelante en §8.9.

Un valor de tipo de referencia es una referencia a una instancia del tipo , lo último conocido como un objeto . El valor null especial es compatible con todos los tipos de referencia e indica la ausencia de una instancia.

8.2.2 Tipos de clase

Un tipo de clase define una estructura de datos que contiene miembros de datos (constantes y campos), miembros de función (métodos, propiedades, eventos, indexadores, operadores, constructores de instancias, finalizadores y constructores estáticos) y tipos anidados. Los tipos de clase admiten la herencia, un mecanismo por el que las clases derivadas pueden extender y especializar clases base. Las instancias de tipos de clase se crean mediante object_creation_expression s (§12.8.17.2).

Los tipos de clase se describen en §15.

Algunos tipos de clase predefinidos tienen un significado especial en el lenguaje C#, como se describe en la tabla siguiente.

Tipo de clase Descripción
System.Object Clase base definitiva de todos los demás tipos. Consulte §8.2.3.
System.String Tipo de cadena del lenguaje C#. Consulte §8.2.5.
System.ValueType Clase base de todos los tipos de valor. Consulte §8.3.2.
System.Enum Clase base de todos los enum tipos. Consulte §19.5.
System.Array Clase base de todos los tipos de matriz. Consulte §17.2.2.
System.Delegate Clase base de todos los delegate tipos. Consulte §20.1.
System.Exception La clase base de todos los tipos de excepción. Consulte §21.3.

8.2.3 El tipo de objeto

El object tipo de clase es la clase base definitiva de todos los demás tipos. Cada tipo de C# deriva directa o indirectamente del tipo de object clase .

La palabra clave object es simplemente un alias para la clase System.Objectpredefinida .

8.2.4 El tipo dinámico

El dynamic tipo, como object, puede hacer referencia a cualquier objeto. Cuando las operaciones se aplican a expresiones de tipo dynamic, su resolución se aplaza hasta que se ejecuta el programa. Por lo tanto, si la operación no se puede aplicar legítimamente al objeto al que se hace referencia, no se da ningún error durante la compilación. En su lugar, se producirá una excepción cuando se produzca un error en la resolución de la operación en tiempo de ejecución.

El dynamic tipo se describe más adelante en §8.7 y enlace dinámico en §12.3.1.

8.2.5 El tipo de cadena

El string tipo es un tipo de clase sellado que hereda directamente de object. Las instancias de la string clase representan cadenas de caracteres Unicode.

Los valores del string tipo se pueden escribir como literales de cadena (§6.4.5.6).

La palabra clave string es simplemente un alias para la clase System.Stringpredefinida .

8.2.6 Tipos de interfaz

Una interfaz define un contrato. Una clase o estructura que implemente una interfaz se adhiere a su contrato. Una interfaz puede heredar de varias interfaces base y una clase o estructura puede implementar varias interfaces.

Los tipos de interfaz se describen en §18.

8.2.7 Tipos de matriz

Una matriz es una estructura de datos que contiene cero o más variables, a las que se accede a través de índices calculados. Las variables contenidas en una matriz, denominadas también elementos de la matriz, son todas del mismo tipo y este tipo se conoce como tipo de elemento de la matriz.

Los tipos de matriz se describen en §17.

8.2.8 Tipos delegados

Un delegado es una estructura de datos que hace referencia a uno o varios métodos. Por ejemplo, los métodos también hacen referencia a sus instancias de objeto correspondientes.

Nota: El equivalente más cercano de un delegado en C o C++ es un puntero de función, pero mientras que un puntero de función solo puede hacer referencia a funciones estáticas, un delegado puede hacer referencia a métodos estáticos e de instancia. En este último caso, el delegado almacena no solo una referencia al punto de entrada del método, sino también una referencia a la instancia de objeto en la que se va a invocar el método. nota final

Los tipos de delegado se describen en §20.

8.3 Tipos de valor

8.3.1 General

Un tipo de valor es un tipo de estructura o un tipo de enumeración. C# proporciona un conjunto de tipos de estructura predefinidos denominados tipos simples. Los tipos simples se identifican mediante palabras clave.

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
    ;

A diferencia de una variable de un tipo de referencia, una variable de un tipo de valor solo puede contener el valor null si el tipo de valor es un tipo de valor que acepta valores NULL (§8.3.12). Para cada tipo de valor que no acepta valores NULL, hay un tipo de valor que acepta valores NULL correspondiente que indica el mismo conjunto de valores más el valor null.

La asignación a una variable de un tipo de valor crea una copia del valor que se asigna. Esto difiere de la asignación a una variable de un tipo de referencia, que copia la referencia, pero no el objeto identificado por la referencia.

8.3.2 El tipo System.ValueType

Todos los tipos de valor heredan implícitamente de , class System.ValueTypeque, a su vez, hereda de la clase object. No es posible que ningún tipo derive de un tipo de valor y, por tanto, los tipos de valor estén sellados implícitamente (§15.2.2.3).

Tenga en cuenta que System.ValueType no es un value_type. En su lugar, es un class_type del que se derivan automáticamente todos los value_type.

8.3.3 Constructores predeterminados

Todos los tipos de valor declaran implícitamente un constructor de instancia sin parámetros público denominado constructor predeterminado. El constructor predeterminado devuelve una instancia inicializada de cero conocida como el valor predeterminado para el tipo de valor:

  • Para todos los simple_types, el valor predeterminado es el valor generado por un patrón de bits de todos los ceros:
    • Para sbyte, , byte, short, intushort, uint, longy ulong, el valor predeterminado es 0.
    • En char, el valor predeterminado es '\x0000'.
    • En float, el valor predeterminado es 0.0f.
    • En double, el valor predeterminado es 0.0d.
    • Para decimal, el valor predeterminado es 0m (es decir, el valor cero con la escala 0).
    • En bool, el valor predeterminado es false.
    • Para un enum_type E, el valor predeterminado es 0, convertido al tipo E.
  • Para un struct_type, el valor predeterminado es el valor generado estableciendo todos los campos de tipo de valor en su valor predeterminado y todos los campos de tipo de referencia en null.
  • Para un nullable_value_type el valor predeterminado es una instancia para la que la HasValue propiedad es false. El valor predeterminado también se conoce como el valor NULL del tipo de valor que acepta valores NULL. Si se intenta leer la Value propiedad de este valor, se produce una excepción de tipo System.InvalidOperationException (§8.3.12).

Al igual que cualquier otro constructor de instancia, el constructor predeterminado de un tipo de valor se invoca mediante el new operador .

Nota: Por motivos de eficacia, este requisito no está pensado para que la implementación genere realmente una llamada de constructor. Para los tipos de valor, la expresión de valor predeterminada (§12.8.21) genera el mismo resultado que el uso del constructor predeterminado. nota final

Ejemplo: en el código siguiente, las variables j iy k se inicializan en cero.

class A
{
    void F()
    {
        int i = 0;
        int j = new int();
        int k = default(int);
    }
}

ejemplo final

Dado que cada tipo de valor tiene implícitamente un constructor de instancia sin parámetros público, no es posible que un tipo de estructura contenga una declaración explícita de un constructor sin parámetros. Sin embargo, se permite que un tipo de estructura declare constructores de instancia con parámetros (§16.4.9).

Tipos de estructura 8.3.4

Un tipo de estructura es un tipo de valor que puede declarar constantes, campos, métodos, propiedades, eventos, indexadores, operadores, constructores de instancia, constructores estáticos y tipos anidados. La declaración de tipos de estructura se describe en §16.

8.3.5 Tipos simples

C# proporciona un conjunto de tipos predefinidos struct denominados tipos simples. Los tipos simples se identifican mediante palabras clave, pero estas palabras clave son simplemente alias para tipos predefinidos struct en el System espacio de nombres, como se describe en la tabla siguiente.

Palabra clave Tipo con alias
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

Dado que un tipo simple alias es un tipo de estructura, cada tipo simple tiene miembros.

Ejemplo: int tiene los miembros declarados en System.Int32 y los miembros heredados de System.Objecty se permiten las siguientes instrucciones:

int i = int.MaxValue;      // System.Int32.MaxValue constant
string s = i.ToString();   // System.Int32.ToString() instance method
string t = 123.ToString(); // System.Int32.ToString() instance method

ejemplo final

Nota: Los tipos simples difieren de otros tipos de estructura en que permiten ciertas operaciones adicionales:

  • La mayoría de los tipos simples permiten crear valores escribiendo literales (§6.4.5), aunque C# no aprovisiona literales de tipos de estructura en general. Ejemplo: 123 es un literal de tipo int y 'a' es un literal de tipo char. ejemplo final
  • Cuando los operandos de una expresión son constantes de tipo simples, es posible que el compilador evalúe la expresión en tiempo de compilación. Esta expresión se conoce como constant_expression (§12.23). Las expresiones que implican operadores definidos por otros tipos de estructura no se consideran expresiones constantes
  • A través const de declaraciones, es posible declarar constantes de los tipos simples (§15.4). No es posible tener constantes de otros tipos de estructura, pero se proporciona un efecto similar por campos estáticos de solo lectura.
  • Las conversiones que implican tipos simples pueden participar en la evaluación de operadores de conversión definidos por otros tipos de estructura, pero un operador de conversión definido por el usuario nunca puede participar en la evaluación de otro operador de conversión definido por el usuario (§10.5.3).

nota final.

8.3.6 Tipos enteros

C# admite nueve tipos enteros: sbyte, , ushortbyteshort, int, uint, long, , ulongy char. Los tipos enteros tienen los siguientes tamaños y rangos de valores:

  • El sbyte tipo representa enteros de 8 bits con signo con valores de -128 a 127, ambos incluidos.
  • El byte tipo representa enteros de 8 bits sin signo con valores de 0 a 255, ambos incluidos.
  • El short tipo representa enteros de 16 bits con signo con valores de -32768 a 32767, ambos incluidos.
  • El ushort tipo representa enteros de 16 bits sin signo con valores de 0 a 65535, ambos incluidos.
  • El int tipo representa enteros de 32 bits con signo con valores de -2147483648 a 2147483647, ambos incluidos.
  • El uint tipo representa enteros de 32 bits sin signo con valores de 0 a 4294967295, ambos incluidos.
  • El long tipo representa enteros de 64 bits con signo con valores de -9223372036854775808 a 9223372036854775807, ambos incluidos.
  • El ulong tipo representa enteros de 64 bits sin signo con valores de 0 a 18446744073709551615, ambos incluidos.
  • El char tipo representa enteros de 16 bits sin signo con valores de 0 a 65535, ambos incluidos. El conjunto de valores posibles para el char tipo corresponde al juego de caracteres Unicode.

    Nota: Aunque char tiene la misma representación que ushort, no todas las operaciones permitidas en un tipo se permiten en la otra. nota final

Todos los tipos enteros firmados se representan mediante el formato de complemento de dos.

Los operadores unarios y binarios de integral_type funcionan siempre con precisión de 32 bits con signo, precisión de 32 bits sin signo, precisión de 64 bits con signo o precisión de 64 bits sin signo, como se detalla en §12.4.7.

El char tipo se clasifica como un tipo entero, pero difiere de los otros tipos enteros de dos maneras:

  • No hay conversiones implícitas predefinidas de otros tipos al char tipo . En concreto, aunque los byte tipos y ushort tienen intervalos de valores que son totalmente representables mediante el char tipo, conversiones implícitas de sbyte, byte o ushort para char no existir.
  • Las constantes del char tipo se escribirán como character_literals o como integer_literalen combinación con una conversión a tipo char.

Ejemplo: (char)10 es el mismo que '\x000A'. ejemplo final

Los checked operadores y unchecked las instrucciones se usan para controlar la comprobación de desbordamiento de operaciones aritméticas de tipo entero y conversiones (§12.8.20). En un checked contexto, un desbordamiento genera un error en tiempo de compilación o hace que se produzca una System.OverflowException excepción . En un unchecked contexto, se omiten los desbordamientos y se descartan los bits de orden alto que no se ajusten al tipo de destino.

8.3.7 Tipos de punto flotante

C# admite dos tipos de punto flotante: float y double. Los float tipos y double se representan con los formatos IEC 60559 de precisión simple de 32 bits y de precisión doble de 64 bits, que proporcionan los siguientes conjuntos de valores:

  • Cero positivo y cero negativo. En la mayoría de las situaciones, cero positivo y cero negativo se comportan de forma idéntica como el valor simple cero, pero ciertas operaciones distinguen entre las dos (§12.10.3).
  • Infinito positivo e infinito negativo. Los infinitos se generan mediante operaciones como la división por cero de un número distinto de cero.

    Ejemplo: 1.0 / 0.0 produce infinito positivo y –1.0 / 0.0 produce infinito negativo. ejemplo final

  • El valor Not-a-Number , a menudo abreviado NaN. Los Nan se generan mediante operaciones de punto flotante no válidas, como la división de cero por cero.
  • Conjunto finito de valores distintos de cero del formulario s × m × 2e, donde s es 1 o -1, y m e están determinados por el tipo de punto flotante particular: para float, 0 <m< 2²⁴ y -149 ≤ e ≤ 104, y para double, 0 <m< 2⁵³ y -1075 ≤ e ≤ 970. Los números de punto flotante desnormalizados se consideran valores no cero válidos. C# no requiere ni prohíbe que una implementación conforme admita números de punto flotante desnormalizados.

El float tipo puede representar valores que van de aproximadamente 1,5 × 10⁻⁴⁵ a 3,4 × 10³⁸ con una precisión de 7 dígitos.

El double tipo puede representar valores que van de aproximadamente 5,0 × 10⁻³²⁴ a 1,7 × 10³⁸ con una precisión de 15-16 dígitos.

Si cualquiera de los operandos de un operador binario es un tipo de punto flotante, se aplican promociones numéricas estándar, como se detalla en §12.4.7 y la operación se realiza con float o double precisión.

Los operadores de punto flotante, incluidos los operadores de asignación, nunca generan excepciones. En su lugar, en situaciones excepcionales, las operaciones de punto flotante producen cero, infinito o NaN, como se describe a continuación:

  • El resultado de una operación de punto flotante se redondea al valor representable más cercano en el formato de destino.
  • Si la magnitud del resultado de una operación de punto flotante es demasiado pequeña para el formato de destino, el resultado de la operación se convierte en cero positivo o negativo cero.
  • Si la magnitud del resultado de una operación de punto flotante es demasiado grande para el formato de destino, el resultado de la operación se convierte en infinito positivo o infinito negativo.
  • Si una operación de punto flotante no es válida, el resultado de la operación se convierte en NaN.
  • Si uno o los dos operandos de una operación de punto flotante son NaN, el resultado de la operación se convierte en NaN.

Las operaciones de punto flotante se pueden realizar con mayor precisión que el tipo de resultado de la operación. Para forzar un valor de un tipo de punto flotante a la precisión exacta de su tipo, se puede usar una conversión explícita (§12.9.7).

Ejemplo: algunas arquitecturas de hardware admiten un tipo de punto flotante "extendido" o "long double" con mayor rango y precisión que el double tipo, y realizan implícitamente todas las operaciones de punto flotante con este tipo de precisión superior. Solo a un costo excesivo en el rendimiento se pueden realizar arquitecturas de hardware para realizar operaciones de punto flotante con menos precisión y, en lugar de requerir una implementación para perder el rendimiento y la precisión, C# permite usar un tipo de precisión superior para todas las operaciones de punto flotante. Aparte de ofrecer resultados más precisos, esto rara vez tiene efectos medibles. Sin embargo, en expresiones del formulario x * y / z, donde la multiplicación genera un resultado que está fuera del double intervalo, pero la división posterior devuelve el resultado temporal al double intervalo, el hecho de que la expresión se evalúa en un formato de rango superior puede provocar que se genere un resultado finito en lugar de un infinito. ejemplo final

8.3.8 El tipo decimal

El tipo decimal es un tipo de datos de 128 bits adecuado para cálculos financieros y monetarios. El decimal tipo puede representar valores incluidos los del intervalo al menos -7,9 × 10⁻²⁸ a 7,9 × 10²⁸, con al menos una precisión de 28 dígitos.

El conjunto finito de valores de tipo decimal es de la forma (–1)v × c × 10⁻e, donde el signo v es 0 o 1, el coeficiente c se da por 0 ≤ c<Cmax, y la escala e es tal que EmineEmax, donde Cmax es al menos 1 × 10²⁸, Emin ≤ 0, y Emax ≥ 28. El decimal tipo no admite necesariamente ceros, infinidades o naN firmados.

Un decimal objeto se representa como un entero escalado por una potencia de diez. Para decimals con un valor absoluto menor que 1.0m, el valor es exacto para al menos la posición decimal 28. Para decimals con un valor absoluto mayor o igual que 1.0m, el valor es exacto a al menos 28 dígitos. Al contrario que los float tipos de datos y double , los números fraccionarios decimales como 0.1 se pueden representar exactamente en la representación decimal. En las float representaciones y double , estos números suelen tener expansiones binarias sin terminación, lo que hace que esas representaciones sean más propensas a errores de redondeo.

Si alguno de los operandos de un operador binario es de decimal tipo , se aplican promociones numéricas estándar, como se detalla en §12.4.7 y la operación se realiza con double precisión.

El resultado de una operación en valores de tipo decimal es que el resultado sería calcular un resultado exacto (conservar la escala, según se define para cada operador) y, a continuación, redondear para ajustarse a la representación. Los resultados se redondean al valor representable más cercano y, cuando un resultado está igualmente cerca de dos valores representables, al valor que tiene un número par en la posición de dígito menos significativo (esto se conoce como "redondeo del banco"). Es decir, los resultados son exactos para al menos el 28º decimal. Tenga en cuenta que el redondeo puede producir un valor cero a partir de un valor distinto de cero.

Si una decimal operación aritmética produce un resultado cuya magnitud es demasiado grande para el decimal formato, se produce una System.OverflowException excepción .

El decimal tipo tiene mayor precisión, pero puede tener un intervalo menor que los tipos de punto flotante. Por lo tanto, las conversiones de los tipos de punto flotante a decimal podrían producir excepciones de desbordamiento y las conversiones de decimal a los tipos de punto flotante podrían provocar la pérdida de excepciones de precisión o desbordamiento. Por estos motivos, no existen conversiones implícitas entre los tipos de punto flotante y decimal, y sin conversiones explícitas, se produce un error en tiempo de compilación cuando los operandos y decimal de punto flotante se mezclan directamente en la misma expresión.

8.3.9 El tipo Bool

El bool tipo representa cantidades lógicas booleanas. Los valores posibles de tipo bool son true y false. La representación de false se describe en §8.3.3. Aunque la representación de true no está especificada, será diferente de la de false.

No existen conversiones estándar entre bool y otros tipos de valor. En concreto, el bool tipo es distinto y independiente de los tipos enteros, no se puede usar un bool valor en lugar de un valor entero y viceversa.

Nota: En los lenguajes C y C++, un valor entero o de punto flotante cero, o un puntero nulo se puede convertir en el valor booleano , y un valor entero o de punto flotante distinto de cero, o un puntero que no sea NULL se puede convertir al valor falsetruebooleano . En C#, estas conversiones se realizan comparando explícitamente un valor entero o de punto flotante con cero, o comparando explícitamente una referencia de objeto a null. nota final

8.3.10 Tipos de enumeración

Un tipo de enumeración es un tipo distinto con constantes con nombre. Cada tipo de enumeración tiene un tipo subyacente, que debe ser , , ushort, , int, uinto ulonglong . shortsbytebyte El conjunto de valores del tipo de enumeración es el mismo que el conjunto de valores del tipo subyacente. Los valores del tipo de enumeración no están restringidos a los valores de las constantes con nombre. Los tipos de enumeración se definen mediante declaraciones de enumeración (§19.2).

8.3.11 Tipos de tupla

Un tipo de tupla representa una secuencia ordenada de valores de longitud fija con nombres opcionales y tipos individuales. El número de elementos de un tipo de tupla se conoce como aridad. Un tipo de tupla se escribe (T1 I1, ..., Tn In) con n ≥ 2, donde los identificadores son nombres de elementos de tupla opcionales I1...In .

Esta sintaxis es abreviada para un tipo construido con los tipos T1...Tn de System.ValueTuple<...>, que será un conjunto de tipos de estructura genéricos capaces de expresar directamente los tipos de tupla de cualquier aridad entre dos y siete inclusive. No es necesario existir una System.ValueTuple<...> declaración que coincida directamente con la aridad de cualquier tipo de tupla con un número correspondiente de parámetros de tipo. En su lugar, las tuplas con una aridad mayor que siete se representan con un tipo System.ValueTuple<T1, ..., T7, TRest> de estructura genérico que, además de los elementos de tupla, tiene un Rest campo que contiene un valor anidado de los elementos restantes, utilizando otro System.ValueTuple<...> tipo. Este anidamiento puede ser observable de varias maneras, por ejemplo, a través de la presencia de un Rest campo. Cuando solo se requiere un único campo adicional, se usa el tipo System.ValueTuple<T1> de estructura genérico; este tipo no se considera un tipo de tupla en sí mismo. Cuando se requieren más de siete campos adicionales, System.ValueTuple<T1, ..., T7, TRest> se usa de forma recursiva.

Los nombres de elementos dentro de un tipo de tupla deben ser distintos. Un nombre de elemento de tupla del formulario ItemX, donde X es cualquier secuencia de0 dígitos decimales no iniciados que podrían representar la posición de un elemento de tupla, solo se permite en la posición indicada por X.

Los nombres de elementos opcionales no se representan en los ValueTuple<...> tipos y no se almacenan en la representación en tiempo de ejecución de un valor de tupla. Las conversiones de identidad (§10.2.2) existen entre tuplas con secuencias convertibles de identidad de tipos de elementos.

El new operador §12.8.17.2 no se puede aplicar con la sintaxis new (T1, ..., Tn)de tipo de tupla . Los valores de tupla se pueden crear a partir de expresiones de tupla (§12.8.6) o aplicando el new operador directamente a un tipo construido a partir de ValueTuple<...>.

Los elementos de tupla son campos públicos con los nombres Item1, Item2, etc., y se puede acceder a ellos a través de un acceso de miembro en un valor de tupla (§12.8.7). Además, si el tipo de tupla tiene un nombre para un elemento determinado, ese nombre se puede usar para tener acceso al elemento en cuestión.

Nota: Incluso cuando las tuplas grandes se representan con valores anidados System.ValueTuple<...> , todavía se puede acceder a cada elemento de tupla directamente con el Item... nombre correspondiente a su posición. nota final

Ejemplo: dados los ejemplos siguientes:

(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}");

Los tipos de tupla para pair1, pair2y pair3 son válidos, con nombres para no, algunos o todos los elementos de tipo de tupla.

El tipo de tupla para pair4 es válido porque los nombres Item1 y Item2 coinciden con sus posiciones, mientras que el tipo de tupla para pair5 no se permite, porque los nombres Item2 y Item123 no.

Las declaraciones de pair6 y pair7 demuestran que los tipos de tupla son intercambiables con tipos construidos del formulario ValueTuple<...>y que el new operador se permite con la última sintaxis.

La última línea muestra que se puede tener acceso a los elementos de tupla por el Item nombre correspondiente a su posición, así como por el nombre del elemento de tupla correspondiente, si está presente en el tipo. ejemplo final

8.3.12 Tipos de valor que aceptan valores NULL

Un tipo de valor que acepta valores NULL puede representar todos los valores de su tipo subyacente más un valor NULL adicional. Se escribe T?un tipo de valor que acepta valores NULL, donde T es el tipo subyacente. Esta sintaxis es abreviada para System.Nullable<T>y los dos formularios se pueden usar indistintamente.

Por el contrario, un tipo de valor que no acepta valores NULL es cualquier tipo distinto System.Nullable<T> de y su abreviatura T? (para cualquier ), además de cualquier Tparámetro de tipo restringido para que sea un tipo de valor que no acepta valores NULL (es decir, cualquier parámetro de tipo con una restricción de tipo de valor (§15.2.5)). El System.Nullable<T> tipo especifica la restricción de tipo de valor para T, lo que significa que el tipo subyacente de un tipo de valor que acepta valores NULL puede ser cualquier tipo de valor que no acepta valores NULL. El tipo subyacente de un tipo de valor que acepta valores NULL no puede ser un tipo de valor que acepta valores NULL ni un tipo de referencia. Por ejemplo, int?? es un tipo no válido. Los tipos de referencia que aceptan valores NULL se tratan en §8.9.

Una instancia de un tipo T? de valor que acepta valores NULL tiene dos propiedades públicas de solo lectura:

  • Propiedad HasValue de tipo bool
  • Propiedad Value de tipo T

Una instancia para la que se dice que HasValue no true es NULL. Una instancia que no es null contiene un valor conocido y Value devuelve ese valor.

Instancia para la que se false dice que HasValue es null. Una instancia nula tiene un valor indefinido. Si se intenta leer la Value de una instancia nula, se produce una System.InvalidOperationException excepción . El proceso de acceso a la propiedad Value de una instancia que acepta valores NULL se conoce como desencapsulado.

Además del constructor predeterminado, cada tipo T? de valor que acepta valores NULL tiene un constructor público con un único parámetro de tipo T. Dado un valor x de tipo T, una invocación de constructor del formulario

new T?(x)

crea una instancia que no es NULL de T? para la que la Value propiedad es x. El proceso de creación de una instancia que no es NULL de un tipo de valor que acepta valores NULL para un valor determinado se conoce como ajuste.

Las conversiones implícitas están disponibles desde el null literal a T? (§10.2.7) y de T a T? (§10.2.6).

El tipo T? de valor que acepta valores NULL no implementa interfaces (§18). En concreto, esto significa que no implementa ninguna interfaz que haga el tipo T subyacente.

8.3.13 Boxing y unboxing

El concepto de conversión boxing y unboxing proporciona un puente entre value_types y reference_types al permitir que cualquier valor de un value_type se convierta en y desde el tipo object. Boxing y unboxing habilitan una vista unificada del sistema de tipos en la que, en última instancia, un valor de cualquier tipo se puede tratar como .object

Boxing se describe con más detalle en §10.2.9 y la unboxing se describe en §10.3.7.

8.4 Tipos construidos

8.4.1 General

Una declaración de tipo genérico, por sí misma, denota un tipo genérico sin enlazar que se usa como "plano técnico" para formar muchos tipos diferentes, mediante la aplicación de argumentos de tipo. Los argumentos de tipo se escriben entre corchetes angulares (< y >) inmediatamente después del nombre del tipo genérico. Un tipo que incluye al menos un argumento de tipo se denomina tipo construido. Un tipo construido se puede usar en la mayoría de los lugares del idioma en el que puede aparecer un nombre de tipo. Un tipo genérico no enlazado solo se puede usar dentro de un typeof_expression (§12.8.18).

Los tipos construidos también se pueden usar en expresiones como nombres simples (§12.8.4) o al acceder a un miembro (§12.8.7).

Cuando se evalúa un namespace_or_type_name , solo se tienen en cuenta los tipos genéricos con el número correcto de parámetros de tipo. Por lo tanto, es posible usar el mismo identificador para identificar distintos tipos, siempre y cuando los tipos tengan diferentes números de parámetros de tipo. Esto resulta útil al mezclar clases genéricas y no genéricas en el mismo programa.

Ejemplo:

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

ejemplo final

Las reglas detalladas para la búsqueda de nombres en las producciones de namespace_or_type_name se describen en §7.8. La resolución de ambigüedades en estas producciones se describe en §6.2.5. Un type_name podría identificar un tipo construido aunque no especifique directamente parámetros de tipo. Esto puede ocurrir cuando un tipo está anidado dentro de una declaración genérica class y el tipo de instancia de la declaración contenedora se usa implícitamente para la búsqueda de nombres (§15.3.9.7).

Ejemplo:

class Outer<T>
{
    public class Inner {...}

    public Inner i; // Type of i is Outer<T>.Inner
}

ejemplo final

No se usará un tipo construido distinto de enumeración como unmanaged_type (§8.8).

8.4.2 Argumentos de tipo

Cada argumento de una lista de argumentos de tipo es simplemente un tipo.

type_argument_list
    : '<' type_arguments '>'
    ;

type_arguments
    : type_argument (',' type_argument)*
    ;   

type_argument
    : type
    | type_parameter nullable_type_annotation?
    ;

Cada argumento de tipo cumplirá cualquier restricción en el parámetro de tipo correspondiente (§15.2.5). Argumento de tipo de referencia cuya nulabilidad no coincide con la nulabilidad del parámetro de tipo satisface la restricción; sin embargo, se puede emitir una advertencia.

8.4.3 Tipos abiertos y cerrados

Todos los tipos se pueden clasificar como tipos abiertos o tipos cerrados. Un tipo abierto es un tipo que implica parámetros de tipo. Más concretamente:

  • Un parámetro de tipo define un tipo abierto.
  • Un tipo de matriz es un tipo abierto si y solo si su tipo de elemento es un tipo abierto.
  • Un tipo construido es un tipo abierto si y solo si uno o varios de sus argumentos de tipo es un tipo abierto. Un tipo anidado construido es un tipo abierto si y solo si uno o varios de sus argumentos de tipo o los argumentos de tipo de sus tipos contenedores son un tipo abierto.

Un tipo cerrado es un tipo que no es un tipo abierto.

En tiempo de ejecución, todo el código dentro de una declaración de tipo genérico se ejecuta en el contexto de un tipo construido cerrado que se creó aplicando argumentos de tipo a la declaración genérica. Cada parámetro de tipo dentro del tipo genérico está enlazado a un tipo en tiempo de ejecución determinado. El procesamiento en tiempo de ejecución de todas las instrucciones y expresiones siempre se produce con tipos cerrados y los tipos abiertos solo se producen durante el procesamiento en tiempo de compilación.

Dos tipos construidos cerrados son convertibles de identidad (§10.2.2) si se construyen a partir del mismo tipo genérico sin enlazar y existe una conversión de identidad entre cada uno de sus argumentos de tipo correspondientes. Los argumentos de tipo correspondientes se pueden cerrar tipos o tuplas construidos que son convertibles de identidad. Los tipos construidos cerrados que son convertibles de identidad comparten un único conjunto de variables estáticas. De lo contrario, cada tipo construido cerrado tiene su propio conjunto de variables estáticas. Dado que un tipo abierto no existe en tiempo de ejecución, no hay variables estáticas asociadas a un tipo abierto.

8.4.4 Tipos enlazados y sin enlazar

El término unbound type hace referencia a un tipo no genérico o a un tipo genérico sin enlazar. El término tipo enlazado hace referencia a un tipo no genérico o a un tipo construido.

Un tipo sin enlazar hace referencia a la entidad declarada por una declaración de tipo. Un tipo genérico no enlazado no es un tipo y no se puede usar como el tipo de una variable, argumento o valor devuelto, o como un tipo base. La única construcción en la que se puede hacer referencia a un tipo genérico sin enlazar es la typeof expresión (§12.8.18).

8.4.5 Restricciones satisfactorias

Cada vez que se hace referencia a un tipo construido o método genérico, los argumentos de tipo proporcionados se comprueban con las restricciones de parámetro de tipo declaradas en el tipo o método genéricos (§15.2.5). Para cada where cláusula, el argumento A type que corresponde al parámetro de tipo con nombre se comprueba con cada restricción de la siguiente manera:

  • Si la restricción es un class tipo, un tipo de interfaz o un parámetro de tipo, deje representar C esa restricción con los argumentos de tipo proporcionados sustituidos por los parámetros de tipo que aparecen en la restricción. Para satisfacer la restricción, será el caso de que el tipo A sea convertible al tipo C por uno de los siguientes:
    • Una conversión de identidad (§10.2.2)
    • Conversión de referencia implícita (§10.2.8)
    • Conversión de conversión boxing (§10.2.9), siempre que el tipo A sea un tipo de valor que no acepta valores NULL.
    • Referencia implícita, conversión de parámetros boxing o de tipo de un parámetro A de tipo a C.
  • Si la restricción es la restricción de tipo de referencia (class), el tipo A cumplirá uno de los siguientes requisitos:
    • A es un tipo de interfaz, un tipo de clase, un tipo delegado, un tipo de matriz o el tipo dinámico.

    Nota: System.ValueType y System.Enum son tipos de referencia que cumplen esta restricción. nota final

    • A es un parámetro de tipo que se sabe que es un tipo de referencia (§8.2).
  • Si la restricción es la restricción de tipo de valor (struct), el tipo A cumplirá uno de los siguientes requisitos:
    • A es un struct tipo o enum tipo, pero no un tipo de valor que acepta valores NULL.

    Nota: System.ValueType y System.Enum son tipos de referencia que no cumplen esta restricción. nota final

    • A es un parámetro de tipo que tiene la restricción de tipo de valor (§15.2.5).
  • Si la restricción es la restricción new()constructor , el tipo A no debe ser abstract y tendrá un constructor sin parámetros público. Esto se cumple si se cumple una de las siguientes condiciones:
    • A es un tipo de valor, ya que todos los tipos de valor tienen un constructor predeterminado público (§8.3.3).
    • A es un parámetro de tipo que tiene la restricción de constructor (§15.2.5).
    • A es un parámetro de tipo que tiene la restricción de tipo de valor (§15.2.5).
    • A es un class que no es abstracto y contiene un constructor público declarado explícitamente sin parámetros.
    • A no abstract es y tiene un constructor predeterminado (§15.11.5).

Se produce un error en tiempo de compilación si uno o varios de los argumentos de tipo especificados no cumplen las restricciones de un parámetro de tipo.

Dado que los parámetros de tipo no se heredan, tampoco se heredan restricciones.

Ejemplo: En lo siguiente, D debe especificar la restricción en su parámetro T de tipo para que T satisfaga la restricción impuesta por la base class B<T>. Por el contrario, class E no es necesario especificar una restricción, ya que List<T> implementa IEnumerable para cualquier T.

class B<T> where T: IEnumerable {...}
class D<T> : B<T> where T: IEnumerable {...}
class E<T> : B<List<T>> {...}

ejemplo final

8.5 Parámetros de tipo

Un parámetro de tipo es un identificador que designa un tipo de valor o un tipo de referencia al que está enlazado el parámetro en tiempo de ejecución.

type_parameter
    : identifier
    ;

Dado que se puede crear una instancia de un parámetro de tipo con muchos argumentos de tipo diferentes, los parámetros de tipo tienen operaciones y restricciones ligeramente diferentes que otros tipos.

Nota: Estas incluyen:

  • Un parámetro de tipo no se puede usar directamente para declarar una clase base (§15.2.4.2) ni una interfaz (§18.2.4).
  • Las reglas para la búsqueda de miembros en parámetros de tipo dependen de las restricciones, si las hay, aplicadas al parámetro de tipo. Se detallan en §12.5.
  • Las conversiones disponibles para un parámetro de tipo dependen de las restricciones, si las hay, aplicadas al parámetro de tipo. Se detallan en §10.2.12 y §10.3.8.
  • El literal null no se puede convertir en un tipo dado por un parámetro de tipo, excepto si se sabe que el parámetro de tipo es un tipo de referencia (§10.2.12). Sin embargo, se puede usar en su lugar una expresión predeterminada (§12.8.21). Además, un valor con un tipo proporcionado por un parámetro de tipo se puede comparar con null mediante == y != (§12.12.7), a menos que el parámetro de tipo tenga la restricción de tipo de valor.
  • Una new expresión (§12.8.17.2) solo se puede usar con un parámetro de tipo si el parámetro de tipo está restringido por un constructor_constraint o la restricción de tipo de valor (§15.2.5).
  • Un parámetro de tipo no se puede usar en ningún lugar dentro de un atributo.
  • No se puede usar un parámetro de tipo en un acceso de miembro (§12.8.7) o en un nombre de tipo (§7.8) para identificar un miembro estático o un tipo anidado.
  • No se puede usar un parámetro de tipo como unmanaged_type (§8.8).

nota final

Como tipo, los parámetros de tipo son puramente una construcción en tiempo de compilación. En tiempo de ejecución, cada parámetro de tipo está enlazado a un tipo en tiempo de ejecución que se especificó proporcionando un argumento de tipo a la declaración de tipo genérico. Por lo tanto, el tipo de una variable declarada con un parámetro de tipo será, en tiempo de ejecución, un tipo construido cerrado §8.4.3. La ejecución en tiempo de ejecución de todas las instrucciones y expresiones que implican parámetros de tipo usa el tipo que se proporcionó como argumento de tipo para ese parámetro.

8.6 Tipos de árbol de expresiones

Los árboles de expresión permiten representar expresiones lambda como estructuras de datos en lugar de código ejecutable. Los árboles de expresión son valores de tipos de árbol de expresión del formulario System.Linq.Expressions.Expression<TDelegate>, donde TDelegate es cualquier tipo de delegado. Para el resto de esta especificación, estos tipos se harán referencia al uso de la abreviatura Expression<TDelegate>.

Si existe una conversión de una expresión lambda a un tipo Ddelegado , también existe una conversión al tipo Expression<TDelegate>de árbol de expresión . Mientras que la conversión de una expresión lambda a un tipo delegado genera un delegado que hace referencia al código ejecutable para la expresión lambda, la conversión a un tipo de árbol de expresión crea una representación de árbol de expresión de la expresión lambda. En §10.7.3 se proporcionan más detalles de esta conversión.

Ejemplo: el siguiente programa representa una expresión lambda como código ejecutable y como árbol de expresiones. Dado que existe una conversión en Func<int,int>, también existe una conversión en Expression<Func<int,int>>:

Func<int,int> del = x => x + 1;             // Code
Expression<Func<int,int>> exp = x => x + 1; // Data

Después de estas asignaciones, el delegado del hace referencia a un método que devuelve x + 1y la expresión tree exp hace referencia a una estructura de datos que describe la expresión x => x + 1.

ejemplo final

Expression<TDelegate> proporciona un método Compile de instancia que genera un delegado de tipo TDelegate:

Func<int,int> del2 = exp.Compile();

Invocar este delegado hace que se ejecute el código representado por el árbol de expresiones. Por lo tanto, dadas las definiciones anteriores, del y del2 son equivalentes, y las dos instrucciones siguientes tendrán el mismo efecto:

int i1 = del(1);
int i2 = del2(1);

Después de ejecutar este código y i1 i2 tendrá el valor 2.

La superficie de API proporcionada por Expression<TDelegate> es la implementación definida más allá del requisito de un Compile método descrito anteriormente.

Nota: Aunque los detalles de la API proporcionada para los árboles de expresión están definidos por la implementación, se espera que una implementación:

  • Habilite el código para inspeccionar y responder a la estructura de un árbol de expresión creado como resultado de una conversión de una expresión lambda.
  • Habilitación de árboles de expresión para crearse mediante programación en el código de usuario

nota final

8.7 El tipo dinámico

El tipo dynamic usa el enlace dinámico, como se describe en detalle en §12.3.2, en lugar de en el enlace estático que usan todos los demás tipos.

El tipo dynamic se considera idéntico a object excepto en los siguientes aspectos:

  • Las operaciones en expresiones de tipo dynamic se pueden enlazar dinámicamente (§12.3.3).
  • La inferencia de tipos (§12.6.3) preferirá dynamic si object ambas son candidatas.
  • dynamic no se puede usar como
    • el tipo de un object_creation_expression (§12.8.17.2)
    • un class_base (§15.2.4)
    • un predefined_type en un member_access (§12.8.7.1)
    • operando del typeof operador
    • un argumento de atributo
    • una restricción
    • un tipo de método de extensión
    • cualquier parte de un argumento de tipo dentro de struct_interfaces (§16.2.5) o interface_type_list (§15.2.4.1).

Debido a esta equivalencia, se mantiene lo siguiente:

  • Hay una conversión de identidad implícita
    • entre object y dynamic
    • entre tipos construidos que son los mismos al reemplazar por dynamicobject
    • entre tipos de tupla que son iguales al reemplazar por dynamicobject
  • Las conversiones implícitas y explícitas a y desde object también se aplican a y desde dynamic.
  • Las firmas que son las mismas al reemplazar dynamic por object se consideran la misma firma.
  • El tipo dynamic es indistinguible del tipo object en tiempo de ejecución.
  • Una expresión del tipo dynamic se conoce como una expresión dinámica.

8.8 Tipos no administrados

unmanaged_type
    : value_type
    | pointer_type     // unsafe code support
    ;

Un unmanaged_type es cualquier tipo que no sea un reference_type, un type_parameter o un tipo construido y no contenga campos de instancia cuyo tipo no sea un unmanaged_type. En otras palabras, un unmanaged_type es uno de los siguientes:

  • sbyte, byte, short, ushort, int, uint, long, ulongchardoublefloat, , o . decimalbool
  • Cualquier enum_type.
  • Cualquier struct_type definido por el usuario que no sea un tipo construido y contenga campos de instancia de solo unmanaged_type.
  • En código no seguro (§23.2), cualquier pointer_type (§23.3).

8.9 Tipos de referencia y nulabilidad

8.9.1 General

Un tipo de referencia que acepta valores NULL se indica anexando un nullable_type_annotation (?) a un tipo de referencia que no acepta valores NULL. No hay ninguna diferencia semántica entre un tipo de referencia que no acepta valores NULL y su tipo que acepta valores NULL correspondientes, ambos pueden ser una referencia a un objeto o null. La presencia o ausencia del nullable_type_annotation declara si una expresión está pensada para permitir valores NULL o no. Un compilador puede proporcionar diagnósticos cuando no se usa una expresión según esa intención. El estado NULL de una expresión se define en §8.9.5. Existe una conversión de identidad entre un tipo de referencia que acepta valores NULL y su correspondiente tipo de referencia que no acepta valores NULL (§10.2.2).

Hay dos formas de nulabilidad para los tipos de referencia:

  • nullable: se puede asignar nullun tipo de referencia que acepta valores NULL. Su estado null predeterminado es tal vez null.
  • que no acepta valores NULL: no se debe asignar un valor a una null referencia que no acepta valores NULL. Su estado null predeterminado no es null.

Nota: Los tipos R y R? se representan mediante el mismo tipo subyacente, R. Una variable de ese tipo subyacente puede contener una referencia a un objeto o ser el valor null, que indica "sin referencia". nota final

La distinción sintáctica entre un tipo de referencia que acepta valores NULL y su correspondiente tipo de referencia que no acepta valores NULL permite que un compilador genere diagnósticos. Un compilador debe permitir el nullable_type_annotation tal como se define en §8.2.1. Los diagnósticos deben limitarse a las advertencias. Ni la presencia o ausencia de anotaciones que aceptan valores NULL ni el estado del contexto que acepta valores NULL pueden cambiar el tiempo de compilación o el comportamiento en tiempo de ejecución de un programa, excepto los cambios en los mensajes de diagnóstico generados en tiempo de compilación.

8.9.2 Tipos de referencia que no aceptan valores NULL

Un tipo de referencia que no acepta valores NULL es un tipo de referencia del formulario T, donde T es el nombre del tipo. El estado null predeterminado de una variable que no acepta valores NULL no es NULL. Es posible que se generen advertencias cuando se usa una expresión que es tal vez null cuando se requiere un valor no NULL .

8.9.3 Tipos de referencia que aceptan valores NULL

Un tipo de referencia del formulario T? (como string?) es un tipo de referencia que acepta valores NULL. El estado null predeterminado de una variable que acepta valores NULL puede ser NULL. La anotación ? indica la intención de que las variables de este tipo aceptan valores NULL. El compilador puede reconocer estas intenciones para emitir advertencias. Cuando el contexto de anotación que acepta valores NULL está deshabilitado, el uso de esta anotación puede generar una advertencia.

8.9.4 Contexto que acepta valores NULL

8.9.4.1 General

Cada línea de código fuente tiene un contexto que acepta valores NULL. Las anotaciones y advertencias marcan para las anotaciones que aceptan valores NULL (§8.9.4.3) y las advertencias que aceptan valores NULL (§8.9.4.4), respectivamente. Cada marca se puede habilitar o deshabilitar. El compilador puede usar el análisis de flujo estático para determinar el estado NULL de cualquier variable de referencia. El estado null de una variable de referencia (§8.9.5) no es null, tal vez null o tal vez predeterminado.

El contexto que acepta valores NULL puede especificarse en el código fuente a través de directivas que aceptan valores NULL (§6.5.9) o a través de algún mecanismo específico de la implementación externo al código fuente. Si se usan ambos enfoques, las directivas que aceptan valores NULL reemplazan la configuración realizada a través de un mecanismo externo.

El estado predeterminado del contexto que acepta valores NULL es la implementación definida.

A lo largo de esta especificación, se supone que todo el código de C# que no contiene directivas que aceptan valores NULL o sobre las que no se realiza ninguna instrucción con respecto al estado de contexto que acepta valores NULL actual, se supone que se ha compilado mediante un contexto que acepta valores NULL donde se habilitan las anotaciones y las advertencias.

Nota: Un contexto que acepta valores NULL donde ambas marcas están deshabilitadas coinciden con el comportamiento estándar anterior para los tipos de referencia. nota final

Deshabilitación que acepta valores NULL 8.9.4.2

Cuando las marcas de advertencia y anotaciones están deshabilitadas, el contexto que acepta valores NULL está deshabilitado.

Cuando el contexto que acepta valores NULL está deshabilitado:

  • No se generará ninguna advertencia cuando se inicialice una variable de un tipo de referencia no anotado con o se le asigne un valor de . null
  • No se generará ninguna advertencia cuando una variable de un tipo de referencia que posiblemente tenga el valor NULL.
  • Para cualquier tipo Tde referencia , la anotación ? de T? genera un mensaje y el tipo T? es el mismo que T.
  • Para cualquier restricción where T : C?de parámetro de tipo , la anotación ? de C? genera un mensaje y el tipo C? es el mismo que C.
  • Para cualquier restricción where T : U?de parámetro de tipo , la anotación ? de U? genera un mensaje y el tipo U? es el mismo que U.
  • La restricción class? genérica genera un mensaje de advertencia. El parámetro type debe ser un tipo de referencia.

    Nota: Este mensaje se caracteriza como "informativo" en lugar de "advertencia", por lo que no se debe confundir con el estado de la configuración de advertencia que acepta valores NULL, que no está relacionada. nota final

  • El operador ! null-forgiving (§12.8.9) no tiene ningún efecto.

Ejemplo:

#nullable disable annotations
string? s1 = null;    // Informational message; ? is ignored
string s2 = null;     // OK; null initialization of a reference
s2 = null;            // OK; null assignment to a reference
char c1 = s2[1];      // OK; no warning on dereference of a possible null;
                      //     throws NullReferenceException
c1 = s2![1];          // OK; ! is ignored

ejemplo final

8.9.4.3 Anotaciones que aceptan valores NULL

Cuando la marca de advertencia está deshabilitada y la marca de anotaciones está habilitada, el contexto que acepta valores NULL es anotaciones.

Cuando el contexto que acepta valores NULL es anotaciones:

  • Para cualquier tipo Tde referencia , la anotación ? de indica T? que un tipo que acepta valores NULL, mientras que T? el valor no anotado T no acepta valores NULL.
  • No se generan advertencias de diagnóstico relacionadas con la nulabilidad.
  • El operador ! null-forgiving (§12.8.9) puede modificar el estado nulo analizado de su operando y qué advertencias de diagnóstico en tiempo de compilación se generan.

Ejemplo:

#nullable disable warnings
#nullable enable annotations
string? s1 = null;    // OK; ? makes s2 nullable
string s2 = null;     // OK; warnings are disabled
s2 = null;            // OK; warnings are disabled
char c1 = s2[1];      // OK; warnings are disabled; throws NullReferenceException
c1 = s2![1];          // No warnings

ejemplo final

8.9.4.4 Advertencias que aceptan valores NULL

Cuando la marca de advertencia está habilitada y la marca de anotaciones está deshabilitada, el contexto que acepta valores NULL es advertencias.

Cuando el contexto que acepta valores NULL es advertencias, un compilador puede generar diagnósticos en los casos siguientes:

  • Se desreferencia una variable de referencia que se ha determinado que puede ser null.
  • Una variable de referencia de un tipo que no acepta valores NULL se asigna a una expresión que puede ser NULL.
  • ? se usa para anotar un tipo de referencia que acepta valores NULL.
  • El operador ! null-forgiving (§12.8.9) se usa para establecer el estado NULL de su operando en no null.

Ejemplo:

#nullable disable annotations
#nullable enable warnings
string? s1 = null;    // OK; ? makes s2 nullable
string 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 NullReferenceException
c1 = s2![1];          // The warning is suppressed

ejemplo final

8.9.4.5 Habilitación que acepta valores NULL

Cuando la marca de advertencia y la marca de anotaciones están habilitadas, se habilita el contexto que acepta valores NULL.

Cuando el contexto que acepta valores NULL está habilitado:

  • Para cualquier tipo Tde referencia , la anotación ? de T? hace T? que un tipo que acepta valores NULL, mientras que el valor no anotado T no acepta valores NULL.
  • El compilador puede usar el análisis de flujo estático para determinar el estado NULL de cualquier variable de referencia. Cuando se habilitan las advertencias que aceptan valores NULL, el estado NULL de una variable de referencia (§8.9.5) no es null, tal vez null o tal vez predeterminado y
  • El operador ! null-forgiving (§12.8.9) establece el estado NULL de su operando en no null.
  • El compilador puede emitir una advertencia si la nulabilidad de un parámetro de tipo no coincide con la nulabilidad de su argumento de tipo correspondiente.

8.9.5 Valores NULLabilities y estados NULL

Un compilador no es necesario para realizar ningún análisis estático ni para generar ninguna advertencia de diagnóstico relacionada con la nulabilidad.

El resto de esta subclausa es condicionalmente normativo.

Un compilador que genera advertencias de diagnóstico se ajusta a estas reglas.

Cada expresión tiene uno de los tres estadosNULL:

  • tal vez null: el valor de la expresión puede evaluarse como NULL.
  • tal vez el valor predeterminado: el valor de la expresión puede evaluarse como el valor predeterminado de ese tipo.
  • not null: el valor de la expresión no es NULL.

El estado null predeterminado de una expresión viene determinado por su tipo y el estado de la marca de anotaciones cuando se declara:

  • El estado null predeterminado de un tipo de referencia que acepta valores NULL es:
    • Tal vez null cuando su declaración está en texto donde está habilitada la marca de anotaciones.
    • No es NULL cuando su declaración está en texto donde se deshabilita la marca de anotaciones.
  • El estado null predeterminado de un tipo de referencia que no acepta valores NULL no es NULL.

Nota: El estado tal vez predeterminado se usa con parámetros de tipo sin restricciones cuando el tipo es un tipo que no acepta valores NULL, como string y la expresión default(T) es el valor NULL. Dado que null no está en el dominio para el tipo que no acepta valores NULL, el estado puede ser predeterminado. nota final

Se puede producir un diagnóstico cuando se inicializa o asigna una variable (§9.2.1) de un tipo de referencia que no acepta valores NULL o se asigna a una expresión que puede ser null cuando esa variable se declara en texto donde está habilitada la marca de anotación.

Ejemplo: considere el siguiente método donde un parámetro acepta valores NULL y ese valor se asigna a un tipo que no acepta valores NULL:

#nullable enable
public class C
{
    public void M(string? p)
    {
        // Assignment of maybe null value to non-nullable variable
        string s = p;
    }
}

El compilador puede emitir una advertencia en la que el parámetro que podría ser NULL se asigna a una variable que no debe ser NULL. Si el parámetro está comprobado en null antes de la asignación, el compilador puede usarlo en su análisis de estado que acepta valores NULL y no emite una advertencia:

#nullable enable
public class C
{
    public void M(string? p)
    {
        if (p != null)
        {
            string s = p;
            // Use s
        }
    }
}

ejemplo final

El compilador puede actualizar el estado NULL de una variable como parte de su análisis.

Ejemplo: el compilador puede optar por actualizar el estado en función de las instrucciones del programa:

#nullable enable
public void M(string? p)
{
    // p is maybe-null
    int length = p.Length;

    // p is not null.
    string s = p;

    if (s != null)
    {
        int l2 = s.Length;
    }
    // s is maybe null
    int l3 = s.Length;
}

En el ejemplo anterior, el compilador puede decidir que después de la instrucción int length = p.Length;, el estado null de p no es null. Si fuera null, esa instrucción habría producido una NullReferenceExceptionexcepción . Esto es similar al comportamiento si el código se había precedido de if (p == null) throw NullReferenceException(); , salvo que el código como escrito puede generar una advertencia, cuyo propósito es advertir de que se puede producir implícitamente una excepción.

Más adelante en el método , el código comprueba que s no es una referencia nula. El estado null de puede cambiar a tal vez null después de s que se cierre el bloque comprobado por NULL. El compilador puede deducir que s es posiblemente null porque el código se escribió para suponer que podría haber sido null. Por lo general, cuando el código contiene una comprobación nula, el compilador puede deducir que el valor podría haber sido NULL.ejemplo final

Ejemplo: El compilador puede tratar una propiedad (§15.7) como una variable con estado o como descriptores de acceso get y set independientes (§15.7.3). En otras palabras, un compilador puede elegir si la escritura en una propiedad cambia el estado NULL de lectura de la propiedad o si la lectura de una propiedad cambia el estado NULL de esa propiedad.

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;
        }
    }
}

En el ejemplo anterior, el campo de respaldo de DisappearingProperty se establece en NULL cuando se lee. Sin embargo, un compilador puede suponer que leer una propiedad no cambia el estado NULL de esa expresión. ejemplo final

Fin del texto normativo condicional