CSharp

Tutorial CSharp: Propiedades y encapsulación

Aprende los modificadores de acceso y propiedades en C# para controlar la encapsulación y proteger datos en tus clases con ejemplos prácticos.

Aprende CSharp y certifícate

Modificadores de acceso

Los modificadores de acceso son una parte fundamental de la encapsulación en C#, ya que permiten controlar la visibilidad y accesibilidad de los miembros de una clase (campos, métodos, propiedades) desde otras partes del código. Estos modificadores son la primera línea de defensa para proteger los datos internos de una clase.

En C#, existen cuatro modificadores de acceso principales que puedes utilizar para controlar quién puede acceder a los miembros de tus clases:

  • public: Acceso sin restricciones desde cualquier parte del código
  • private: Acceso limitado solo a la clase que contiene el miembro
  • protected: Acceso limitado a la clase que contiene el miembro y a sus clases derivadas
  • internal: Acceso limitado al ensamblado (proyecto) actual

Veamos cómo funcionan estos modificadores con ejemplos prácticos.

Modificador public

Cuando declaras un miembro como public, este puede ser accedido desde cualquier parte del código, tanto dentro como fuera de la clase. Es el nivel de acceso menos restrictivo.

public class Persona
{
    // Campo público (no recomendado)
    public string nombre;
    
    // Método público
    public void Saludar()
    {
        Console.WriteLine($"Hola, soy {nombre}");
    }
}

// Uso desde otra clase
class Program
{
    static void Main()
    {
        Persona persona = new Persona();
        persona.nombre = "Ana"; // Acceso directo al campo público
        persona.Saludar();      // Acceso al método público
    }
}

Aunque el código anterior funciona, exponer campos públicamente como nombre no es una buena práctica, ya que cualquier código podría modificar ese valor directamente, incluso asignando valores inválidos.

Modificador private

El modificador private es el más restrictivo y solo permite el acceso desde dentro de la misma clase. Es ideal para ocultar los detalles internos de implementación.

public class CuentaBancaria
{
    // Campo privado
    private decimal saldo;
    
    // Método público que usa el campo privado
    public void Depositar(decimal cantidad)
    {
        if (cantidad > 0)
        {
            saldo += cantidad;
            Console.WriteLine($"Depósito de {cantidad:C} realizado. Nuevo saldo: {saldo:C}");
        }
        else
        {
            Console.WriteLine("La cantidad a depositar debe ser mayor que cero.");
        }
    }
}

// Uso desde otra clase
class Program
{
    static void Main()
    {
        CuentaBancaria cuenta = new CuentaBancaria();
        cuenta.Depositar(100);
        
        // Esto generaría un error de compilación:
        // cuenta.saldo = -5000; // Error: 'saldo' es inaccesible debido a su nivel de protección
    }
}

En este ejemplo, el campo saldo está protegido contra modificaciones directas desde fuera de la clase. Solo se puede modificar a través del método Depositar, que incluye validaciones para garantizar que solo se acepten cantidades positivas.

Modificador protected

El modificador protected permite el acceso desde la clase que contiene el miembro y desde cualquier clase que herede de ella. Es útil cuando quieres que las clases derivadas puedan acceder a ciertos miembros sin exponerlos públicamente.

public class Vehiculo
{
    // Campo protegido
    protected int velocidad;
    
    public void Acelerar(int incremento)
    {
        if (incremento > 0)
        {
            velocidad += incremento;
            Console.WriteLine($"Acelerando a {velocidad} km/h");
        }
    }
}

// Clase derivada
public class Coche : Vehiculo
{
    public void MostrarVelocidadActual()
    {
        // Puede acceder al campo protegido de la clase base
        Console.WriteLine($"Velocidad actual del coche: {velocidad} km/h");
    }
    
    public void AplicarFrenoEmergencia()
    {
        // Puede modificar el campo protegido
        velocidad = 0;
        Console.WriteLine("¡Freno de emergencia aplicado!");
    }
}

En este ejemplo, la clase Coche puede acceder y modificar el campo velocidad heredado de Vehiculo porque está declarado como protected.

Modificador internal

El modificador internal limita el acceso a los miembros dentro del mismo ensamblado (proyecto). Es útil cuando quieres compartir funcionalidad entre clases del mismo proyecto sin exponerla a proyectos externos.

// En un archivo del proyecto
internal class ConfiguracionInterna
{
    internal static string ObtenerRutaArchivos()
    {
        return @"C:\Datos\Aplicacion";
    }
}

// En otro archivo del mismo proyecto
public class GestorArchivos
{
    public void GuardarArchivo(string nombre, byte[] datos)
    {
        // Puede acceder a la clase y método internal
        string ruta = ConfiguracionInterna.ObtenerRutaArchivos();
        string rutaCompleta = Path.Combine(ruta, nombre);
        
        // Código para guardar el archivo...
        Console.WriteLine($"Archivo guardado en: {rutaCompleta}");
    }
}

Combinación de modificadores

C# también permite combinar internal con protected para crear el modificador protected internal, que permite el acceso desde el mismo ensamblado o desde clases derivadas en otros ensamblados.

public class ComponenteBase
{
    // Accesible desde el mismo ensamblado o desde clases derivadas
    protected internal void InicializarComponente()
    {
        Console.WriteLine("Componente inicializado");
    }
}

Beneficios de usar modificadores de acceso adecuados

Utilizar los modificadores de acceso correctamente proporciona varios beneficios:

  • Encapsulación: Oculta los detalles de implementación y expone solo lo necesario.
  • Seguridad: Previene modificaciones no autorizadas de los datos.
  • Mantenibilidad: Facilita los cambios internos sin afectar al código que usa la clase.
  • Robustez: Permite implementar validaciones antes de modificar el estado interno.

Ejemplo práctico: Encapsulación con modificadores de acceso

Veamos un ejemplo completo que muestra cómo los modificadores de acceso contribuyen a la encapsulación:

public class Empleado
{
    // Campos privados
    private string nombre;
    private int edad;
    private decimal salario;
    
    // Constructor público
    public Empleado(string nombre, int edad, decimal salarioInicial)
    {
        this.nombre = nombre;
        
        // Validación de la edad
        if (edad >= 18 && edad <= 65)
            this.edad = edad;
        else
            throw new ArgumentException("La edad debe estar entre 18 y 65 años");
            
        // Validación del salario
        if (salarioInicial >= 0)
            this.salario = salarioInicial;
        else
            throw new ArgumentException("El salario no puede ser negativo");
    }
    
    // Método público
    public void AumentarSalario(decimal porcentaje)
    {
        if (porcentaje > 0)
        {
            decimal aumento = salario * porcentaje / 100;
            salario += aumento;
            Console.WriteLine($"{nombre} recibió un aumento del {porcentaje}%. Nuevo salario: {salario:C}");
        }
        else
        {
            Console.WriteLine("El porcentaje de aumento debe ser positivo");
        }
    }
    
    // Método público que muestra información
    public void MostrarInformacion()
    {
        Console.WriteLine($"Empleado: {nombre}, Edad: {edad}, Salario: {salario:C}");
    }
    
    // Método privado (solo para uso interno)
    private bool EsElegibleParaBono()
    {
        // Lógica interna que no necesita ser expuesta
        return edad > 30 && salario < 50000;
    }
    
    // Método público que usa el método privado
    public void ProcesarBonoAnual()
    {
        if (EsElegibleParaBono())
        {
            decimal bono = salario * 0.1m;
            salario += bono;
            Console.WriteLine($"{nombre} recibió un bono anual de {bono:C}");
        }
        else
        {
            Console.WriteLine($"{nombre} no es elegible para bono anual");
        }
    }
}

En este ejemplo:

  • Los campos nombre, edad y salario son privados, lo que impide su modificación directa desde fuera de la clase.
  • El constructor valida los datos antes de asignarlos a los campos privados.
  • El método AumentarSalario proporciona una forma controlada de modificar el salario, incluyendo validaciones.
  • El método EsElegibleParaBono es privado porque contiene lógica interna que no necesita ser expuesta.
  • Los métodos MostrarInformacion y ProcesarBonoAnual son públicos porque representan operaciones que los usuarios de la clase necesitan realizar.

Así es como se utilizaría esta clase:

class Program
{
    static void Main()
    {
        try
        {
            Empleado empleado = new Empleado("Carlos Gómez", 35, 45000);
            empleado.MostrarInformacion();
            
            empleado.AumentarSalario(5);
            empleado.ProcesarBonoAnual();
            
            // Esto no sería posible debido a los modificadores de acceso:
            // empleado.salario = -10000; // Error: 'salario' es inaccesible
            // empleado.EsElegibleParaBono(); // Error: 'EsElegibleParaBono' es inaccesible
        }
        catch (ArgumentException ex)
        {
            Console.WriteLine($"Error: {ex.Message}");
        }
    }
}

Este enfoque de encapsulación mediante modificadores de acceso garantiza que los datos del empleado se mantengan en un estado válido y que solo puedan ser modificados a través de métodos controlados que implementan la lógica de negocio necesaria.

Propiedades getter/setter

Las propiedades en C# son una característica fundamental que mejora la encapsulación al proporcionar una interfaz pública para acceder y modificar los campos privados de una clase. A diferencia de los campos públicos, las propiedades permiten implementar lógica adicional como validaciones, cálculos o notificaciones cuando se lee o modifica un valor.

Una propiedad completa en C# consta de dos partes principales:

  • Un getter (accesador): método que permite leer el valor
  • Un setter (mutador): método que permite modificar el valor

Estructura básica de una propiedad

La sintaxis básica de una propiedad con getter y setter es la siguiente:

private tipo campoPrivado;

public tipo NombrePropiedad
{
    get { return campoPrivado; }
    set { campoPrivado = value; }
}

Donde:

  • tipo es el tipo de dato de la propiedad
  • campoPrivado es el campo privado que almacena el valor real
  • NombrePropiedad es el nombre de la propiedad (por convención, en PascalCase)
  • value es una palabra clave especial que representa el valor que se está asignando

Ejemplo práctico de propiedades

Veamos un ejemplo sencillo de una clase Producto que utiliza propiedades para encapsular sus datos:

public class Producto
{
    // Campos privados
    private string nombre;
    private decimal precio;
    private int stock;

    // Propiedad para el nombre
    public string Nombre
    {
        get { return nombre; }
        set { nombre = value; }
    }

    // Propiedad para el precio con validación
    public decimal Precio
    {
        get { return precio; }
        set 
        { 
            if (value >= 0)
                precio = value;
            else
                throw new ArgumentException("El precio no puede ser negativo");
        }
    }

    // Propiedad para el stock con validación
    public int Stock
    {
        get { return stock; }
        set 
        { 
            if (value >= 0)
                stock = value;
            else
                throw new ArgumentException("El stock no puede ser negativo");
        }
    }
}

Uso de propiedades vs campos públicos

Para entender mejor por qué las propiedades son preferibles a los campos públicos, comparemos dos enfoques:

Enfoque 1: Usando campos públicos (no recomendado)

public class ProductoMalo
{
    public string Nombre;    // Campo público
    public decimal Precio;   // Campo público
    public int Stock;        // Campo público
}

// Uso:
ProductoMalo producto = new ProductoMalo();
producto.Precio = -50;  // ¡Permitido pero incorrecto!

Enfoque 2: Usando propiedades (recomendado)

public class ProductoBueno
{
    private string nombre;
    private decimal precio;
    private int stock;

    public string Nombre
    {
        get { return nombre; }
        set { nombre = value; }
    }

    public decimal Precio
    {
        get { return precio; }
        set 
        { 
            if (value >= 0)
                precio = value;
            else
                throw new ArgumentException("El precio no puede ser negativo");
        }
    }

    public int Stock
    {
        get { return stock; }
        set 
        { 
            if (value >= 0)
                stock = value;
            else
                throw new ArgumentException("El stock no puede ser negativo");
        }
    }
}

// Uso:
ProductoBueno producto = new ProductoBueno();
try
{
    producto.Precio = -50;  // Lanza excepción
}
catch (ArgumentException ex)
{
    Console.WriteLine(ex.Message);  // "El precio no puede ser negativo"
}

Ventajas de usar propiedades

Las propiedades ofrecen varias ventajas importantes sobre los campos públicos:

  • Validación de datos: Puedes verificar que los valores asignados cumplan con ciertas reglas.
  • Cálculos derivados: Puedes calcular valores sobre la marcha en lugar de almacenarlos.
  • Notificaciones: Puedes ejecutar código adicional cuando un valor cambia.
  • Depuración: Puedes establecer puntos de interrupción en getters/setters para rastrear cambios.
  • Compatibilidad con interfaces: Las interfaces pueden definir propiedades pero no campos.
  • Flexibilidad: Puedes comenzar con una propiedad simple y agregar lógica más tarde sin cambiar la interfaz pública.

Propiedades de solo lectura y solo escritura

Puedes crear propiedades que solo permitan leer o escribir valores:

public class Usuario
{
    private string nombre;
    private DateTime fechaRegistro;

    // Propiedad de lectura y escritura
    public string Nombre
    {
        get { return nombre; }
        set { nombre = value; }
    }

    // Propiedad de solo lectura (no tiene setter)
    public DateTime FechaRegistro
    {
        get { return fechaRegistro; }
    }

    // Propiedad de solo escritura (no tiene getter)
    public string Contraseña
    {
        set { GuardarContraseñaEncriptada(value); }
    }

    private void GuardarContraseñaEncriptada(string contraseña)
    {
        // Código para encriptar y guardar la contraseña
        Console.WriteLine("Contraseña guardada de forma segura");
    }

    // Constructor
    public Usuario()
    {
        fechaRegistro = DateTime.Now;
    }
}

Propiedades calculadas

Las propiedades también pueden calcular valores dinámicamente sin necesidad de almacenarlos:

public class Rectangulo
{
    private double ancho;
    private double alto;

    public double Ancho
    {
        get { return ancho; }
        set 
        { 
            if (value > 0)
                ancho = value;
            else
                throw new ArgumentException("El ancho debe ser positivo");
        }
    }

    public double Alto
    {
        get { return alto; }
        set 
        { 
            if (value > 0)
                alto = value;
            else
                throw new ArgumentException("El alto debe ser positivo");
        }
    }

    // Propiedad calculada (no tiene campo de respaldo)
    public double Area
    {
        get { return ancho * alto; }
    }

    // Propiedad calculada
    public double Perimetro
    {
        get { return 2 * (ancho + alto); }
    }
}

En este ejemplo, Area y Perimetro son propiedades calculadas que no almacenan datos, sino que calculan un valor basado en otros campos.

Propiedades con diferentes niveles de acceso

Puedes definir diferentes niveles de acceso para los getters y setters:

public class Empleado
{
    private string nombre;
    private decimal salario;

    // Propiedad con getter público y setter privado
    public string Nombre
    {
        get { return nombre; }
        private set { nombre = value; }
    }

    // Propiedad con getter público y setter protegido
    public decimal Salario
    {
        get { return salario; }
        protected set 
        { 
            if (value >= 0)
                salario = value;
            else
                throw new ArgumentException("El salario no puede ser negativo");
        }
    }

    // Constructor
    public Empleado(string nombre, decimal salarioInicial)
    {
        this.Nombre = nombre;  // Usa el setter privado
        this.Salario = salarioInicial;  // Usa el setter protegido
    }
}

// Clase derivada
public class Gerente : Empleado
{
    public Gerente(string nombre, decimal salarioInicial) 
        : base(nombre, salarioInicial)
    {
    }

    public void AumentarSalario(decimal porcentaje)
    {
        if (porcentaje > 0)
        {
            decimal nuevoSalario = Salario * (1 + porcentaje / 100);
            Salario = nuevoSalario;  // Puede usar el setter protegido
        }
    }
}

En este ejemplo:

  • Nombre tiene un getter público pero un setter privado, lo que significa que solo se puede establecer dentro de la clase Empleado.
  • Salario tiene un getter público pero un setter protegido, lo que permite que las clases derivadas como Gerente modifiquen el salario.

Ejemplo completo: Sistema de gestión de biblioteca

Veamos un ejemplo más completo que muestra cómo las propiedades pueden mejorar la encapsulación en una aplicación real:

public class Libro
{
    // Campos privados
    private string titulo;
    private string autor;
    private string isbn;
    private int paginas;
    private bool prestado;
    private DateTime? fechaPrestamo;

    // Propiedades
    public string Titulo
    {
        get { return titulo; }
        set 
        { 
            if (!string.IsNullOrWhiteSpace(value))
                titulo = value;
            else
                throw new ArgumentException("El título no puede estar vacío");
        }
    }

    public string Autor
    {
        get { return autor; }
        set 
        { 
            if (!string.IsNullOrWhiteSpace(value))
                autor = value;
            else
                throw new ArgumentException("El autor no puede estar vacío");
        }
    }

    public string ISBN
    {
        get { return isbn; }
        set 
        { 
            if (!string.IsNullOrWhiteSpace(value) && value.Length == 13)
                isbn = value;
            else
                throw new ArgumentException("El ISBN debe tener 13 caracteres");
        }
    }

    public int Paginas
    {
        get { return paginas; }
        set 
        { 
            if (value > 0)
                paginas = value;
            else
                throw new ArgumentException("El número de páginas debe ser positivo");
        }
    }

    // Propiedad de solo lectura
    public bool EstaPrestado
    {
        get { return prestado; }
    }

    // Propiedad de solo lectura
    public DateTime? FechaPrestamo
    {
        get { return fechaPrestamo; }
    }

    // Propiedad calculada
    public bool EstaDisponible
    {
        get { return !prestado; }
    }

    // Constructor
    public Libro(string titulo, string autor, string isbn, int paginas)
    {
        Titulo = titulo;    // Usa la propiedad, no el campo
        Autor = autor;      // Usa la propiedad, no el campo
        ISBN = isbn;        // Usa la propiedad, no el campo
        Paginas = paginas;  // Usa la propiedad, no el campo
        prestado = false;
        fechaPrestamo = null;
    }

    // Métodos que modifican el estado
    public void Prestar()
    {
        if (!prestado)
        {
            prestado = true;
            fechaPrestamo = DateTime.Now;
            Console.WriteLine($"Libro '{titulo}' prestado con éxito.");
        }
        else
        {
            Console.WriteLine($"El libro '{titulo}' ya está prestado.");
        }
    }

    public void Devolver()
    {
        if (prestado)
        {
            prestado = false;
            fechaPrestamo = null;
            Console.WriteLine($"Libro '{titulo}' devuelto con éxito.");
        }
        else
        {
            Console.WriteLine($"El libro '{titulo}' no estaba prestado.");
        }
    }

    // Método que usa las propiedades
    public void MostrarInformacion()
    {
        Console.WriteLine($"Título: {Titulo}");
        Console.WriteLine($"Autor: {Autor}");
        Console.WriteLine($"ISBN: {ISBN}");
        Console.WriteLine($"Páginas: {Paginas}");
        Console.WriteLine($"Estado: {(EstaDisponible ? "Disponible" : "Prestado")}");
        
        if (fechaPrestamo.HasValue)
            Console.WriteLine($"Fecha de préstamo: {fechaPrestamo.Value.ToShortDateString()}");
    }
}

Y así es como se utilizaría esta clase:

class Program
{
    static void Main()
    {
        try
        {
            Libro libro = new Libro(
                "El Quijote", 
                "Miguel de Cervantes", 
                "9788420412146", 
                863);
                
            libro.MostrarInformacion();
            
            libro.Prestar();
            Console.WriteLine($"¿Está disponible? {libro.EstaDisponible}");
            Console.WriteLine($"Fecha de préstamo: {libro.FechaPrestamo}");
            
            libro.Devolver();
            Console.WriteLine($"¿Está disponible? {libro.EstaDisponible}");
            
            // Esto generaría una excepción:
            // libro.ISBN = "123"; // Error: El ISBN debe tener 13 caracteres
        }
        catch (ArgumentException ex)
        {
            Console.WriteLine($"Error: {ex.Message}");
        }
    }
}

En este ejemplo, las propiedades nos permiten:

  1. Validar datos (título, autor, ISBN, páginas)
  2. Proporcionar propiedades de solo lectura (EstaPrestado, FechaPrestamo)
  3. Crear propiedades calculadas (EstaDisponible)
  4. Encapsular la lógica de préstamo y devolución en métodos específicos

Este enfoque garantiza que el objeto Libro siempre se mantenga en un estado válido y que las operaciones que modifican su estado se realicen de manera controlada.

Propiedades auto-implementadas

Las propiedades auto-implementadas son una característica de C# que simplifica enormemente la sintaxis para crear propiedades cuando no se necesita lógica adicional en los accesores get y set. Introducidas en C# 3.0, estas propiedades permiten declarar propiedades sin tener que definir explícitamente un campo de respaldo.

Sintaxis básica

La sintaxis de una propiedad auto-implementada es mucho más concisa que la de una propiedad tradicional:

// Propiedad auto-implementada
public tipo NombrePropiedad { get; set; }

Cuando se utiliza esta sintaxis, el compilador de C# genera automáticamente un campo privado de respaldo y proporciona implementaciones básicas para los métodos get y set. Este campo generado no es directamente accesible en el código.

Comparación con propiedades tradicionales

Para entender mejor las ventajas de las propiedades auto-implementadas, comparemos ambos enfoques:

Propiedad tradicional:

// Campo privado de respaldo
private string nombre;

// Propiedad con implementación explícita
public string Nombre
{
    get { return nombre; }
    set { nombre = value; }
}

Propiedad auto-implementada:

// Propiedad auto-implementada (el campo de respaldo es generado por el compilador)
public string Nombre { get; set; }

Como puedes ver, la versión auto-implementada es mucho más concisa y requiere menos código, lo que hace que las clases sean más limpias y fáciles de leer.

Cuándo usar propiedades auto-implementadas

Las propiedades auto-implementadas son ideales cuando:

  • No necesitas lógica adicional en los accesores get o set
  • No requieres validaciones al asignar valores
  • No necesitas transformar los datos al leerlos o escribirlos
  • Simplemente quieres encapsular un campo con accesores básicos

Ejemplo práctico

Veamos un ejemplo de una clase Persona que utiliza propiedades auto-implementadas:

public class Persona
{
    // Propiedades auto-implementadas
    public string Nombre { get; set; }
    public string Apellido { get; set; }
    public int Edad { get; set; }
    
    // Propiedad calculada (no auto-implementada)
    public string NombreCompleto
    {
        get { return $"{Nombre} {Apellido}"; }
    }
    
    // Constructor
    public Persona(string nombre, string apellido, int edad)
    {
        Nombre = nombre;
        Apellido = apellido;
        Edad = edad;
    }
}

Y así es como se utilizaría:

Persona persona = new Persona("Juan", "Pérez", 30);
Console.WriteLine(persona.NombreCompleto); // Muestra "Juan Pérez"

// Modificar una propiedad
persona.Edad = 31;

Propiedades auto-implementadas de solo lectura

A partir de C# 6.0, puedes crear propiedades auto-implementadas de solo lectura omitiendo el modificador set:

public class Producto
{
    // Propiedad auto-implementada de solo lectura
    public string Codigo { get; }
    
    // Propiedades auto-implementadas normales
    public string Nombre { get; set; }
    public decimal Precio { get; set; }
    
    public Producto(string codigo)
    {
        // Las propiedades de solo lectura solo pueden asignarse en el constructor
        Codigo = codigo;
    }
}

En este ejemplo, Codigo es una propiedad de solo lectura que solo puede asignarse en el constructor. Después de la inicialización del objeto, su valor no puede cambiar.

Inicialización de propiedades auto-implementadas

Desde C# 6.0, también puedes inicializar propiedades auto-implementadas directamente en su declaración:

public class Configuracion
{
    // Propiedades auto-implementadas con valores iniciales
    public bool ModoOscuro { get; set; } = false;
    public string Idioma { get; set; } = "Español";
    public int TamañoFuente { get; set; } = 12;
    
    // Propiedad de solo lectura con valor inicial
    public DateTime FechaCreacion { get; } = DateTime.Now;
}

Esta característica es especialmente útil para establecer valores predeterminados sin tener que hacerlo en el constructor.

Modificadores de acceso en accesores

También puedes aplicar diferentes modificadores de acceso a los accesores get y set en propiedades auto-implementadas:

public class Cliente
{
    // Propiedad con getter público y setter privado
    public string Id { get; private set; }
    
    // Propiedad con getter y setter públicos
    public string Nombre { get; set; }
    
    public Cliente(string id, string nombre)
    {
        Id = id;        // Podemos asignar aquí porque estamos dentro de la clase
        Nombre = nombre;
    }
    
    public void GenerarNuevoId()
    {
        // Podemos modificar Id dentro de la clase
        Id = Guid.NewGuid().ToString();
    }
}

En este ejemplo, el Id del cliente solo puede modificarse dentro de la clase Cliente, mientras que Nombre puede modificarse desde cualquier lugar.

Ejemplo completo: Sistema de gestión de inventario

Veamos un ejemplo más completo que muestra cómo las propiedades auto-implementadas pueden simplificar el código en una aplicación real:

public class Articulo
{
    // Propiedades auto-implementadas de solo lectura
    public string Codigo { get; }
    public DateTime FechaRegistro { get; }
    
    // Propiedades auto-implementadas normales
    public string Nombre { get; set; }
    public string Descripcion { get; set; }
    public decimal Precio { get; set; }
    
    // Propiedad auto-implementada con setter privado
    public int Stock { get; private set; }
    
    // Constructor
    public Articulo(string codigo, string nombre, decimal precio, int stockInicial)
    {
        Codigo = codigo;
        FechaRegistro = DateTime.Now;
        Nombre = nombre;
        Precio = precio;
        Stock = stockInicial;
    }
    
    // Métodos que modifican el stock
    public bool RetirarStock(int cantidad)
    {
        if (cantidad <= 0)
        {
            Console.WriteLine("La cantidad a retirar debe ser positiva");
            return false;
        }
        
        if (cantidad > Stock)
        {
            Console.WriteLine("No hay suficiente stock disponible");
            return false;
        }
        
        Stock -= cantidad;
        Console.WriteLine($"Stock actualizado: {Stock} unidades");
        return true;
    }
    
    public void AgregarStock(int cantidad)
    {
        if (cantidad <= 0)
        {
            Console.WriteLine("La cantidad a agregar debe ser positiva");
            return;
        }
        
        Stock += cantidad;
        Console.WriteLine($"Stock actualizado: {Stock} unidades");
    }
    
    // Método que muestra información del artículo
    public void MostrarInformacion()
    {
        Console.WriteLine($"Código: {Codigo}");
        Console.WriteLine($"Nombre: {Nombre}");
        Console.WriteLine($"Descripción: {Descripcion ?? "No disponible"}");
        Console.WriteLine($"Precio: {Precio:C}");
        Console.WriteLine($"Stock: {Stock} unidades");
        Console.WriteLine($"Fecha de registro: {FechaRegistro}");
    }
}

Y así es como se utilizaría esta clase:

// Crear un nuevo artículo
Articulo telefono = new Articulo(
    "TECH-001",
    "Smartphone XYZ",
    599.99m,
    10);

// Establecer descripción
telefono.Descripcion = "Smartphone de última generación con 128GB de almacenamiento";

// Mostrar información
telefono.MostrarInformacion();

// Retirar stock
telefono.RetirarStock(3);

// Agregar stock
telefono.AgregarStock(5);

// Esto no sería posible debido al setter privado:
// telefono.Stock = 100; // Error: El setter de 'Stock' es inaccesible

// Esto no sería posible debido a que son propiedades de solo lectura:
// telefono.Codigo = "TECH-002"; // Error: La propiedad 'Codigo' es de solo lectura
// telefono.FechaRegistro = DateTime.Now; // Error: La propiedad 'FechaRegistro' es de solo lectura

Ventajas de las propiedades auto-implementadas

Las propiedades auto-implementadas ofrecen varias ventajas importantes:

  • Código más limpio: Reducen significativamente la cantidad de código repetitivo.
  • Menos errores: Al no tener que escribir manualmente los campos de respaldo, se eliminan posibles errores de tipeo.
  • Mantenibilidad: Si más adelante necesitas agregar lógica a una propiedad, puedes convertirla en una propiedad completa sin cambiar la interfaz pública.
  • Encapsulación: Siguen proporcionando los beneficios de encapsulación de las propiedades tradicionales.

Cuándo evitar propiedades auto-implementadas

Aunque son muy útiles, hay situaciones donde no deberías usar propiedades auto-implementadas:

  • Cuando necesitas validar los datos antes de asignarlos
  • Cuando necesitas notificar a otros componentes cuando cambia un valor
  • Cuando necesitas transformar los datos al leerlos o escribirlos
  • Cuando necesitas lógica personalizada en los accesores get o set

En estos casos, deberías usar propiedades completas con implementación explícita.

Migración de campos a propiedades

Una ventaja importante de las propiedades auto-implementadas es que facilitan la migración de campos públicos a propiedades sin romper el código existente:

// Versión 1: Usando campos públicos (mala práctica)
public class ClienteV1
{
    public string Nombre;
    public string Email;
}

// Versión 2: Migración a propiedades auto-implementadas (mejor práctica)
public class ClienteV2
{
    public string Nombre { get; set; }
    public string Email { get; set; }
}

El código que usa ClienteV1 seguirá funcionando sin cambios con ClienteV2, pero ahora tienes la flexibilidad de agregar lógica a las propiedades en el futuro si es necesario.

Propiedades auto-implementadas vs campos públicos

Para ilustrar por qué las propiedades auto-implementadas son mejores que los campos públicos, consideremos este ejemplo:

// Enfoque 1: Usando campos públicos (no recomendado)
public class VehiculoMalo
{
    public string Marca;
    public int Año;
    public int Kilometraje;
}

// Uso:
VehiculoMalo coche = new VehiculoMalo();
coche.Marca = "Toyota";
coche.Año = 2020;
coche.Kilometraje = -5000; // ¡Permitido pero incorrecto!

Si más tarde necesitamos agregar validación para el kilometraje, tendríamos que cambiar el campo por una propiedad, lo que podría romper el código existente.

Enfoque 2: Usando propiedades auto-implementadas (recomendado)

public class VehiculoBueno
{
    public string Marca { get; set; }
    public int Año { get; set; }
    public int Kilometraje { get; set; }
}

// Uso:
VehiculoBueno coche = new VehiculoBueno();
coche.Marca = "Toyota";
coche.Año = 2020;
coche.Kilometraje = -5000; // Aún permitido, pero podemos agregar validación después

Si más tarde necesitamos agregar validación, podemos convertir la propiedad auto-implementada en una propiedad completa sin cambiar la interfaz pública:

public class VehiculoMejorado
{
    public string Marca { get; set; }
    public int Año { get; set; }
    
    // Campo privado de respaldo (ahora explícito)
    private int kilometraje;
    
    // Propiedad completa con validación
    public int Kilometraje
    {
        get { return kilometraje; }
        set 
        { 
            if (value >= 0)
                kilometraje = value;
            else
                throw new ArgumentException("El kilometraje no puede ser negativo");
        }
    }
}

El código que usa VehiculoBueno seguirá funcionando con VehiculoMejorado, excepto que ahora se validará el kilometraje.

CONSTRUYE TU CARRERA EN IA Y PROGRAMACIÓN SOFTWARE

Accede a +1000 lecciones y cursos con certificado. Mejora tu portfolio con certificados de superación para tu CV.

30 % DE DESCUENTO

Plan mensual

19.00 /mes

13.30 € /mes

Precio normal mensual: 19 €
63 % DE DESCUENTO

Plan anual

10.00 /mes

7.00 € /mes

Ahorras 144 € al año
Precio normal anual: 120 €
Aprende CSharp online

Ejercicios de esta lección Propiedades y encapsulación

Evalúa tus conocimientos de esta lección Propiedades y encapsulación 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

  • Comprender los diferentes modificadores de acceso en C# y su impacto en la visibilidad de miembros.
  • Aprender a usar propiedades con getters y setters para encapsular campos privados.
  • Diferenciar entre propiedades tradicionales y auto-implementadas y cuándo usar cada una.
  • Implementar validaciones y lógica en propiedades para mantener la integridad de los datos.
  • Aplicar encapsulación para proteger el estado interno de los objetos y mejorar la mantenibilidad del código.