Desenvolvimento - C#
Boas Práticas de Programação
Para termos um código bem enxuto e com boa qualidade não basta apenas termos convenção de nomes, classes, variáveis, etc.
por Israel AéceVeremos no decorrrer desta seção algumas técnicas que não são muito utilizadas no dia-a-dia dos programadores pois são pequenos detalhes que influenciam na performance e na escrita de um bom código. Essas boas práticas vão desde a ter um código elegante até como melhorá-lo, mas claro, estaremos abordando isso superficialmente. Além do code-tunning falaremos um pouco sobre Exceptions.
Operadores de curto-circuito
Operadores que operam em curto-circuito é uma exclusividade de algumas linguagens de programação e, felizmente, entre elas estão o C# e o Visual Basic .NET. Esses operadores ajudam-nos a poupar verificações desnecessárias em nosso código, pois se algumas das condições falharem, a outra nem é executada.
Existem operadores AndAlso (&&) e OrElse (||) que operam em curto-circuito, os quais veremos alguns exemplos abaixo:
if(condicao1 && condicao2){ //… } if(condicao1 || condicao2){ //… } If condicao1 AndAlso condicao2 Then ... If condicao1 OrElse condicao2 Then ... |
|||
C# | VB.NET |
Como podemos analisar no código acima, os operadores de curto-circuito nos ajudam a ter uma melhor performance, já que no caso do operador AndAlso (&&), se a condição 1 falhar, a segunda condição não será avaliada. Já no caso do operador OrElse (||), se a primeira condição for verdadeira, a segunda também não será executada, pois o resultado já está formado.
Teste de ordenação lógica
Um detalhe bastante importante que muitas vezes não nos atentamos é quando utilizamos o bloco switch (Select Case em VB.NET). Neste caso, o ideal é sempre ordenarmos a lista de possibilidades da mais freqüente para a menos freqüente. Isso evitará que a avaliação seja feita em vários itens, tendo assim uma perda de performance, pois se o item freqüente está no último item a ser avaliado, ele deverá passar por todos os outros antes.
Para exemplificar faremos um teste em que vamos analisar o tempo que é levado para que o valor que está sendo procurado seja encontrado dentro da lista.
class Program { private static void TesteCase(string value) { switch (value) { case "A": ProcessarValor(value); break; case "1": ProcessarValor(value); break; case "B": ProcessarValor(value); break; case "2": ProcessarValor(value); break; default: break; } } } Module Program Public Sub TesteCase(ByVal value As String) Select Case value Case "A" ProcessarValor(value) Case "1" ProcessarValor(value) Case "B" ProcessarValor(value) Case "2" ProcessarValor(value) Case Else Exit Select End Select End Sub End Module |
|||
C# | VB.NET |
Como vemos no código acima, se estivermos procurando pelo valor "2" dentro da lista de possibilidades do método TesteCase e, como ele é o último item e o número de pesquisa por ele for relativamente grande, então teríamos aqui uma pequena perda de performance. Para vermos o resultado veremos a média de tempo em 10 consultas com o valor "2" sendo o último item, e depois sendo o primeiro item da lista:
|
Fusão de Loops
A fusão de loops é quando usam-se dois loops distintos para operar o mesmo conjunto de elementos e, em cada um deles, efetuar uma ação diferente. Na maioria das vezes utilizamos isso em coleções para alterar algum valor, ou algo do tipo. Abaixo podemos visualizar o código que está com o problema e, em seguida, o código já melhorado:
for(int i = 0; iFor i As Integer = 0 To dados.Count - 1 dados(i).Nome = String.Empty Next For i As Integer = 0 To dados.Count - 1 dados(i).Id = -1 Next |
|||
C# | VB.NET |
A melhor opção para este código é:
for(int i = 0; iFor i As Integer = 0 To dados.Count - 1 dados(i).Nome = String.Empty dados(i).Id = -1 Next |
|||
C# | VB.NET |
Minimizando o trabalho dentro de Loops
Este é um dos pontos essenciais para ganharmos em performance na aplicação. Muitas vezes colocamos operações custosas dentro de loops, o que acarretará na execução desta operação o mesmo número de vezes que o loop for executado. Na maioria dos casos, esse código custoso faz sempre a mesma coisa, ou seja, é um cálculo que independe de qualquer valor proveniente do loop. Se analisarmos o código abaixo, veremos o problema:
for(int i = 0; iFor i As Integer = 0 To dados.Count - 1 dados(i).Taxa = GeraTaxa() * 2.25 Next |
|||
C# | VB.NET |
Se executarmos o código acima, a função GeraTaxa multiplicada pelo valor 2.25 será executada o número de vezes que o loop acontecer. Como neste caso, o valor será sempre o mesmo, o ideal é você isolar o cálculo fora do loop e, conseqüentemente, ter um código mais performático:
double taxa = GeraTaxa() * 2.25; for(int i = 0; iDim taxa As Double = GeraTaxa() * 2.25 For i As Integer = 0 To dados.Count - 1 dados(i).Taxa = taxa Next |
|||
C# | VB.NET |
Um outro detalhe importante é a referência de arrays dentro de loops. Se for mal projetado você pode ter problemas de performance, já que você fará o acesso a algum índice de acordo com o número de iterações do teu loop. O exemplo de código abaixo, mostra essa deficiência:
for(int i = 0; iFor i As Integer = 0 To dados.Count - 1 For j As Integer = 0 To dados(i).Items.Count - 1 dados(i).Items(j).Valor = dados(i).Valor + 2 Next Next |
|||
C# | VB.NET |
A melhor opção para este código é mover o cálculo para fora do loop interno, já que o valor será proveniente do cálculo com um valor fornecido pelo loop principal. Sendo assim, o código fica da seguinte forma:
for(int i = 0; iFor i As Integer = 0 To dados.Count - 1 Dim valor As Double = dados(i).Valor For j As Integer = 0 To dados(i).Items.Count - 1 dados(i).Items(j).Valor = valor + 2 Next Next |
|||
C# | VB.NET |
Code Caching
Code caching significa salvar um determinado valor que é proveniente de algum cálculo mais complexo em um membro interno que, por sua vez, será exposto pela classe. Geralmente utilizamos essa técnica quando o valor é freqüentemente utilizado e, se for sempre calculado, teremos uma perda de performance, já que o cálculo seria efetuado o mesmo número de vezes que a propriedade é invocada.
Essa técnica exige que dentro da propriedade que expõe o valor "cacheado" devemos efetuar uma verificação para saber se o valor já foi ou não calculado. Para exemplificar, veremos abaixo um exemplo de como colocamos essa técnica em prática:
internal class Funcionario { private double _salario; private bool _salarioGerado; public double Salario { get { if (!this._salarioGerado) { this._salario = CalcularSalario(this.FuncionarioId); this._salarioGerado = true; } return this._salario; } } } Friend Class Funcionario Private _salario As Double Private _salarioGerado As Boolean Public ReadOnly Property Salario() As Double Get If Not Me._salarioGerado Then Me._salario = CalcularSalario(Me.FuncionarioId) Me._salarioGerado = True End If Return Me._salario End Get End Property End Class |
|||
C# | VB.NET |
Como podemos ver, a primeira vez que a propriedade é chamada, a condicional verifica se o membro _salarioGerado é atendida, ou seja, verifica se o salário já foi ou não gerado. Se ainda não tiver sido gerado, o método CalcularSalario é executado, passando para o mesmo o identificador do funcionário para efetuar o cálculo do salário. O retorno deste método é armazenado no membro privado _salario, definindo o membro _salarioGerado para True. Sendo assim, da segunda vez que a propriedade é invocada pelo consumidor da classe, o cálculo, que é a parte mais custosa do código, não será mais executado devido ao flag que estará como True.
É natural que, em algum momento, você precise reiniciar o valor, pois algum processo dentro da classe necessite que o salário seja recalculado e, para isso, você pode simplesmente voltar o valor do membro privado _salarioGerado para False.
Magic Numbers
Os magic-numbers são aqueles números que temos no código que referenciam índices de arrays, campos de colunas do banco de dados, contadores, etc.. Em alguns casos, como por exemplo, o acesso aos campos do DataReader, a utilização de números para referenciar as colunas do result-set é sempre mais performático do que passar o nome do campo mais, muitas vezes, isso dificulta a legibilidade do código, principalmente se precisar dar manutenção neste código mais tarde.
Este é um cenário típico para o uso de variáveis constantes do tipo inteiro, que devemos especificar o nome do campo da base de dados e definir o número correspondente que este campo está dentro do result-set. Abaixo podemos ver um exemplo de como isso funciona:
#region Colunas da DB const int ID = 0; const int NOME = 1; const int EMAIL = 2; #endregion Cliente c = new Cliente(); //.... c.Email = dr.GetString(EMAIL); c.ID = dr.GetInt32(ID); c.Nome = dr.GetString(NOME); #region Colunas da DB const ID As Integer = 0 const NOME As Integer = 1 const EMAIL As Integer = 2 #endregion Dim c As New Cliente() ".... c.Email = dr.GetString(EMAIL) c.ID = dr.GetInt32(ID) c.Nome = dr.GetString(NOME) |
|||
C# | VB.NET |
Isso não afetará em nada a performance da aplicação já que quando o código é compilado, o compilador se encarrega de trocar as constantes pelo valor correspondente.
Exceções
O foco desta seção não é abordar como é feito o tratamento de exceções no .NET, mas sim entender algumas das boas práticas e também algumas dicas com relação à performance.
Um ponto importante é que nem sempre uma exceção representa um erro. Exceção é uma violação de alguma suposição da interface do seu tipo. Por exemplo, ao projetar um determinado tipo, você imagina as mais diversas situações em que seu tipo será utilizado, definindo também seus campos, propriedades, métodos e eventos. Como já sabemos, a maneira como você define esses membros torna-se a interface do seu tipo.
Assim sendo, dado um método chamado TransferenciaValores, que recebe como parâmetro dois objetos do tipo Conta e um determinado valor (do tipo Double) que deve ser transferido entre elas, precisamos "validá-los" para que a transferência possa ser efetuada com êxito. O desenvolvedor da classe precisará ter conhecimento suficiente para implementar essa tal "validação" e não esquecer do mais importante: documentar claramente para que os utilizadores deste componente possam implementar o código que fará a chamada ao método da maneira mais eficiente possível, poupando ao máximo que surpresas ocorram em tempo de execução.
public static void TransferenciaValores(Conta de, Conta para, double valor) { //.... } Public Shared Sub TransferenciaValores(de As Conta, para As Conta, valor As Double) ".... End Sub |
|||
C# | VB.NET |
Dentro deste nosso cenário, vamos analisar algumas suposições ("validações") que devemos fazer para que o método acima possa ser executado da forma esperada:
- Certificar que de e para não são nulos;
- Certificar que de e para não referenciam a mesma conta;
- Se o valor for maior que zero;
- Se o valor é maior que o saldo disponível.
Necessitamos agora informar ao chamador que alguma dessas regras foi violada. Mas como fazemos isso? Atirando uma exceção. Como dissemos logo no começo desta seção, ter uma exceção nem sempre é algo negativo na aplicação, pois o tratamento de exceções permite capturar a exceção, tratá-la e a aplicação continuará correndo normalmente.
Capturando e atirando Exceções
Um erro bastante comum é utilizar blocos catch em demasia. Ao capturar uma exceção, você informa ao runtime o que espera por aquela exceção, entendendo o porque aquilo aconteceu, definindo uma diretiva para a aplicação.
try { //código que pode eventualmente //atirar uma exceção } catch(Exception) { //... } Try "código que pode eventualmente "atirar uma exceção Catch ex As Exception "... End Try |
|||
C# | VB.NET |
Como pode notar, o código acima espera que ocorra qualquer tipo de exceção para que ele o trate. Qualquer tipo que faça parte de uma biblioteca de classes, nunca deve capturar a exceção mais genérica, pois não há maneira da aplicação chamadora ser notificada de que algum erro ocorreu.
Se o código envolvido pelo código try lançar uma exceção, a mesma deverá ser lançada até o topo da pilha de chamadas e deixar o nível mais alto tratar a exceção da maneira que desejar.
Quando falamos em lançar a exceção devemos nos atentar em como realizar essa tarefa. Isso é feito através da keyword throw, onde você deve desenhar a sua estrutura sem especificar nenhuma exceção no bloco catch, ou seja, fazer todo o trabalho que necessita, ou melhor, voltar o objeto utilizado em um estado consistente e, finalmente, notificar o chamador que uma exceção aconteceu, atirando-a, sendo ela qual for.
try { obj.Validate(); } catch { obj.ResetValues(); throw; } Try obj.Validate() Catch obj.ResetValues() Throw End Try |
|||
C# | VB.NET |
Validações
Evite sempre o bloco de tratamento de exceções Try/Catch para fazer validações simples, como por exemplo cálculos, conversões, etc.. Há sempre uma alternativa mais performática e ao mesmo tempo mais elegante de fazer essas operações para evitar todo o overhead que existe em um bloco Try/Catch. Para exemplifcar isso, podemos citar alguns exemplos de códigos onde teremos primeiramente o código ruim e, em seguida, o mesmo código já reformulado:
Código ruim
try { IReader reader = (IReader)e.Data; reader.ExecuteOperation(); } catch (InvalidCastException) { Response.Write("Tipo incompatível."); } try { int id = Convert.ToInt32(Request.QueryString["Id"]); this.BindForm(id); } catch (InvalidCastException) { Response.Write("Id inválido."); } Try Dim reader As IReader = DirectCast(e.Data, IReader) reader.ExecuteOperation() Catch ex As InvalidCastException Response.Write("Tipo incompatível.") End Try Try Dim id As Integer = Convert.ToInt32(Request.QueryString("Id")) Me.BindForm(id) Catch ex As InvalidCastException Response.Write("Id inválido.") End Try |
|||
C# | VB.NET |
Código reformulado
IReader reader = (IReader)e.Data as IReader; if(reader != null) { reader.ExecuteOperation(); } else { Response.Write("Tipo incompatível."); } int id = 0; if(int.TryParse(Request.QueryString["Id"], out id)) { this.BindForm(id); } else { Response.Write("Id inválido."); } Dim reader As IReader = TryCast(e.Data, IReader) If Not IsNothing(reader) Then reader.ExecuteOperation() Else Response.Write("Tipo incompatível.") End If Dim id As Integer = 0 If Integer.TryParse(Request.QueryString("Id"), id) Me.BindForm(id) Else Response.Write("Id inválido.") End if |
|||
C# | VB.NET |
Conclusão: Boas práticas de programação são sempre bem-vindas em qualquer tipo de linguagem. Claro que as técnicas não param por aqui. Existem muitas outras técnicas e benefícios relacionados a cada uma delas que este artigo não contempla. Este artigo dá apenas uma visão de técnicas que devem ser utilizadas no desenvolvimento de aplicações baseadas na plataforma .NET para tirar um melhor proveito da linguagem, não perdendo performance.