"Lanzar ou non lanzar, esa é a cuestión. Se é máis nobre para o código sufrir as frechas dos erros inesperados, ou tomar as armas contra un mar de problemas e, opóndose a eles, atrapalos." Hamlet, Acto C#, Escena Try-Catch
TL;DR (Resumo)
- As excepcións son custosas: Lanzar unha excepción é 50-335 veces máis lento que un simple if ou un método Try*.
- Cando usalas: Para erros xenuinamente excepcionais (rede caída, arquivo inexistente).
- Cando NON usalas: Para control de fluxo normal, validación de entradas ou en código crítico.
- Alternativas: Usa TryParse, TryGetValue e similares cando esperas erros frecuentes.
- Mellor práctica: O simple feito de ter bloques try/catch ralentiza o teu código un 20%.
Que é unha excepción, realmente?
En C#, unha excepción é un mecanismo para manexar erros de forma estruturada. Lanzar (throw
) e capturar (try/catch
) unha excepción é máis que un if
con esteroides: implica crear un obxecto, recompilar o stack trace, e alterar o fluxo do programa bruscamente.
É dicir, non é de balde. É como enviar a Ofelia ao río con todas as túas variables de erro - dramático pero custoso.
A proba do algodón: medindo o custo
"A verdade demóstrase con feitos, non con palabras", como diría Polonio así que imos poñer números ao drama das excepcións.
Preparei este simple escenario de batalla onde unha excepción enfróntase a unha validación simple: Buscar unha clave no noso dicionario.
[Benchmark]
public void ConIfSimple()
{
// Escenario cotiá: buscar un valor nun dicionario
var dict = new Dictionary<string, int> { { "un", 1 }, { "dous", 2 } };
if (dict.ContainsKey("tres"))
Console.WriteLine("Atopado");
else
_ = 0; // Non facer nada
}
[Benchmark]
public void ConExcepcionSinLanzar()
{
// Mesmo escenario pero con try-catch (sen chegar a lanzar)
var dict = new Dictionary<string, int> { { "un", 1 }, { "dous", 2 } };
try
{
var valor = dict["tres"];
Console.WriteLine("Atopado");
}
catch (KeyNotFoundException) { _ = 0; }
}
[Benchmark]
public void ConExcepcionLanzando()
{
// Mesmo escenario pero lanzando excepcións
var dict = new Dictionary<string, int> { { "un", 1 }, { "dous", 2 } };
try
{
throw new KeyNotFoundException();
}
catch (KeyNotFoundException) { Console.WriteLine("Excepción lanzada"); }
}
[Benchmark]
public void ConTryGetValue()
{
// A alternativa óptima
var dict = new Dictionary<string, int> { { "un", 1 }, { "dous", 2 } };
if (dict.TryGetValue("tres", out var valor))
Console.WriteLine("Atopado");
else
_ = 0; // Non facer nada
}
Os resultados son tan dramáticos como a morte de Ofelia:
| Method | Mean | Error |
|-------------------------:|-------------:|--------------:|
| ConIfSimple | 0.2989 ms | 2.4094 ms |
| ConExcepcionSinLanzar | 2.0568 ms | 16.6938 ms |
| ConExcepcionLanzando | 3.0651 ms | 24.9719 ms |
| ConTryGetValue | 0.2999 ms | 2.4416 ms |
Por todos os fantasmas de Elsinor! Vexamos que temos aquí:
- ConIfSimple: A nosa liña base cun simple
if
. Eficiente e directo. - ConExcepcionSinLanzar: O simple feito de ter un
try-catch
xa ten un gran custo (un 588% máis), aínda que nunca se lance a excepción. - ConExcepcionLanzando: 925.4% veces máis lento! Lanzar excepcións é unha traxedia para o rendemento.
- ConTryGetValue: Sorprendentemente, o patrón TryGetValue é igual de rápido que o if manual, grazas a optimizacións internas.
A diferenza é abismal: 925.4% máis lento usando excepcións para algo tan sinxelo como buscar unha clave nun dicionario.
Por que é tan dramaticamente custoso lanzar excepcións?
- Creación do obxecto: Instánciase a excepción en memoria.
- Captura do stack trace: A CLR percorre toda a pila de chamadas.
- Procura do handler: Búscase o primeiro
catch
compatible, desenrolando a pila. - Impacto no JIT: Rompe optimizacións como inlining.
- Branch prediction fallida: Confunde ao procesador que espera un fluxo "normal".
- Presión no GC: Máis obxectos efémeros que recolectar despois.
En resumo: lanzar excepcións é custoso como un desfile real na corte de Dinamarca. Capturalas aínda máis. É como o soliloquio de Hamlet - extenso, dramático, e consome moitos recursos escénicos. Mentres que un simple "if" ou un "TryParse" é como o directo "To be or not to be" - conciso e efectivo.
Isto non quere dicir que non debas usar nunca as excepcións ou o try/catch. Só que as uses para o que realmente son: casos excepcionais e non controlables no teu código.
O pecado orixinal: Antipatróns con excepcións
Algo cheira a podre en Dinamarca cando vemos código como este:
public int ParseIdade(string valor)
{
try
{
return int.Parse(valor);
}
catch
{
return 0; // Valor por defecto
}
}
Horror! Este antipatrón é o equivalente a usar unha espada para abrir un sobre. Que está mal aquí?
- Uso de excepcións como fluxo de control normal: As excepcións son para situacións excepcionais, non para validacións cotiás.
- Catch xenérico: Atrapa calquera excepción, non só
FormatException
. - Esconde problemas reais: Se hai un erro xenuíno, ignórao silenciosamente.
- Rendemento pésimo: Cada string inválida pagará o prezo completo de xerar e capturar unha excepción.
Entón nunca hai que usalas?
Non tan rápido, Horacio!
Como dicía antes, as excepcións existen por algo. Usalas ben mellora a claridade do código. Imaxina unha API:
User GetUserById(string id)
Se non atopa o usuario, que fai?
- Devolve null? (O fantasma de Polonio aparecerá nas túas NullReferenceExceptions)
- Un bool de saída?
- Un TryGetUser como patrón?
- Ou lanza unha excepción?
A resposta é: depende do contrato do método.
Cando usar excepcións (con gusto)
-
Erros excepcionais: Un ficheiro que non existe, un índice fóra de rango, un erro de rede... Non son parte do fluxo normal do programa. Son excepcións, con todas as letras.
-
Precondicións violadas: Se o teu método especifica claramente que
id
non pode ser null, entónArgumentNullException
é apropiado. -
Validación externa: Se o usuario mete datos inválidos, lanzar unha excepción é lexítimo.
-
APIs públicas: Claridade > rendemento. É mellor lanzar ArgumentNullException que deixar que algo explote por null.
-
Para erros que non deberían ignorarse: Erros que requiren atención inmediata.
Cando evitalas (polo teu ben)
-
En bucles de alto rendemento: Se tes un método que se chama 100.000 veces por segundo, non deberías usar excepcións como control de fluxo. Usa TryParse, TryGetValue, TryWhatever.
-
Para condicións esperadas: Un usuario non existe? Iso espérase. Devolve null ou usa un patrón como
Result<T>
. (Na segunda parte deste artigo falaremos longo e tendido del). -
En código de baixo nivel ou crítico: Networking, gráficos, procesamento masivo... aí cada milisegundo conta.
-
Para validar entrada de usuario: O usuario raramente introduce datos perfectos. Non convertas cada validación nunha excepción.
-
Para controlar fluxo do programa: As excepcións non son "GoTo's" sofisticados.
A Nobre Arte do TryParse
Lembras o noso exemplo horrible de ParseIdade? Aquí está a súa redención:
public int ParseIdade(string valor)
{
if (int.TryParse(valor, out int resultado))
return resultado;
return 0; // Valor por defecto
}
POR SE AS MOSCAS
Sei que es unha persoa intelixente e seguramente xa o saberás, pero por se acaso,
TryParse
intenta parsear o valor que lle pasas á variable de saída; e devolverá un booleano indicando se a conversión tivo éxito ou non.
Esta versión:
- É ordes de magnitude máis rápida
- Expresa claramente a intención
- Non abusa do sistema de excepcións
- Mantén limpo o stack trace
Imos ver medir os tempos destes dous exemplos nos que nun caso usamos o TryParse e no outro capturamos a excepción cando non se poida parsear:
[Benchmark]
public void ParseConExcepcion()
{
for (int i = 0; i < 1000; i++)
{
try { int.Parse("non-e-un-numero"); }
catch { /* nada */ }
}
}
[Benchmark]
public void ParseConTry()
{
for (int i = 0; i < 1000; i++)
{
int.TryParse("non-e-un-numero", out _);
}
}
E os resultados do benchmark son:
| Method | Mean | Error |
|-------------------:|-------------:|------------:|
| ParseConExcepcion | 3,612 ms | 0,07196 ms |
| ParseConTry | 0,00732 ms | 0,00014 ms |
Case 500 veces máis lento con excepcións! É como comparar a velocidade de Hamlet decidindo que facer coa do raio!
A Familia Try: Os Teus Novos Mellores Amigos
O Framework .NET está cheo de métodos Try* que te axudan a evitar excepcións:
- int.TryParse, DateTime.TryParse, etc. - Para conversións de tipos
- Dictionary.TryGetValue - Para buscar en coleccións
- File.Exists + File.Open - Para comprobar antes de abrir
- int.TryFormat, DateTime.TryFormat - Para formateo de valores
E moitas API modernas seguen este patrón. Aprende a recoñecelos e amalos.
Alternativas modernas
Hoxe en día hai solucións intermedias máis limpas que lanzar ou morrer:
public bool TryGetUser(string id, out User user)
{
user = _repository.Find(id);
return user != null;
}
Ou usando métodos que devolven tuplas:
public (bool success, User? user) GetUser(string id)
{
var user = _repository.Find(id);
return (user != null, user);
}
Tamén podes implementar os teus propios métodos Try seguindo o patrón establecido:
public bool TryDividir(int numerador, int denominador, out int resultado)
{
if (denominador == 0)
{
resultado = 0;
return false;
}
resultado = numerador / denominador;
return true;
}
Fáltanos por nomear un patrón moi interesante á hora de xestionar alternativas a lanzar excepcións: o patrón Result
. Pero isto veremolo na segunda parte do artigo xa que ten chicha.
O Monólogo da Destrución: Empty Catch Blocks
"Oh, que miserable práctica é o catch baleiro! Menos malo é morrer, que atrapar sen tratar."
O catch baleiro é probablemente o pecado máis grave no manexo de excepcións:
try
{
// Algo perigoso
}
catch { } // Horror e desolación!
Este patrón é o equivalente a varrer o po baixo a alfombra. As excepcións ocorren por algo, e ignoralas todas é convidar ao desastre. Se un código así existe nas túas aplicacións é o que adoita chamar un Code smell de libro.
Como mínimo:
try
{
// Algo perigoso
}
catch (Exception ex)
{
_logger.Error($"Operación fallida: {ex.Message}");
// Quizais relanzar algo apropiado
}
Recomendacións finais - O Soliloquio da Excepción
- Non teñas medo de lanzar excepcións, pero faino con cabeza.
- Nunca uses excepcións para control de fluxo normal.
- En código hot-path, evita lanzar, incluso evita try/catch se é posible.
- Prefire métodos tipo TryXxx cando esperas erros comúns.
- Cando captures excepcións, faino de forma específica.
- Non deixes bloques catch baleiros. Polo menos rexistra o erro.
- Usa finally para garantir a limpeza de recursos.
E se te atopas escribindo algo como:
try
{
return Parse(input);
}
catch
{
return defaultValue;
}
... probablemente haxa un TryParse esperando a ser usado, e Yorick estarase revolvendo na súa tumba.
Conclusión
Lanzar excepcións en C# non é o fin do mundo. Pero tampouco é de balde. É unha ferramenta poderosa, útil para comunicar erros reais, non para validar se unha cadea é un número.
Como nos ensinou Hamlet, a indecisión pode ser fatal. Pero en programación, decidir correctamente cando lanzar unha excepción pode ser a diferenza entre unha aplicación que brilla en escena ou unha que morre no terceiro acto.
O meu consello final? Usa excepcións como os puñais envelenados de Shakespeare: reservados para os momentos verdadeiramente dramáticos, non para cada pequena escaramuza no teu código.
Se che interesou este artigo non te perdas o segundo acto onde tentarei explicar as diferenzas entre throw;
, throw ex;
e throw new Exception();
A Arte de Relanzar Excepcións: Os Tres Destinos dun Erro