La gestión de memoria y la recolección de basura son aspectos cruciales en la programación con C#. Entender cómo funciona la memoria y cómo se maneja en C# te permitirá escribir código más eficiente y evitar problemas comunes como las fugas de memoria.

  1. Introducción a la Gestión de Memoria

En C#, la memoria se divide principalmente en dos áreas:

  • Stack (Pila): Almacena variables de tipo valor y referencias a objetos.
  • Heap (Montón): Almacena objetos y datos dinámicos.

Stack vs Heap

Característica Stack Heap
Almacenamiento Variables de tipo valor y referencias a objetos Objetos y datos dinámicos
Gestión LIFO (Last In, First Out) Gestión dinámica
Velocidad Rápida Más lenta comparada con el stack
Tamaño Limitado Generalmente más grande que el stack

Ejemplo de Stack y Heap

public class Program
{
    public static void Main()
    {
        int x = 10; // Almacenado en el stack
        Person person = new Person(); // Referencia almacenada en el stack, objeto en el heap
        person.Name = "John";
    }
}

public class Person
{
    public string Name { get; set; }
}

En el ejemplo anterior:

  • x es una variable de tipo valor almacenada en el stack.
  • person es una referencia almacenada en el stack, mientras que el objeto Person se almacena en el heap.

  1. Recolección de Basura (Garbage Collection)

La recolección de basura en C# es un proceso automático que libera memoria ocupada por objetos que ya no son accesibles en el programa. El recolector de basura (GC) en .NET sigue un algoritmo de generación para optimizar el rendimiento.

Generaciones en Garbage Collection

  • Generación 0: Contiene objetos de vida corta. La recolección de basura ocurre frecuentemente en esta generación.
  • Generación 1: Contiene objetos que han sobrevivido a la recolección de la Generación 0.
  • Generación 2: Contiene objetos de larga vida. La recolección de basura ocurre menos frecuentemente en esta generación.

Funcionamiento del Garbage Collector

  1. Identificación de Objetos Inalcanzables: El GC identifica los objetos que ya no tienen referencias activas.
  2. Compactación de Memoria: El GC compacta la memoria moviendo los objetos alcanzables para eliminar fragmentación.
  3. Liberación de Memoria: La memoria ocupada por objetos inalcanzables se libera.

Ejemplo de Recolección de Basura

public class Program
{
    public static void Main()
    {
        CreateObjects();
        GC.Collect(); // Forzar la recolección de basura
    }

    public static void CreateObjects()
    {
        for (int i = 0; i < 1000; i++)
        {
            Person person = new Person();
        }
    }
}

public class Person
{
    public string Name { get; set; }
}

En el ejemplo anterior, se crean múltiples objetos Person en un bucle. Al llamar a GC.Collect(), se fuerza la recolección de basura, liberando la memoria ocupada por los objetos Person que ya no son accesibles.

  1. Buenas Prácticas para la Gestión de Memoria

  • Liberar Recursos No Administrados: Implementa la interfaz IDisposable y el patrón Dispose para liberar recursos no administrados.
  • Evitar Fugas de Memoria: Asegúrate de eliminar referencias a objetos que ya no necesitas.
  • Usar using para Recursos: Utiliza la declaración using para asegurar que los recursos se liberen correctamente.

Ejemplo de IDisposable y using

public class ResourceHolder : IDisposable
{
    private bool disposed = false;

    public void Dispose()
    {
        Dispose(true);
        GC.SuppressFinalize(this);
    }

    protected virtual void Dispose(bool disposing)
    {
        if (!disposed)
        {
            if (disposing)
            {
                // Liberar recursos administrados
            }
            // Liberar recursos no administrados
            disposed = true;
        }
    }

    ~ResourceHolder()
    {
        Dispose(false);
    }
}

public class Program
{
    public static void Main()
    {
        using (ResourceHolder resource = new ResourceHolder())
        {
            // Usar el recurso
        } // El recurso se libera automáticamente aquí
    }
}

  1. Ejercicios Prácticos

Ejercicio 1: Identificar el Stack y el Heap

Dado el siguiente código, identifica qué variables se almacenan en el stack y cuáles en el heap.

public class Program
{
    public static void Main()
    {
        int a = 5;
        string b = "Hello";
        Person person = new Person();
        person.Name = "Alice";
    }
}

public class Person
{
    public string Name { get; set; }
}

Solución:

  • a se almacena en el stack.
  • b es una referencia almacenada en el stack, mientras que el objeto string se almacena en el heap.
  • person es una referencia almacenada en el stack, mientras que el objeto Person y su propiedad Name se almacenan en el heap.

Ejercicio 2: Implementar IDisposable

Implementa la interfaz IDisposable en una clase FileManager que maneja un archivo.

public class FileManager : IDisposable
{
    private FileStream fileStream;
    private bool disposed = false;

    public FileManager(string filePath)
    {
        fileStream = new FileStream(filePath, FileMode.OpenOrCreate);
    }

    public void WriteToFile(string content)
    {
        byte[] info = new UTF8Encoding(true).GetBytes(content);
        fileStream.Write(info, 0, info.Length);
    }

    public void Dispose()
    {
        Dispose(true);
        GC.SuppressFinalize(this);
    }

    protected virtual void Dispose(bool disposing)
    {
        if (!disposed)
        {
            if (disposing)
            {
                // Liberar recursos administrados
                if (fileStream != null)
                {
                    fileStream.Close();
                    fileStream = null;
                }
            }
            // Liberar recursos no administrados
            disposed = true;
        }
    }

    ~FileManager()
    {
        Dispose(false);
    }
}

Uso de FileManager con using:

public class Program
{
    public static void Main()
    {
        using (FileManager fileManager = new FileManager("example.txt"))
        {
            fileManager.WriteToFile("Hello, World!");
        } // El recurso se libera automáticamente aquí
    }
}

Conclusión

En esta sección, hemos cubierto los conceptos fundamentales de la gestión de memoria y la recolección de basura en C#. Entender cómo funciona la memoria y cómo se maneja en C# es crucial para escribir código eficiente y evitar problemas comunes. Hemos aprendido sobre el stack y el heap, el funcionamiento del garbage collector, y las mejores prácticas para la gestión de memoria. Además, hemos practicado con ejercicios para reforzar estos conceptos. En el próximo módulo, exploraremos la construcción de aplicaciones en C#.

¡Continúa practicando y aplicando estos conceptos para mejorar tus habilidades en C#!

© Copyright 2024. Todos los derechos reservados