Ir al contenido principal

Estructuras definidas por el usuario: struct

Hasta ahora hemos trabajado con tipos de datos básicos como int, string y double, así como con arrays y colecciones. Sin embargo, a menudo necesitamos crear nuestros propios tipos de datos que representen conceptos más complejos de nuestro dominio. En C#, las estructuras (struct) nos permiten definir tipos de datos personalizados que agrupan diferentes variables relacionadas bajo un mismo nombre.

Las estructuras son especialmente útiles cuando necesitamos representar entidades simples que contienen varios datos relacionados, como un punto en el espacio, una fecha personalizada, o las dimensiones de un rectángulo. A diferencia de las clases (que veremos más adelante), las estructuras son tipos por valor, lo que las hace más eficientes en memoria para datos pequeños y simples.

En este artículo aprenderemos a crear nuestras propias estructuras, cómo trabajar con ellas y cuándo es apropiado utilizarlas en lugar de otros tipos de datos.

Qué son las estructuras en C#

Una estructura (struct) es un tipo de datos personalizado que permite agrupar variables de diferentes tipos bajo un mismo nombre. Las estructuras son tipos por valor, lo que significa que cuando asignamos una estructura a una variable o la pasamos como parámetro, se crea una copia completa de todos sus datos.

Características principales de las estructuras

Característica Descripción
Tipo por valor Se almacenan en la pila, no en el heap
Copia por valor Las asignaciones crean copias completas
No herencia No pueden heredar de otras estructuras o clases
Constructores Pueden tener constructores personalizados
Métodos y propiedades Pueden contener métodos y propiedades
Inicialización Todos los campos deben inicializarse

Declaración básica de una estructura

La sintaxis para declarar una estructura es la siguiente:

struct NombreDeLaEstructura
{
    // Campos (variables miembro)
    public tipo campo1;
    public tipo campo2;
    
    // Métodos opcionales
    public void MetodoEjemplo()
    {
        // Código del método
    }
}

Veamos un ejemplo práctico creando una estructura para representar un punto en el plano cartesiano:

using System;

struct Punto
{
    public double x;
    public double y;
    
    // Constructor personalizado
    public Punto(double x, double y)
    {
        this.x = x;
        this.y = y;
    }
    
    // Método para mostrar las coordenadas
    public void MostrarCoordenadas()
    {
        Console.WriteLine($"Punto: ({x}, {y})");
    }
    
    // Método para calcular la distancia al origen
    public double DistanciaAlOrigen()
    {
        return Math.Sqrt(x * x + y * y);
    }
}

class Program
{
    static void Main()
    {
        // Crear puntos usando diferentes métodos
        Punto punto1 = new Punto(3.5, 2.1);
        Punto punto2 = new Punto();
        punto2.x = 1.0;
        punto2.y = -2.5;
        
        // Usar los métodos de la estructura
        punto1.MostrarCoordenadas();
        punto2.MostrarCoordenadas();
        
        Console.WriteLine($"Distancia de punto1 al origen: {punto1.DistanciaAlOrigen():F2}");
        Console.WriteLine($"Distancia de punto2 al origen: {punto2.DistanciaAlOrigen():F2}");
    }
}

Constructores en estructuras

Las estructuras pueden tener constructores personalizados, pero con algunas reglas específicas:

Reglas para constructores en estructuras

Regla Descripción
Constructor sin parámetros No se puede definir explícitamente
Inicialización completa El constructor debe inicializar todos los campos
Constructor implícito Siempre existe uno que inicializa todos los campos a sus valores por defecto
struct Rectangulo
{
    public double ancho;
    public double alto;
    public string color;
    
    // Constructor con todos los parámetros
    public Rectangulo(double ancho, double alto, string color)
    {
        this.ancho = ancho;
        this.alto = alto;
        this.color = color;
    }
    
    // Constructor con valores por defecto para el color
    public Rectangulo(double ancho, double alto) : this(ancho, alto, "Blanco")
    {
        // Llamamos al constructor principal usando 'this'
    }
    
    public double CalcularArea()
    {
        return ancho * alto;
    }
    
    public double CalcularPerimetro()
    {
        return 2 * (ancho + alto);
    }
    
    public void MostrarInformacion()
    {
        Console.WriteLine($"Rectángulo {color}: {ancho}x{alto}");
        Console.WriteLine($"Área: {CalcularArea()} - Perímetro: {CalcularPerimetro()}");
    }
}

Comportamiento de tipos por valor

Una característica fundamental de las estructuras es que son tipos por valor. Esto significa que cuando asignamos una estructura a otra variable, se crea una copia completa:

using System;

struct Contador
{
    public int valor;
    
    public Contador(int valorInicial)
    {
        valor = valorInicial;
    }
    
    public void Incrementar()
    {
        valor++;
    }
    
    public void Mostrar()
    {
        Console.WriteLine($"Contador: {valor}");
    }
}

class Program
{
    static void Main()
    {
        // Demostración del comportamiento por valor
        Contador contador1 = new Contador(10);
        Contador contador2 = contador1; // Se crea una copia
        
        Console.WriteLine("Estado inicial:");
        contador1.Mostrar();
        contador2.Mostrar();
        
        // Modificamos contador1
        contador1.Incrementar();
        contador1.Incrementar();
        
        Console.WriteLine("\nDespués de incrementar contador1:");
        contador1.Mostrar(); // Mostrará 12
        contador2.Mostrar(); // Seguirá mostrando 10 (es una copia independiente)
        
        // Demostración con parámetros de método
        ModificarContador(contador1);
        Console.WriteLine("\nDespués de llamar ModificarContador:");
        contador1.Mostrar(); // Seguirá mostrando 12 (se pasó una copia)
    }
    
    static void ModificarContador(Contador contador)
    {
        contador.Incrementar();
        Console.WriteLine($"Dentro del método: {contador.valor}");
    }
}

Estructuras con propiedades

Las estructuras también pueden tener propiedades, lo que nos permite controlar el acceso a los campos:

struct Temperatura
{
    private double celsius;
    
    public Temperatura(double celsius)
    {
        this.celsius = celsius;
    }
    
    // Propiedad para Celsius
    public double Celsius
    {
        get { return celsius; }
        set { celsius = value; }
    }
    
    // Propiedades calculadas para otras escalas
    public double Fahrenheit
    {
        get { return (celsius * 9.0 / 5.0) + 32; }
        set { celsius = (value - 32) * 5.0 / 9.0; }
    }
    
    public double Kelvin
    {
        get { return celsius + 273.15; }
        set { celsius = value - 273.15; }
    }
    
    public void MostrarTemperaturas()
    {
        Console.WriteLine($"Temperatura:");
        Console.WriteLine($"  Celsius: {Celsius:F1}°C");
        Console.WriteLine($"  Fahrenheit: {Fahrenheit:F1}°F");
        Console.WriteLine($"  Kelvin: {Kelvin:F1}K");
    }
    
    // Método para verificar si está congelando (en Celsius)
    public bool EstaCongelando()
    {
        return celsius <= 0;
    }
}

Ejemplo práctico: sistema de coordenadas 3D

Veamos un ejemplo más complejo que muestra las capacidades de las estructuras:

using System;

struct Vector3D
{
    public double x, y, z;
    
    public Vector3D(double x, double y, double z)
    {
        this.x = x;
        this.y = y;
        this.z = z;
    }
    
    // Propiedad para calcular la magnitud del vector
    public double Magnitud
    {
        get { return Math.Sqrt(x * x + y * y + z * z); }
    }
    
    // Método para normalizar el vector
    public Vector3D Normalizar()
    {
        double mag = Magnitud;
        if (mag == 0) return new Vector3D(0, 0, 0);
        
        return new Vector3D(x / mag, y / mag, z / mag);
    }
    
    // Método para calcular el producto punto con otro vector
    public double ProductoPunto(Vector3D otro)
    {
        return x * otro.x + y * otro.y + z * otro.z;
    }
    
    // Método para sumar vectores
    public Vector3D Sumar(Vector3D otro)
    {
        return new Vector3D(x + otro.x, y + otro.y, z + otro.z);
    }
    
    // Método para mostrar el vector
    public void Mostrar()
    {
        Console.WriteLine($"Vector3D({x:F2}, {y:F2}, {z:F2}) - Magnitud: {Magnitud:F2}");
    }
}

class Program
{
    static void Main()
    {
        Vector3D vector1 = new Vector3D(3, 4, 5);
        Vector3D vector2 = new Vector3D(1, -2, 2);
        
        Console.WriteLine("Vectores originales:");
        vector1.Mostrar();
        vector2.Mostrar();
        
        Console.WriteLine($"\nProducto punto: {vector1.ProductoPunto(vector2):F2}");
        
        Vector3D suma = vector1.Sumar(vector2);
        Console.WriteLine("\nSuma de vectores:");
        suma.Mostrar();
        
        Vector3D normalizado = vector1.Normalizar();
        Console.WriteLine("\nVector1 normalizado:");
        normalizado.Mostrar();
    }
}

Cuándo usar estructuras

Las estructuras son apropiadas cuando:

Casos recomendados para usar struct

Caso Descripción Ejemplo
Datos pequeños Tipos que ocupan pocos bytes (≤16 bytes típicamente) Punto, Color RGB, Temperatura
Inmutabilidad Valores que no cambian después de crearse Coordenadas, Fechas personalizadas
Tipos de valor Cuando necesitas semántica de copia Dimensiones, Vectores matemáticos
Sin herencia No necesitas características de orientación a objetos Estructuras matemáticas simples
Alto rendimiento Cuando la eficiencia en memoria es crucial Cálculos intensivos, juegos

Casos donde NO usar struct

// NO recomendado para estructuras grandes
struct PersonaGrande // ❌ Evitar
{
    public string nombre;
    public string apellido;
    public string direccion;
    public string telefono;
    public string email;
    public DateTime fechaNacimiento;
    // ... muchos más campos
}

// Mejor usar class para datos complejos
class Persona // ✅ Recomendado
{
    public string Nombre { get; set; }
    public string Apellido { get; set; }
    // ... otros campos
}

Estructuras mutables vs inmutables

Es una buena práctica hacer las estructuras inmutables cuando sea posible:

// Estructura inmutable (recomendado)
struct PuntoInmutable
{
    private readonly double x;
    private readonly double y;
    
    public PuntoInmutable(double x, double y)
    {
        this.x = x;
        this.y = y;
    }
    
    public double X => x;
    public double Y => y;
    
    // Los métodos devuelven nuevas instancias en lugar de modificar
    public PuntoInmutable Mover(double deltaX, double deltaY)
    {
        return new PuntoInmutable(x + deltaX, y + deltaY);
    }
}

class Program
{
    static void Main()
    {
        PuntoInmutable punto1 = new PuntoInmutable(1, 2);
        PuntoInmutable punto2 = punto1.Mover(3, 4);
        
        Console.WriteLine($"Punto original: ({punto1.X}, {punto1.Y})");
        Console.WriteLine($"Punto movido: ({punto2.X}, {punto2.Y})");
    }
}

Resumen

Las estructuras en C# son una herramienta poderosa para crear tipos de datos personalizados que representan conceptos simples de nuestro dominio. Son tipos por valor que se almacenan eficientemente en la pila y proporcionan semántica de copia, lo que las hace ideales para representar datos pequeños e inmutables como coordenadas, dimensiones, o valores matemáticos.

Hemos aprendido a declarar estructuras con campos, constructores, propiedades y métodos, así como a comprender su comportamiento como tipos por valor. Las estructuras son especialmente útiles cuando necesitamos agrupar datos relacionados sin la complejidad de la orientación a objetos completa, y cuando el rendimiento y la eficiencia en memoria son importantes. En los próximos artículos exploraremos las clases, que nos darán capacidades más avanzadas para modelar conceptos complejos en nuestros programas.