CSharp

Tutorial CSharp: Tipos de datos

Aprende los tipos de datos en C#, diferencias entre valor y referencia, y cómo realizar conversiones básicas para un código eficiente y seguro.

Aprende CSharp y certifícate

Tipos primitivos

En C#, los tipos primitivos son los bloques fundamentales para representar datos simples. Estos tipos están integrados directamente en el lenguaje y permiten almacenar valores básicos como números, caracteres y valores lógicos.

Tipos numéricos enteros

C# ofrece varios tipos para representar números enteros, cada uno con diferentes rangos y tamaños en memoria:

  • sbyte: Entero con signo de 8 bits (-128 a 127)
  • byte: Entero sin signo de 8 bits (0 a 255)
  • short: Entero con signo de 16 bits (-32,768 a 32,767)
  • ushort: Entero sin signo de 16 bits (0 a 65,535)
  • int: Entero con signo de 32 bits (-2,147,483,648 a 2,147,483,647)
  • uint: Entero sin signo de 32 bits (0 a 4,294,967,295)
  • long: Entero con signo de 64 bits (-9,223,372,036,854,775,808 a 9,223,372,036,854,775,807)
  • ulong: Entero sin signo de 64 bits (0 a 18,446,744,073,709,551,615)

El tipo int es el más comúnmente utilizado para representar números enteros:

int edad = 25;
int temperatura = -5;
uint cantidadPositiva = 100; // Solo valores positivos
long poblacionMundial = 7_900_000_000; // Los guiones bajos mejoran la legibilidad

Tipos numéricos de punto flotante

Para representar números con decimales, C# proporciona tres tipos:

  • float: Punto flotante de precisión simple (32 bits)
  • double: Punto flotante de precisión doble (64 bits)
  • decimal: Punto flotante de alta precisión (128 bits)
float altura = 1.75f;        // Requiere el sufijo 'f'
double pi = 3.14159265359;   // No requiere sufijo, es el tipo por defecto
decimal precio = 19.99m;     // Requiere el sufijo 'm'

El tipo double ofrece mayor precisión que float y es el tipo por defecto para literales de punto flotante. El tipo decimal proporciona la mayor precisión y es ideal para cálculos financieros donde la exactitud decimal es crucial.

Tipo booleano

El tipo bool representa valores lógicos verdadero o falso:

bool esMayorDeEdad = true;
bool tieneDescuento = false;

// Uso común en estructuras de control
if (esMayorDeEdad)
{
    Console.WriteLine("Puede acceder al contenido");
}

Tipo carácter

El tipo char representa un único carácter Unicode de 16 bits:

char letra = 'A';
char simbolo = '$';
char unicode = '\u00A9'; // Símbolo de copyright ©

Los caracteres se definen entre comillas simples, a diferencia de las cadenas que usan comillas dobles.

Literales y sufijos

Los literales son valores constantes en el código. Dependiendo del tipo, pueden requerir sufijos específicos:

int normal = 1000;
long grande = 3000000000L;    // Sufijo 'L' para long
float decimal1 = 45.6f;       // Sufijo 'f' para float
double decimal2 = 45.6;       // Sin sufijo (por defecto)
decimal preciso = 45.6m;      // Sufijo 'm' para decimal

Valores predeterminados

Cada tipo primitivo tiene un valor predeterminado que se asigna automáticamente cuando se declara una variable sin inicializar:

int enteroNoInicializado;     // Valor predeterminado: 0
bool booleanoNoInicializado;  // Valor predeterminado: false
char caracterNoInicializado;  // Valor predeterminado: '\0'
float flotanteNoInicializado; // Valor predeterminado: 0.0f

Sin embargo, las variables locales deben inicializarse antes de usarse, o el compilador generará un error.

Rangos y precisión

Es importante elegir el tipo adecuado según los requisitos de la aplicación:

// Ejemplo de desbordamiento (overflow)
byte pequeño = 255; // Valor máximo para byte
// pequeño = pequeño + 1; // Esto causaría un desbordamiento

// Ejemplo de pérdida de precisión
float numeroGrande = 10000000.0f;
float pequeñoIncremento = 0.0001f;
float resultado = numeroGrande + pequeñoIncremento; // El incremento se pierde

Constantes

Para valores que no deben cambiar, se utiliza la palabra clave const:

const int DIAS_SEMANA = 7;
const double GRAVEDAD = 9.81;
// DIAS_SEMANA = 8; // Error: no se puede modificar una constante

Inferencia de tipos

C# permite usar la palabra clave var para que el compilador infiera el tipo automáticamente:

var edad = 25;           // Infiere int
var nombre = "Ana";      // Infiere string
var pi = 3.14;           // Infiere double
var activo = true;       // Infiere bool

Es importante entender que var no hace que la variable sea de tipo dinámico; el tipo se determina en tiempo de compilación basado en el valor inicial.

Tipos numéricos sin signo

Los tipos sin signo (byte, ushort, uint, ulong) son útiles cuando se trabaja con valores que nunca serán negativos:

uint cantidadProductos = 500;
// cantidadProductos = -10; // Error: no puede ser negativo

// Útil para representar tamaños o cantidades
ulong tamañoArchivo = 3_500_000_000; // Más de 2 mil millones

Operaciones con tipos primitivos

Los tipos primitivos soportan operaciones aritméticas y lógicas básicas:

int a = 10;
int b = 3;

int suma = a + b;        // 13
int resta = a - b;       // 7
int multiplicacion = a * b; // 30
int division = a / b;    // 3 (división entera)
int modulo = a % b;      // 1 (resto)

bool comparacion = a > b;  // true
bool igualdad = a == b;    // false

Los tipos primitivos en C# son la base para construir tipos más complejos y estructuras de datos. Comprender sus características, rangos y comportamientos es fundamental para escribir código eficiente y libre de errores.

Valor vs referencia

En C#, la forma en que las variables almacenan y manipulan datos depende fundamentalmente de si son tipos de valor o tipos de referencia. Esta distinción es crucial para entender cómo se comportan las variables cuando se asignan o se pasan como parámetros.

Tipos de valor

Los tipos de valor almacenan directamente sus datos en la memoria asignada a la variable. Cuando declaras una variable de tipo valor, se reserva espacio en la pila (stack) para contener el valor completo. Los tipos primitivos que vimos anteriormente (int, float, bool, char, etc.) son todos tipos de valor.

int x = 10;  // Se almacena el valor 10 directamente en la variable x

Además de los tipos primitivos, las estructuras (struct) también son tipos de valor:

struct Punto
{
    public int X;
    public int Y;
}

Punto p1;
p1.X = 5;
p1.Y = 10;

Comportamiento de los tipos de valor en asignaciones

Cuando asignas un tipo de valor a otra variable, se crea una copia independiente del valor:

int original = 10;
int copia = original;  // Se crea una copia del valor

// Modificar la copia no afecta al original
copia = 20;
Console.WriteLine($"Original: {original}, Copia: {copia}");
// Muestra: "Original: 10, Copia: 20"

Este comportamiento se conoce como paso por valor. Cada variable mantiene su propia copia independiente de los datos.

Tipos de referencia

Los tipos de referencia funcionan de manera diferente. En lugar de almacenar directamente los datos, almacenan una referencia (esencialmente una dirección de memoria) que apunta a donde realmente se encuentran los datos en el montículo (heap).

Los tipos de referencia más comunes incluyen:

  • Cadenas (string)
  • Arrays
  • Clases
// Las cadenas son tipos de referencia
string mensaje = "Hola mundo";

// Los arrays son tipos de referencia
int[] numeros = { 1, 2, 3, 4, 5 };

Comportamiento de los tipos de referencia en asignaciones

Cuando asignas un tipo de referencia a otra variable, ambas variables apuntan a los mismos datos en memoria:

int[] original = { 1, 2, 3 };
int[] referencia = original;  // Ambas variables apuntan al mismo array

// Modificar a través de la referencia afecta al original
referencia[0] = 99;
Console.WriteLine($"Original[0]: {original[0]}, Referencia[0]: {referencia[0]}");
// Muestra: "Original[0]: 99, Referencia[0]: 99"

Este comportamiento se conoce como paso por referencia. Las variables comparten los mismos datos subyacentes.

Comparación visual

Para entender mejor la diferencia, podemos visualizarlo así:

Tipos de valor:

Variable x (en stack): [10]
Variable y (en stack): [10]  // Copia independiente

Tipos de referencia:

Variable a (en stack): [Referencia] ---> [Datos reales] (en heap)
Variable b (en stack): [Referencia] --/

Ejemplo práctico con arrays

Los arrays son un buen ejemplo para ilustrar el comportamiento de referencia:

// Creamos un array (tipo de referencia)
int[] a = { 1, 2, 3 };
int[] b = a;  // b y a apuntan al mismo array

// Modificamos a través de b
b[0] = 100;

// Verificamos que a también cambió
Console.WriteLine(a[0]);  // Muestra: 100

El caso especial de string

Aunque string es un tipo de referencia, tiene un comportamiento especial. Las cadenas en C# son inmutables, lo que significa que no pueden modificarse después de crearse:

string s1 = "hola";
string s2 = s1;

// Esto no modifica s1, sino que crea una nueva cadena
s2 = s2 + " mundo";

Console.WriteLine(s1);  // Muestra: "hola"
Console.WriteLine(s2);  // Muestra: "hola mundo"

Esto puede dar la impresión de que las cadenas se comportan como tipos de valor, pero en realidad es debido a su inmutabilidad.

Implicaciones en el rendimiento

Esta distinción tiene importantes implicaciones en el rendimiento:

  • Los tipos de valor son generalmente más eficientes para datos pequeños y simples, ya que evitan la sobrecarga de la asignación en el heap.
  • Los tipos de referencia son más eficientes para datos grandes o complejos, ya que evitan copiar grandes cantidades de datos.
// Eficiente: tipo de valor para datos simples
int contador = 0;

// Eficiente: tipo de referencia para datos grandes
int[] granArray = new int[10000];

Paso de parámetros a métodos

El comportamiento de valor vs referencia también afecta cómo se pasan los parámetros a los métodos:

void ModificarValor(int x)
{
    x = 100;  // Modifica solo la copia local
}

void ModificarReferencia(int[] arr)
{
    arr[0] = 100;  // Modifica el array original
}

int numero = 5;
ModificarValor(numero);
Console.WriteLine(numero);  // Sigue siendo 5

int[] miArray = { 1, 2, 3 };
ModificarReferencia(miArray);
Console.WriteLine(miArray[0]);  // Ahora es 100

Modificadores ref y out

C# proporciona los modificadores ref y out para cambiar este comportamiento predeterminado:

// Con 'ref', podemos modificar el valor original
void DuplicarNumero(ref int n)
{
    n = n * 2;
}

int valor = 10;
DuplicarNumero(ref valor);
Console.WriteLine(valor);  // Muestra: 20

El modificador out es similar, pero no requiere que la variable esté inicializada antes de pasarla:

void ObtenerCoordenadas(out int x, out int y)
{
    x = 10;
    y = 20;
}

int coordX, coordY;
ObtenerCoordenadas(out coordX, out coordY);
Console.WriteLine($"X: {coordX}, Y: {coordY}");  // Muestra: "X: 10, Y: 20"

Tipos nulables

Los tipos de valor normalmente no pueden ser nulos. Sin embargo, C# permite crear tipos de valor nulables usando el operador ?:

int? numeroNulable = null;
numeroNulable = 10;

if (numeroNulable.HasValue)
{
    Console.WriteLine(numeroNulable.Value);  // Muestra: 10
}

Entender la diferencia entre tipos de valor y referencia es fundamental para escribir código C# eficiente y predecible, especialmente cuando se trabaja con estructuras de datos complejas o se pasan parámetros entre métodos.

Conversiones básicas

En C# es común necesitar convertir datos de un tipo a otro durante el desarrollo de aplicaciones. Estas conversiones (también llamadas "casting" o "type casting") permiten transformar valores entre diferentes tipos de datos para adaptarlos a los requisitos específicos de nuestro código.

Conversiones implícitas

Las conversiones implícitas ocurren automáticamente cuando no hay riesgo de pérdida de datos. El compilador las realiza sin necesidad de código adicional cuando asignamos un valor de un tipo a una variable de otro tipo compatible.

int entero = 100;
long numeroLargo = entero;  // Conversión implícita de int a long

float numeroFloat = 10.5f;
double numeroDouble = numeroFloat;  // Conversión implícita de float a double

Las conversiones implícitas siguen una jerarquía lógica donde los tipos "más pequeños" pueden convertirse automáticamente a tipos "más grandes":

  • byteshortintlongfloatdoubledecimal
byte valorPequeno = 25;
int valorMedio = valorPequeno;  // Implícita
double valorGrande = valorMedio;  // Implícita

Conversiones explícitas

Cuando existe riesgo de pérdida de datos (como convertir de un tipo más grande a uno más pequeño), debemos realizar una conversión explícita utilizando el operador de casting:

double precio = 29.99;
int precioRedondeado = (int)precio;  // Conversión explícita, se pierde la parte decimal

Console.WriteLine(precioRedondeado);  // Muestra: 29

Es importante tener en cuenta que las conversiones explícitas pueden provocar pérdida de precisión o incluso resultados inesperados:

long numeroGrande = 9876543210;
int numeroTruncado = (int)numeroGrande;  // Desbordamiento - pérdida de datos

Console.WriteLine(numeroTruncado);  // Muestra un valor inesperado debido al desbordamiento

Métodos de conversión de la clase Convert

La clase Convert proporciona métodos para realizar conversiones más seguras entre tipos de datos:

string edadTexto = "25";
int edad = Convert.ToInt32(edadTexto);  // Convierte string a int

bool activo = true;
int valorNumerico = Convert.ToInt32(activo);  // true se convierte a 1, false a 0

Console.WriteLine(edad);         // Muestra: 25
Console.WriteLine(valorNumerico);  // Muestra: 1

La clase Convert ofrece métodos para la mayoría de los tipos primitivos:

// Conversiones comunes con la clase Convert
double valorDecimal = 123.45;
int enteroConvertido = Convert.ToInt32(valorDecimal);  // Redondea a 123

string cadenaBooleana = "True";
bool valorBooleano = Convert.ToBoolean(cadenaBooleana);  // Convierte a true

DateTime fecha = Convert.ToDateTime("2023-05-15");  // Convierte string a fecha

Métodos Parse y TryParse

Los tipos numéricos y algunos otros tipos proporcionan métodos Parse para convertir cadenas en valores tipados:

string numeroTexto = "42";
int numero = int.Parse(numeroTexto);

string precioTexto = "19.99";
double precio = double.Parse(precioTexto);  // Nota: esto depende de la configuración regional

Sin embargo, Parse lanza una excepción si la conversión falla. Para conversiones más seguras, es recomendable usar TryParse:

string entrada = "abc";
int resultado;

if (int.TryParse(entrada, out resultado))
{
    Console.WriteLine($"Conversión exitosa: {resultado}");
}
else
{
    Console.WriteLine("La conversión falló");  // Se ejecutará esta línea
}

El método TryParse intenta realizar la conversión y devuelve un booleano indicando si tuvo éxito, sin lanzar excepciones:

// Ejemplo más completo de TryParse
string[] entradas = { "100", "12.5", "no es un número", "true" };

foreach (string entrada in entradas)
{
    int valorEntero;
    if (int.TryParse(entrada, out valorEntero))
    {
        Console.WriteLine($"'{entrada}' -> {valorEntero}");
    }
    else
    {
        Console.WriteLine($"'{entrada}' no se puede convertir a entero");
    }
}

Conversión entre cadenas y otros tipos

La conversión entre cadenas y otros tipos es muy común en aplicaciones que interactúan con usuarios:

// De tipos primitivos a string
int cantidad = 42;
string cantidadTexto = cantidad.ToString();

double temperatura = 23.5;
string temperaturaTexto = temperatura.ToString("F1");  // "23.5" (1 decimal)

// Formateo específico
decimal precio = 1299.99m;
string precioFormateado = precio.ToString("C");  // "$1,299.99" (formato de moneda)

Conversiones con operadores "as" y "is"

Para tipos de referencia, C# proporciona los operadores as e is que son útiles para conversiones seguras:

object valor = "Hola mundo";

// Comprobación con "is"
if (valor is string)
{
    Console.WriteLine("El valor es una cadena");
}

// Conversión con "as"
string texto = valor as string;  // Si no es posible, devuelve null en lugar de lanzar excepción
if (texto != null)
{
    Console.WriteLine(texto.ToUpper());  // "HOLA MUNDO"
}

El operador as solo funciona con tipos de referencia y tipos nulables, no con tipos de valor primitivos.

Conversiones con el patrón de coincidencia (pattern matching)

En versiones modernas de C#, el patrón de coincidencia simplifica las conversiones y comprobaciones de tipo:

object item = 123;

if (item is int numero)
{
    // La variable 'numero' ya está convertida y disponible
    Console.WriteLine($"El cuadrado es: {numero * numero}");
}

// También funciona en expresiones switch
switch (item)
{
    case int i:
        Console.WriteLine($"Es un entero: {i}");
        break;
    case string s:
        Console.WriteLine($"Es una cadena: {s}");
        break;
    default:
        Console.WriteLine("Tipo no reconocido");
        break;
}

Conversiones entre tipos numéricos con verificación

Para realizar conversiones seguras entre tipos numéricos con verificación de desbordamiento, podemos usar las palabras clave checked y unchecked:

int valorMaximo = int.MaxValue;  // 2,147,483,647

try
{
    checked
    {
        int resultado = valorMaximo + 1;  // Lanzará OverflowException
    }
}
catch (OverflowException ex)
{
    Console.WriteLine("Se produjo un desbordamiento: " + ex.Message);
}

// Sin verificación (comportamiento predeterminado)
unchecked
{
    int resultado = valorMaximo + 1;  // No lanza excepción, pero produce un resultado incorrecto
    Console.WriteLine(resultado);  // Muestra: -2,147,483,648
}

Conversiones entre tipos personalizados

También es posible definir conversiones personalizadas entre tipos definidos por el usuario mediante operadores de conversión:

public struct Celsius
{
    public double Temperatura { get; }

    public Celsius(double temperatura)
    {
        Temperatura = temperatura;
    }

    // Conversión implícita de double a Celsius
    public static implicit operator Celsius(double temperatura)
    {
        return new Celsius(temperatura);
    }

    // Conversión explícita de Celsius a Fahrenheit
    public static explicit operator Fahrenheit(Celsius celsius)
    {
        return new Fahrenheit(celsius.Temperatura * 9 / 5 + 32);
    }
}

public struct Fahrenheit
{
    public double Temperatura { get; }

    public Fahrenheit(double temperatura)
    {
        Temperatura = temperatura;
    }
}

Las conversiones entre tipos son una parte fundamental de la programación en C#, permitiéndonos trabajar con diferentes representaciones de datos según las necesidades de nuestra aplicación. Elegir el método de conversión adecuado es esencial para mantener la integridad de los datos y evitar errores en tiempo de ejecución.

Aprende CSharp online

Otros ejercicios de programación de CSharp

Evalúa tus conocimientos de esta lección Tipos de datos con nuestros retos de programación de tipo Test, Puzzle, Código y Proyecto con VSCode, guiados por IA.

Todas las lecciones de CSharp

Accede a todas las lecciones de CSharp y aprende con ejemplos prácticos de código y ejercicios de programación con IDE web sin instalar nada.

Introducción A C#

Introducción Y Entorno

Creación De Proyecto C#

Introducción Y Entorno

Variables Y Constantes

Sintaxis

Tipos De Datos

Sintaxis

Operadores

Sintaxis

Control De Flujo

Sintaxis

Funciones

Sintaxis

Estructuras De Control Iterativo

Sintaxis

Interpolación De Strings

Sintaxis

Estructuras De Control Condicional

Sintaxis

Manejo De Valores Nulos

Sintaxis

Clases Y Encapsulación

Programación Orientada A Objetos

Objetos

Programación Orientada A Objetos

Constructores Y Destructores

Programación Orientada A Objetos

Herencia

Programación Orientada A Objetos

Polimorfismo

Programación Orientada A Objetos

Genéricos

Programación Orientada A Objetos

Métodos Virtuales Y Sobrecarga

Programación Orientada A Objetos

Clases Abstractas

Programación Orientada A Objetos

Interfaces

Programación Orientada A Objetos

Propiedades Y Encapsulación

Programación Orientada A Objetos

Métodos De Extensión

Programación Orientada A Objetos

Clases Y Objetos

Programación Orientada A Objetos

Clases Parciales

Programación Orientada A Objetos

Miembros Estáticos

Programación Orientada A Objetos

Tuplas Y Tipos Anónimos

Programación Orientada A Objetos

Arrays Y Listas

Colecciones Y Linq

Diccionarios

Colecciones Y Linq

Conjuntos, Colas Y Pilas

Colecciones Y Linq

Uso De Consultas Linq

Colecciones Y Linq

Linq Avanzado

Colecciones Y Linq

Colas Y Pilas

Colecciones Y Linq

Conjuntos

Colecciones Y Linq

Linq Básico

Colecciones Y Linq

Delegados Funcionales

Programación Funcional

Records

Programación Funcional

Expresiones Lambda

Programación Funcional

Linq Funcional

Programación Funcional

Fundamentos De La Programación Funcional

Programación Funcional

Pattern Matching

Programación Funcional

Testing Unitario Con Xunit

Testing

Excepciones

Excepciones

Delegados

Programación Asíncrona

Eventos

Programación Asíncrona

Lambdas

Programación Asíncrona

Uso De Async Y Await

Programación Asíncrona

Tareas

Programación Asíncrona

Accede GRATIS a CSharp y certifícate

En esta lección

Objetivos de aprendizaje de esta lección

  • Identificar y utilizar los tipos primitivos en C# y sus características.
  • Comprender la diferencia entre tipos de valor y tipos de referencia y su comportamiento en memoria.
  • Aplicar conversiones implícitas y explícitas entre tipos de datos.
  • Utilizar métodos seguros de conversión como Convert, Parse y TryParse.
  • Entender el uso de modificadores ref y out, así como tipos nulables y conversiones personalizadas.