Compartir vía


El sistema de tipos de C#

C# es un lenguaje fuertemente tipado. Cada variable y constante tiene un tipo, al igual que todas las expresiones que se evalúan como un valor. Cada declaración de método especifica un nombre, el tipo y el modo (valor, referencia o salida) para cada parámetro de entrada y para el valor de retorno. La biblioteca de clases de .NET define tipos numéricos integrados y tipos complejos que representan una amplia variedad de construcciones. Estos incluyen el sistema de archivos, las conexiones de red, las colecciones y las matrices de objetos y fechas. Un programa típico de C# usa tipos de la biblioteca de clases y tipos definidos por el usuario que modela los conceptos específicos del dominio de problemas del programa.

La información almacenada en un tipo puede incluir los siguientes elementos:

  • Espacio de almacenamiento que requiere una variable del tipo.
  • Valores máximos y mínimos que puede representar.
  • Los miembros (métodos, campos, eventos, etc.) que contiene.
  • El tipo base del que hereda.
  • Las interfaces que implementa.
  • Las operaciones permitidas.

El compilador usa información de tipo para asegurarse de que todas las operaciones que se realizan en el código son seguras para tipos. Por ejemplo, si declara una variable de tipo int, el compilador permite usar la variable en operaciones de suma y resta. Si intenta realizar esas mismas operaciones en una variable de tipo bool, el compilador genera un error, como se muestra en el ejemplo siguiente:

int a = 5;
int b = a + 2; //OK

bool test = true;

// Error. Operator '+' cannot be applied to operands of type 'int' and 'bool'.
int c = a + test;

Nota:

Los desarrolladores de C y C++, observen que en C#, bool no se puede convertir en int.

El compilador inserta la información de tipo en el archivo ejecutable como metadatos. Common Language Runtime (CLR) usa los metadatos en tiempo de ejecución para garantizar aún más la seguridad de tipos cuando asigna y reclama memoria.

Especificación de tipos en declaraciones de variables

Al declarar una variable o una constante en un programa, debe especificar su tipo o usar la var palabra clave para permitir que el compilador infiera el tipo. En el ejemplo siguiente se muestran algunas declaraciones de variables que usan tipos numéricos integrados y tipos complejos definidos por el usuario:

// Declaration only:
float temperature;
string name;
MyClass myClass;

// Declaration with initializers (four examples):
char firstLetter = 'C';
var limit = 3;
int[] source = [0, 1, 2, 3, 4, 5];
var query = from item in source
            where item <= limit
            select item;

Los tipos de parámetros de método y valores devueltos se especifican en la declaración de método. La firma siguiente muestra un método que requiere un int como argumento de entrada y devuelve una cadena:

public string GetName(int ID)
{
    if (ID < names.Length)
        return names[ID];
    else
        return String.Empty;
}
private string[] names = ["Spencer", "Sally", "Doug"];

Después de declarar una variable, no se puede volver a declarar con un nuevo tipo y no se puede asignar un valor no compatible con su tipo declarado. Por ejemplo, no se puede declarar int y, a continuación, asignarle un valor booleano de true. Sin embargo, los valores se pueden convertir a otros tipos, por ejemplo, cuando se asignan a nuevas variables o se pasan como argumentos de método. El compilador realiza automáticamente una conversión de tipos que no causa la pérdida de datos. Una conversión que pueda dar lugar a la pérdida de datos requiere un valor cast en el código fuente.

Para obtener más información, vea Conversiones de tipos.

Tipos integrados

C# proporciona un conjunto estándar de tipos integrados. Representan enteros, valores de punto flotante, expresiones booleanas, caracteres de texto, valores decimales y otros tipos de datos. También hay tipos integrados string y object. Estos tipos están disponibles para su uso en cualquier programa de C#. Para obtener la lista completa de los tipos integrados, consulte Tipos integrados.

Tipos personalizados

Use las construcciones struct, class, interface, enumy record para crear sus propios tipos personalizados. La propia biblioteca de clases de .NET es una colección de tipos personalizados que puede usar en sus propias aplicaciones. De forma predeterminada, los tipos más usados de la biblioteca de clases están disponibles en cualquier programa de C#. Otros solo están disponibles cuando se agrega explícitamente una referencia de proyecto al ensamblado que las define. Después de que el compilador tenga una referencia al ensamblado, puede declarar variables (y constantes) de los tipos declarados en ese ensamblado en el código fuente. Para obtener más información, vea Biblioteca de clases de .NET.

Una de las primeras decisiones que se toman al definir un tipo es decidir qué construcción usar para el tipo. La lista siguiente ayuda a tomar esa decisión inicial. Hay superposición en las opciones. En la mayoría de los escenarios, más de una opción es una opción razonable.

  • Si el tamaño del almacenamiento de datos es pequeño, no más de 64 bytes, elija o structrecord struct.
  • Si el tipo es inmutable o desea una mutación no destructiva, elija un struct o un record struct.
  • Si su tipo debe tener semántica de valor para la igualdad, elija un record class o un record struct.
  • Si el tipo se usa principalmente para almacenar datos, no para el comportamiento, elija o record classrecord struct.
  • Si el tipo forma parte de una jerarquía de herencia, elija un record class o un class.
  • Si el tipo usa polimorfismo, elija un class.
  • Si el propósito principal es el comportamiento, elija un class.

El sistema de tipos común

Es importante comprender dos puntos fundamentales sobre el sistema de tipos en .NET:

  • Admite el principio de herencia. Los tipos pueden derivar de otros tipos, denominados tipos base. El tipo derivado hereda (con algunas restricciones) los métodos, las propiedades y otros miembros del tipo base. El tipo base puede derivar a su vez de algún otro tipo, en cuyo caso el tipo derivado hereda los miembros de ambos tipos base en su jerarquía de herencia. Todos los tipos, incluidos los tipos numéricos integrados, como System.Int32 (palabra clave de C#: int), derivan en última instancia de un único tipo base, que es System.Object (palabra clave de C#: object). Esta jerarquía de tipos unificada se denomina Common Type System (CTS). Para obtener más información sobre la herencia en C#, vea Herencia.
  • Cada tipo de CTS se define como un tipo de valor o un tipo de referencia. Estos tipos incluyen todos los tipos personalizados en la biblioteca de clases de .NET y también sus propios tipos definidos por el usuario. Los tipos que defina mediante la struct palabra clave son tipos de valor; todos los tipos numéricos integrados son structs. Los tipos que defina mediante la class palabra clave o record son tipos de referencia. Los tipos de referencia y los tipos de valor tienen reglas en tiempo de compilación diferentes y un comportamiento en tiempo de ejecución diferente.

En la ilustración siguiente se muestra la relación entre los tipos de valor y los tipos de referencia en el CTS.

Captura de pantalla que muestra los tipos de valor y los tipos de referencia de CTS.

Nota:

Puede ver que los tipos más usados se organizan en el System espacio de nombres. Sin embargo, el espacio de nombres en el que se encuentra un tipo no tiene relación con si es un tipo de valor o un tipo de referencia.

Las clases y estructuras son dos de las construcciones básicas del sistema de tipos común en .NET. Cada es básicamente una estructura de datos que encapsula un conjunto de datos y comportamientos que pertenecen a ellos como una unidad lógica. Los datos y los comportamientos son los miembros de la clase, estructura o registro. Los miembros incluyen sus métodos, propiedades, eventos, etc., como se muestra más adelante en este artículo.

Una declaración de clase, estructura o registro es como un plano técnico que se usa para crear instancias o objetos en tiempo de ejecución. Si define una clase, estructura o registro denominado Person, Person es el nombre del tipo. Si declara e inicializa una variable p de tipo Person, se dice que p es un objeto o instancia de Person. Se pueden crear varias instancias del mismo tipo de Person y cada instancia puede tener valores diferentes en sus propiedades y campos.

Una clase es un tipo de referencia. Cuando se crea un objeto del tipo, la variable a la que se asigna el objeto contiene solo una referencia a esa memoria. Cuando la referencia de objeto se asigna a una nueva variable, la nueva variable hace referencia al objeto original. Los cambios realizados a través de una variable se reflejan en la otra variable porque ambos hacen referencia a los mismos datos.

Una estructura es un tipo de valor. Cuando se crea una estructura, la variable a la que se asigna la estructura contiene los datos reales del struct. Cuando la estructura se asigna a una nueva variable, se copia. Por lo tanto, la nueva variable y la variable original contienen dos copias independientes de los mismos datos. Los cambios realizados en una copia no afectan a la otra copia.

Los tipos de registro pueden ser tipos de referencia (record class) o tipos de valor (record struct). Los tipos de registro contienen métodos que admiten la igualdad de valores.

En general, las clases se usan para modelar un comportamiento más complejo. Las clases suelen almacenar datos que están diseñados para modificarse después de crear un objeto de clase. Las estructuras son más adecuadas para estructuras de datos pequeñas. Las estructuras suelen almacenar datos que no están diseñados para modificarse después de crear la estructura. Los tipos de registro son estructuras de datos con miembros sintetizados adicionales del compilador. Los registros suelen almacenar datos que no están diseñados para modificarse después de crear el objeto.

Tipos de valor

Los tipos de valor derivan de System.ValueType, que se deriva de System.Object. Los tipos que derivan de System.ValueType tienen un comportamiento especial en CLR. Las variables de tipo de valor contienen directamente sus valores. La memoria de un struct se asigna en línea en cualquier contexto que se declare la variable. No hay ninguna sobrecarga de asignación de montón o recolección de elementos no utilizados independientes para las variables de tipo valor. Puede declarar tipos record struct que son tipos de valor e incluir los miembros sintetizados para los registros.

Hay dos categorías de tipos de valor: struct y enum.

Los tipos numéricos integrados son estructuras y tienen campos y métodos a los que puede acceder:

// constant field on type byte.
byte b = byte.MaxValue;

Pero declara y asigna valores a ellos como si fueran tipos no agregados simples:

byte num = 0xA;
int i = 5;
char c = 'Z';

Los tipos de valor se sellados. No se puede derivar un tipo de ningún tipo de valor, por ejemplo, System.Int32. No se puede definir una estructura para heredar de cualquier clase o estructura definida por el usuario porque una estructura solo puede heredar de System.ValueType. Sin embargo, una estructura puede implementar una o varias interfaces. No se puede convertir un tipo de struct en cualquier tipo de interfaz que implemente. Esta conversión provoca una que operación boxing encapsule el struct dentro de un objeto de tipo de referencia en el montón administrado. Las operaciones de conversión boxing se producen cuando se pasa un tipo de valor a un método que toma System.Object o cualquier tipo de interfaz como parámetro de entrada. Para obtener más información, vea Boxing y Unboxing.

Use la palabra clave struct para crear sus propios tipos de valor personalizados. Normalmente, una estructura se usa como contenedor para un pequeño conjunto de variables relacionadas, como se muestra en el ejemplo siguiente:

public struct Coords
{
    public int x, y;

    public Coords(int p1, int p2)
    {
        x = p1;
        y = p2;
    }
}

Para obtener más información sobre las estructuras, vea Tipos de estructura. Para obtener más información sobre los tipos de valor, vea Tipos de valor.

La otra categoría de tipos de valor es enum. Una enumeración define un conjunto de constantes integrales con nombre. Por ejemplo, la enumeración System.IO.FileMode de la biblioteca de clases de .NET contiene un conjunto de enteros de constantes con nombre que especifican cómo se debe abrir un archivo. Se define como se muestra en el ejemplo siguiente:

public enum FileMode
{
    CreateNew = 1,
    Create = 2,
    Open = 3,
    OpenOrCreate = 4,
    Truncate = 5,
    Append = 6,
}

La constante System.IO.FileMode.Create tiene un valor de 2. Sin embargo, el nombre es mucho más significativo para los seres humanos que leen el código fuente y, por ese motivo, es mejor usar enumeraciones en lugar de números literales constantes. Para obtener más información, consulte System.IO.FileMode.

Todas las enumeraciones se heredan de System.Enum, el cual se hereda de System.ValueType. Todas las reglas que se aplican a las estructuras también se aplican a las enumeraciones. Para obtener más información sobre las enumeraciones, vea Tipos de enumeración.

Tipos de referencia

Un tipo que se define como class, record, delegate, array o interface es un reference type.

Cuando declaras una variable de reference type, contiene el valor null hasta que se le asigna una instancia de ese tipo o se crea una mediante el operador new. La creación y asignación de una clase se muestran en el ejemplo siguiente:

MyClass myClass = new MyClass();
MyClass myClass2 = myClass;

Una instancia de interface no puede ser creada directamente mediante el operador new. En su lugar, cree y asigne una instancia de una clase que implemente la interfaz . Considere el ejemplo siguiente:

MyClass myClass = new MyClass();

// Declare and assign using an existing value.
IMyInterface myInterface = myClass;

// Or create and assign a value in a single statement.
IMyInterface myInterface2 = new MyClass();

Cuando se crea un objeto, se asigna memoria en el montón administrado. La variable contiene solo una referencia a la ubicación del objeto . Los tipos del montón administrado producen sobrecarga cuando se asignan y cuando se reclaman. La recolección de basura es la funcionalidad de administración automática de memoria del CLR (Common Language Runtime), que realiza la recuperación. Sin embargo, la recolección de basura también está muy optimizada y en la mayoría de los escenarios no crea un problema de rendimiento. Para obtener más información sobre la recolección de basura, consulte Administración automática de memoria.

Todas las matrices son tipos de referencia, incluso si sus elementos son tipos de valor. Las matrices derivan implícitamente de la System.Array clase . Se declaran y se usan con la sintaxis simplificada proporcionada por C#, como se muestra en el ejemplo siguiente:

// Declare and initialize an array of integers.
int[] nums = [1, 2, 3, 4, 5];

// Access an instance property of System.Array.
int len = nums.Length;

Los tipos de referencia admiten la herencia completamente. Al crear una clase, puede heredar de cualquier otra interfaz o clase que no esté definida como sellada. Otras clases pueden heredar de la clase e invalidar sus métodos virtuales. Para obtener más información sobre cómo crear sus propias clases, vea Clases, estructuras y registros. Para obtener más información sobre la herencia y los métodos virtuales, vea Herencia.

Tipos de valores literales

En C#, los valores literales reciben un tipo del compilador. Puede especificar cómo se debe escribir un literal numérico anexando una letra al final del número. Por ejemplo, para especificar que el valor 4.56 debe tratarse como , floatanexe "f" o "F" después del número: 4.56f. Si no se anexa ninguna letra, el compilador infiere un tipo para el literal. Para obtener más información sobre qué tipos se pueden especificar con sufijos de letra, vea Tipos numéricos enteros y tipos numéricos de punto flotante.

Dado que los literales se escriben y todos los tipos derivan en última instancia de System.Object, puede escribir y compilar código como el código siguiente:

string s = "The answer is " + 5.ToString();
// Outputs: "The answer is 5"
Console.WriteLine(s);

Type type = 12345.GetType();
// Outputs: "System.Int32"
Console.WriteLine(type);

Tipos genéricos

Un tipo se puede declarar con uno o varios parámetros de tipo que sirven como marcador de posición para el tipo real (el tipo concreto). El código de cliente proporciona el tipo concreto cuando crea una instancia del tipo . Estos tipos se denominan tipos genéricos. Por ejemplo, el tipo System.Collections.Generic.List<T> de .NET tiene un parámetro de tipo que por convención recibe el nombre T. Al crear una instancia del tipo, especifique el tipo de los objetos que contiene la lista, por ejemplo, string:

List<string> stringList = new List<string>();
stringList.Add("String example");
// compile time error adding a type other than a string:
stringList.Add(4);

El uso del parámetro type permite reutilizar la misma clase para contener cualquier tipo de elemento, sin tener que convertir cada elemento en objeto. Las clases de colección genéricas se denominan colecciones fuertemente tipadas porque el compilador conoce el tipo específico de los elementos de la colección y puede generar un error en tiempo de compilación si, por ejemplo, intenta agregar un entero al stringList objeto en el ejemplo anterior. Para obtener más información, vea Genéricos.

Tipos implícitos, tipos anónimos y tipos de valor anulables

Puede escribir implícitamente una variable local (pero no miembros de clase) mediante la var palabra clave . La variable sigue recibiendo un tipo en tiempo de compilación, pero el compilador proporciona el tipo. Para obtener más información, vea Variables locales con tipo implícito.

En algunos casos, resulta conveniente crear un tipo con nombre para conjuntos sencillos de valores relacionados que no desea almacenar ni pasar fuera de los límites del método. Puede crear tipos anónimos para este propósito. Para obtener más información, vea Tipos anónimos.

Los tipos de valor normal no pueden tener un valor de null. pero se pueden crear tipos de valor que aceptan valores NULL mediante la adición de ? después del tipo. Por ejemplo, int? es un int tipo que también puede tener el valor null. Los tipos de valor anulables son instancias del tipo de estructura genérico System.Nullable<T>. Los tipos de valor que aceptan valores NULL son especialmente útiles cuando se pasan datos a y desde bases de datos en las que los valores numéricos pueden ser null. Para obtener más información, consulte Tipos de valor anulable.

Tipo en tiempo de compilación y tipo en tiempo de ejecución

Una variable puede tener distintos tipos en tiempo de compilación y en tiempo de ejecución. El tipo de tiempo de compilación es el tipo declarado o inferido de la variable en el código fuente. El tipo en tiempo de ejecución es el tipo de la instancia a la que hace referencia esa variable. A menudo, esos dos tipos son los mismos que en el ejemplo siguiente:

string message = "This is a string of characters";

En otros casos, el tipo en tiempo de compilación es diferente, tal y como se muestra en los dos ejemplos siguientes:

object anotherMessage = "This is another string of characters";
IEnumerable<char> someCharacters = "abcdefghijklmnopqrstuvwxyz";

En los dos ejemplos anteriores, el tipo en tiempo de ejecución es string. El tipo en tiempo de compilación es object en la primera línea y IEnumerable<char> en la segunda.

Si los dos tipos son diferentes para una variable, es importante comprender cuándo se aplica el tipo en tiempo de compilación y el tipo en tiempo de ejecución. El tipo en tiempo de compilación determina todas las acciones realizadas por el compilador. Estas acciones del compilador incluyen la resolución de llamadas a métodos, la resolución de sobrecarga y las conversiones implícitas y explícitas disponibles. El tipo en tiempo de ejecución determina todas las acciones que se resuelven en tiempo de ejecución. Estas acciones en tiempo de ejecución incluyen la distribución de llamadas a métodos virtuales, la evaluación de las expresiones is y switch y otras pruebas de tipos de API. Para comprender mejor cómo interactúa el código con los tipos, reconozca qué acción se aplica a qué tipo.

Para obtener más información, consulte los artículos siguientes:

Especificación del lenguaje C#

Para obtener más información, consulte la Especificación del lenguaje C#. La especificación del lenguaje es el origen definitivo de la sintaxis y el uso de C#.