Eu vou responder do ponto de vista do C ++. Tenho certeza de que todos os conceitos principais são transferíveis para c #.
Parece que seu estilo preferido é "sempre lance exceções":
int CalculateArea(int x, int y) {
if (x < 0 || y < 0) {
throw Exception("negative side lengths");
}
return x * y;
}
Isso pode ser um problema para o código C ++ porque o tratamento de exceções é pesado - faz com que o caso de falha seja executado lentamente e aloca memória (que às vezes nem sequer está disponível) e geralmente torna as coisas menos previsíveis. O peso pesado de EH é uma das razões pelas quais você ouve pessoas dizendo coisas como "Não use exceções para controlar o fluxo".
Portanto, algumas bibliotecas (como <filesystem>
) usam o que o C ++ chama de "API dupla" ou o que o C # chama de Try-Parse
padrão (obrigado Peter pela dica!)
int CalculateArea(int x, int y) {
if (x < 0 || y < 0) {
throw Exception("negative side lengths");
}
return x * y;
}
bool TryCalculateArea(int x, int y, int& result) {
if (x < 0 || y < 0) {
return false;
}
result = x * y;
return true;
}
int a1 = CalculateArea(x, y);
int a2;
if (TryCalculateArea(x, y, a2)) {
// use a2
}
Você pode ver o problema com as "APIs duplas" imediatamente: muita duplicação de código, nenhuma orientação para os usuários sobre qual API é a "correta" a ser usada, e o usuário deve fazer uma escolha difícil entre mensagens de erro úteis ( CalculateArea
) e speed ( TryCalculateArea
) porque a versão mais rápida pega nossa "negative side lengths"
exceção útil e a torna inútil false
- "algo deu errado, não me pergunte o que ou onde". (Alguns dupla APIs usar um tipo de erro mais expressivo, como int errno
ou C ++ 's std::error_code
, mas que ainda não lhe diz onde o erro ocorreu - só que ele fez ocorrer em algum lugar.)
Se você não consegue decidir como o seu código deve se comportar, sempre pode levar a decisão até o chamador!
template<class F>
int CalculateArea(int x, int y, F errorCallback) {
if (x < 0 || y < 0) {
return errorCallback(x, y, "negative side lengths");
}
return x * y;
}
int a1 = CalculateArea(x, y, [](auto...) { return 0; });
int a2 = CalculateArea(x, y, [](int, int, auto msg) { throw Exception(msg); });
int a3 = CalculateArea(x, y, [](int, int, auto) { return x * y; });
Isso é essencialmente o que seu colega de trabalho está fazendo; exceto que ele está fatorando o "manipulador de erros" em uma variável global:
std::function<int(const char *)> g_errorCallback;
int CalculateArea(int x, int y) {
if (x < 0 || y < 0) {
return g_errorCallback("negative side lengths");
}
return x * y;
}
g_errorCallback = [](auto) { return 0; };
int a1 = CalculateArea(x, y);
g_errorCallback = [](const char *msg) { throw Exception(msg); };
int a2 = CalculateArea(x, y);
Mover parâmetros importantes de parâmetros explícitos de função para o estado global é quase sempre uma má ideia. Eu não recomendo. (O fato de não ser um estado global no seu caso, mas simplesmente um estado membro em toda a instância atenua um pouco a maldade, mas não muito.)
Além disso, seu colega de trabalho está desnecessariamente limitando o número de possíveis comportamentos de manipulação de erros. Em vez de permitir qualquer lambda de manipulação de erros, ele decidiu apenas dois:
bool g_errorViaException;
int CalculateArea(int x, int y) {
if (x < 0 || y < 0) {
return g_errorViaException ? throw Exception("negative side lengths") : 0;
}
return x * y;
}
g_errorViaException = false;
int a1 = CalculateArea(x, y);
g_errorViaException = true;
int a2 = CalculateArea(x, y);
Este é provavelmente o "ponto azedo" de qualquer uma dessas estratégias possíveis. Você tirou toda a flexibilidade do usuário final, forçando-o a usar um de seus exatamente dois retornos de chamada de tratamento de erros; e você tem todos os problemas do estado global compartilhado; e você ainda está pagando por esse ramo condicional em qualquer lugar.
Finalmente, uma solução comum em C ++ (ou qualquer linguagem com compilação condicional) seria forçar o usuário a tomar a decisão de todo o programa, globalmente, em tempo de compilação, para que o caminho de código não utilizado possa ser totalmente otimizado:
int CalculateArea(int x, int y) {
if (x < 0 || y < 0) {
#ifdef NEXCEPTIONS
return 0;
#else
throw Exception("negative side lengths");
#endif
}
return x * y;
}
// Now these two function calls *must* have the same behavior,
// which is a nice property for a program to have.
// Improves understandability.
//
int a1 = CalculateArea(x, y);
int a2 = CalculateArea(x, y);
Um exemplo de algo que funciona dessa maneira é a assert
macro em C e C ++, que condiciona seu comportamento na macro do pré-processador NDEBUG
.