A gestão de memória e a gestão de recursos são dois conceitos facilmente confundíveis quando se começa a desenvolver software. Conhecer a diferença entre eles é importante para escrever software sem erros e de fácil manutenção.
O objetivo deste artigo é discutir as diferenças entre os dois conceitos e esclarecer qualquer dúvida existente.
Gestão de memória
A memória é um recurso gerido – por outras palavras, está sob o controlo direto do tempo de execução. Quando um objeto sai de cena e deixa de ser referenciado, o coletor de lixo (Garbage Collector) limpa automaticamente a memória, eventualmente. Assim, como programadores, não precisamos de nos preocupar com a libertação de memória.
É importante notar que, embora seja possível chamar manualmente o método estático GC.Collect() para instruir o coletor de lixo a libertar a memória, geralmente não é aconselhável fazê-lo, pois faria mais mal do que bem.
A maneira exata como a recolha de lixo acontece está fora do âmbito deste artigo, mas resumidamente: os objetos na memória Heap passam por uma fase de marcação, durante a qual objetos que ainda são referenciados são “marcados” para não serem recolhidos pelo coletor de lixo. Depois, passamos para a segunda fase, na qual os objetos não marcados são recolhidos.
Gestão de recursos
Por outro lado, recursos como os identificadores de ficheiros ou as ligações de rede abertas são conhecidos como recursos não geridos, isto é, são executados fora do tempo de execução do .NET. Estes recursos não são automaticamente “geridos” pelo CLR (Common Language Runtime), pelo que têm de ser tratados pelo programador.
A principal forma de lidar com esses recursos no .NET é implementando a interface “IDisposable” (falaremos sobre isso mais à frente neste artigo). Desta forma, o consumidor do objeto sabe que este deve ser explicitamente eliminado quando já não for necessário. Este processo é conhecido como gestão determinística de recursos, o que significa que permite ao utilizador desalocar o recurso de forma determinística.
Como nota adicional, é preferível, ao tentar eliminar um objeto, utilizar a palavra-chave “using”, para garantir que o método “Dispose” é chamado mesmo que ocorra uma exceção no bloco. Caso contrário, envolve o objeto num bloco “try catch” e chama o método Dispose() dentro do bloco finally.
using (var disposableObject = new SomeDisposableObject())
{
// Code that uses disposableObject
}
Em vez de:
var disposableObject = new SomeDisposableObject();
try
{
// Code that uses disposableObject
}
catch (Exception ex)
{
}
finally
{
disposableObject.Dispose();
}
Outra forma de libertação de recursos é através de finalizadores (destrutores). Os finalizadores são chamados na fase de recolha de lixo quando o consumidor não se desfaz corretamente de um objeto que possui recursos não geridos. Este processo é conhecido como gestão não determinística de recursos, uma vez que o utilizador não sabe exatamente quando o recurso será desalocado.
É importante notar que os finalizadores introduzem uma carga computacional adicional e devem, geralmente, ser evitados.
Implementação do padrão “IDisposable”
A razão pela qual deixei de fora a implementação do padrão “IDisposable” até agora, é porque precisávamos de conhecer antes os finalizadores.
Geralmente, existem dois cenários:
- Existe apenas uma classe que contém outros recursos geridos, ou seja, objetos que implementam IDisposable (fluxos de ficheiros, conexões de bases de dados, etc.). Na maioria das situações, este é o cenário normal.
- Existe uma classe que contém diretamente um recurso não gerido (recursos que pertencem ao sistema operativo, como os identificadores de ficheiros, identificadores de janelas, entre outros).
Primeiro cenário
A implementação é simples:
public class MyResource : IDisposable
{
public void Dispose()
{
// Clean up managed resources, call Dispose on member variables..
}
Segundo cenário
Precisamos de implementar um finalizador, mas antes de aí chegarmos, é importante notar que a necessidade de implementar explicitamente um finalizador e lidar diretamente com recursos não geridos é relativamente rara. Na maioria das vezes, usamos tipos existentes (como FileStream, HttpClient...) que já lidam com a gestão de recursos corretamente.
Assim, a menos que estejas a manter código legado que envolva lidar com API mais antigas, ou a trabalhar numa biblioteca de nível de baixo onde a gestão direta de recursos é necessária, provavelmente só terás de lidar com o primeiro cenário.
Quanto ao código para implementar um finalizador:
public class MyResource : IDisposable
{
private bool _disposed = false;
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
protected virtual void Dispose(bool disposing)
{
if (!_disposed)
{
if (disposing)
{
// Clean up managed resources, dispose other IDisposable objects here
}
// Clean up unmanaged resources (release native resources, file handles etc)
_disposed = true;
}
}
// Finalizer
~MyResource()
{
Dispose(false);
}
}
Algumas notas sobre isto:
- Quando Dispose(true) é chamado, indica que o objeto está a ser explicitamente eliminado com o método Dispose(). Nesse caso, devemos libertar os recursos geridos e não geridos.
- Como explicado anteriormente, o finalizador é executado durante a recolha de lixo como uma rede de segurança, caso o consumidor da classe não chame o método Dispose() explicitamente. Nele, chamamos Dispose(false) para libertar apenas recursos não geridos. Ao passar o false, evitamos entrar novamente no código gerido (uma vez que o coletor de lixo já está a fazer a limpeza), prevenindo assim potenciais problemas como o acesso a objetos geridos eliminados.
- Chamamos GC.SuppressFinalize(this) no método Dispose()para informar o coletor de lixo que o finalizador para este objeto não deve ser executado, evitando sobrecarga desnecessária durante a recolha de lixo.
Nota: é crucial seguir este padrão para evitar fugas de recursos e garantir a limpeza adequada.