NullableTypes - Tratando valores nulos no Delphi 2005

 

Nulo. [Do lat. nullu]Adj. 1. Que não é válido; que não tem valor 2. Sem valor ou sem efeito; inútil, vão 3. Nenhum 4. Inepto, incapaz 5. Inerte

 

Quando vamos desenvolver softwares que utilizam banco de dados normalmente encontramos um problema: a representação do valor NULL. Existem diversas técnicas para solucionar o problema.

As mais simples utilizam valores pré-definidos para representar o valor nulo, por exemplo, uma string vazia para um campo alfanumérico, e 0 ou -1 para campos numéricos e datas. Essas estratégias são fáceis de implementar, porém apresentam outro problema se você precisa utilizar toda a faixa de valores (ex: de 0 a 255 para um campo Byte) ou precisa diferenciar se um campo alfanumérico não está preenchido ou se ele foi preenchido com uma string vazia.

Já as mais elaboradas fazem uso de objetos ou estruturas que além de guardar o valor do campo, guardam também se ele é nulo ou não. Até o Delphi 7, quem precisasse desse mecanismo entrava num dilema ao ter que escolher entre utilizar classes (para encapsular certos comportamentos com o custo de utilizar o heap) ou records (simplesmente para guardar dados utilizando o stack). Apesar de no .Net termos recursos adequados para uma implementação eficaz de tipos nulos, felizmente não precisamos fazê-lo, pois podemos utilizar o projeto de código aberto (licença MIT) NullableTypes.

Conhecendo NullableTypes

Programado em C#, por um italiano conhecido por “luKa”, o NullableTypes é  um conjunto de tipos que funcionam como os tipos internos do .Net. Por serem estruturas, eles são “leves” (ficando no stack), não precisam ser instanciados e são sempre passados por valor. Essas características são as mesmas de todos os outros Value-Types do .Net (Int32, String, Boolean e etc). Além disso, essas estruturas possuem métodos que encapsulam comportamentos básicos como conversões, comparações, operações matemáticas e serialização.

Cada NullableType foi feito para representar um tipo nativo do .Net e todos implementam a interface INullable, representando o fato que todos eles podem conter um valor nulo.

Os tipos implementados pelo NullableTypes são: NullableBoolean, NullableByte, NullableDateTime, NullableDecimal, NullableDouble, NullableInt16, NullableInt32, NullableInt64, NullableSingle, NullableString.

A interface INullable contém apenas uma propriedade somente-leitura “IsNull” que indica se o valor é nulo ou não. Importante notar que ser “nulo” significa que o valor está faltando, é desconhecido ou é inaplicável. Além disso, é bom deixar claro algumas regrinhas que se aplicam aos valores nulos:

1. Um valor nulo não pode implicar em nenhum outro valor e vice-e-versa. Nulo é simplesmente nulo;

2. Qualquer comparação ou operação aritmética envolvendo pelo menos um operando nulo retornará nulo.

Instalando NullableTypes

Para instalar NullableTypes clique com o botão direito no Project Manager e escolha a opção Add Reference. Em seguida, selecione o assembly “NullableTypes.dll”. Confirme a operação clicando em Ok (Figura 1).

 

Figura 1. Instalando NullableTypes

Exemplos Práticos

Listagem 1. Exemplo de uso.

procedure TForm1.btnNullableTypes1Click(Sender: TObject);

{$REGION 'Declaracoes'}

var

  nint: NullableInt32;

  nullint: NullableInt32;

  nbool: NullableBoolean;

  i: integer;

{$ENDREGION}

begin

  {$REGION 'Inicializacoes'}

  //Aqui antes da atribuição ocorre uma conversão implícita de um Int32

  //para um NullableInt32

  nint := 320;

  nullint := NullableInt32.Null;

  i := 10;

  {$ENDREGION}

 

  {$REGION 'Testes de conversao'}

  //Uma conversão implícita acontece antes da atribuicao mas só

  //depois da soma entre a variável "i" e o 100

  nint := i + 100; //nint = 330

 

  //Antes da atribuição ocorre uma conversão implícita da variável "i"

  //que é Int32 em um NullableInt32

  nint := i; //nint = 10

 

  //A conversão de um NullableInt32 para um Int32 deve ser explícita

  //e levantará uma exceção caso a variável "nint" seja nula

  i := Int32(nint); //i = 10

  {$ENDREGION}

 

  {$REGION 'Testes de comparacao'}

  //Para fazer uma expressão como "nint > 10" deve-se utilizar

  //a funcão GreaterThan, e o 10 será implicitamente convertido para

  //um NullableInt32

  //Note que o fato dessa função retornar falso não implica que "nint <= 10"

  //pois pode ser que nint seja nulo

  if (NullableInt32.GreaterThan(nint,10).IsTrue) then

    MessageBox.Show('nint > 10')

  else

    MessageBox.Show('nint <= 10 ou é nulo');

 

  //Outro modo de codificar

  if (nint.IsNull and (nint.Value > 10)) then

    MessageBox.Show('nint > 10')

  else

    MessageBox.Show('nint <= 10 ou é nulo');

 

  //Mais um modo de codificar mas agora usando a variável "nullint"

  if (nullint.IsNull) then

    MessageBox.Show('nullint é nulo')

  else if (nullint.Value > 10) then

    MessageBox.Show('nullint > 10')

  else

    MessageBox.Show('nullint <= 10');

  {$ENDREGION}

end;

 

Para facilitar a conversão dos NullableTypes em outros tipos do .Net existem duas classes estáticas: a DBNullConvert e a NullConvert.

A classe NullConvert contém métodos para converter NullableTypes em tipos não nulos do .Net e vice-e-versa. Ela é muito útil na camada de apresentação, visto que nenhum controle do .Net suporta tipos nulos. Utilize o método From passando um NullableType e o valor que você deseja que seja retornado caso o valor seja nula; ele retornará o valor num tipo nativo do .Net. Use os métodos To<nullabletype>, onde “<nullabletype>” é o tipo para que você quer converter, passando um tipo nativo do .Net e o valor que você deseja que seja considerado como nulo; e eles retornarão um NullableType (Listagem 2).

 

Listagem 2. Exemplo de NullConvert.

procedure TesteNullConvert;

var

  dataDotNet: DateTime;

  dataNullable: NullableDateTime;

begin

  //inicializa dataNullable com um valor nulo

  dataNullable := NullableDateTime.Null;

  //dataNullable é nula

  //converte para um tipo nativo usando a data 01/01/1000 se for nulo

  dataDotNet := NullConvert.From(dataNullable,DateTime.Create(1000,1,1));

  //dataDotNet = 01/01/1000

  //converte de volta para um NullableType considerando a data 01/01/2000 como nula

 dataNullable:= NullConvert.ToNullableDateTime(dataDotNet,DateTime.Create(2000,1,1));

  //dataNullable = 01/01/1000

  //pois esse valor não foi considerado como nulo e sim como um valor válido

end;

 

Já a classe DBNullConvert contém métodos para converter entre NullableTypes e os valores vindos dos Data Providers (usados como parâmetros da classe Command e valores retornados pelo DataReader) e dos Datasets (valores da propriedades Items de um DataRow). O método From recebe um parâmetro NullableType e retorna um SqlType. Os métodos To<nullabletype>, onde “<nullabletype>” é o tipo para que você quer converter, recebem um Object (que deve ser um ValueType ou um SqlType) e eles retornarão um NullableType (Listagem 3).

 

Listagem 3. Exemplo de DBNullConvert.

procedure TesteDbNullConvert;

var

  dataNullable: NullableDateTime;

  dt: System.Data.DataTable;

  row: System.Data.DataRow;

begin

  //inicializa dataNullable com um valor nulo

  dataNullable := NullableDateTime.Create(2005,08,12);

  //dataNullable é 12/08/2005

  //cria e inicializa um DataTable

  dt := DataTable.Create;

  dt.Columns.Add('data', &Type.GetType('System.DateTime'));

  dt.Columns['data'].AllowDBNull := true;

  //cria uma nova linha para o DataTable

  row := dt.NewRow;

  row['data'] := DBNullConvert.From(dataNullable);

  //a coluna 'data' do DataRow agora vale 12/08/2005

  row['data'] := DBNull.Value;

  //a coluna 'data' do DataRow agora vale NULL

end;

NullableTypes x SQLTypes

No framework .Net também encontramos os SqlTypes, que são tipos que implementam uma interface INullable e tem praticamente os mesmo comportamento do NullableTypes. No entanto, os SqlTypes são fortemente ligados com os tipos nativos dos banco de dados e devem ser utilizados apenas na camada de acesso a dados. Para a camada de negócio e apresentação é fortemente recomendado que você utilize outros tipos que sejam independentes de banco, que é o caso dos NullableTypes. Além de não depender de nenhum banco de dados, os NullableTypes são melhores que os SqlTypes porque suportam serialização (logo funcionam com remoting e webservices) e têm classes auxiliares que ajudam na integração com controles para Web/WinForms e na conversão de e para valores de banco de dados.

Dicas Importantes

Como acontece com toda boa novidade, o uso de NullableTypes requer atenção por causa de algumas características do framework. Vamos a algumas delas:

A herança do C#

Todo programador C sabe: a divisão entre inteiros só pode retornar um inteiro. Ou seja, justamente o que não acontece quando realizamos a mesma operação no Delphi. Porém, é a antiga regra do C que prevalece (sim, em pleno Delphi !!) com operações envolvendo NullableInt32. Este comportamento acontece porque NullableTypes foi desenvolvido em C# e grande parte do código do projeto está concentrado na sobrecarga de operadores. O exemplo da Listagem 4 deixa isto bem claro.

 

Listagem 4. Diferenças na divisão com NullableTypes.

procedure TWinForm.Button1_Click(sender: System.Object; e: System.EventArgs);

var

  UmNullableInteger, OutroNullableInteger, MaisUmNullableInteger: NullableInt32;

  UmDelphiInteger, OutroDelphiInteger: Integer;

  UmDelphiDouble: Double;

  UmNullableDouble: NullableDouble;

begin

  UmDelphiInteger    := 1;

  OutroDelphiInteger := 2;

 

  // Todo programador Delphi sabe que o resultado de uma divisão entre inteiros

  // não pode ser associado a um inteiro...

  UmDelphiDouble := UmDelphiInteger / OutroDelphiInteger;

  UmNullableInteger := 1;

  OutroNullableInteger := 2;

 

  // ... mas isso não se aplica quando utilizamos NullableTypes. A divisão entre

  // inteiros sempre retorna um inteiro !

  UmNullableDouble    := UmNullableInteger / OutroNullableInteger;

  MaisUmNullableInteger := UmNullableInteger / OutroNullableInteger;

 

  MessageBox.Show(UmDelphiDouble.ToString);  // Saída: 0,5

  MessageBox.Show(MaisUmNullableInteger.ToString); // Saída: 0

  MessageBox.Show(UmNullableDouble.ToString); // Saída: 0

end;

NullableBoolean: um paradoxo ?

Como já foi demonstrado anteriormente, sempre que se tentar acessar a propriedade Value, devemos fazer uma checagem utilizando o método IsNull. Porém, seria muito trabalhoso fazer estas checagens com NullableBoolean. Seria complexo, por exemplo, testar se três condições são verdadeiras (Listagem 5).

 

Listagem 5. Uma maneira complicada de utilizar NullableBoolean.

procedure TWinForm.Button3_Click(sender: System.Object; e: System.EventArgs);

var

  condicao1, condicao2, condicao3: NullableBoolean;

begin

  if ((not condicao1.isNull) and condicao1.Value) and

     ((not condicao2.isNull) and condicao2.Value) and

     ((not condicao3.isNull) and condicao3.Value) then

    MessageBox.Show('verdadeiro')

  else

    MessageBox.Show('falso');

end;

 

É para esse tipo de situação que servem os métodos IsTrue e IsFalse. Assim podemos simplificar o código, como demonstrado na Listagem 6.

 

Listagem 6. Simplificando o uso de NullableBoolean.

procedure TWinForm.Button3_Click(sender: System.Object; e: System.EventArgs);

var

  condicao1, condicao2, condicao3: NullableBoolean;

begin

  condicao1 := True;

  condicao2 := True;

  condicao3 := False;

 

  if condicao1.IsTrue and condicao2.IsTrue and condicao3.IsTrue then

    MessageBox.Show('verdadeiro')

  else

    MessageBox.Show('falso');

end;

 

É importante lembrar que um NullableBoolean nulo não é nem verdadeiro e nem falso. Ou seja, um nulo enquanto Boolean simplesmente não pode ser avaliado e não será verdadeiro, nem falso. Será simplesmente nulo. Parece um ‘paradoxo’ quando pensamos em NullableBoolean como tendo dois estados possíveis, tal qual é o Boolean. Mas a verdade é que NullableBoolean tem três estados: True, False e Null.

 

Listagem 7. Diferenças entre NullableBoolean e Boolean.

procedure TWinForm.Button4_Click(sender: System.Object; e: System.EventArgs);

var

  condicaonula: NullableBoolean;

begin

  condicaonula := NullableBoolean.Null;

  if condicaonula.IsTrue then

    MessageBox.Show('Condição nula é verdadeira !');

  if condicaonula.IsFalse then

    MessageBox.Show('Condição nula é falsa !');

  // Para um Boolean, não ser verdadeiro é ser falso

  if (not True) = False then

    MessageBox.Show('Para um Boolean, não ser verdadeiro é ser falso !');

  // Porém, para um NullableBoolean, isso nem sempre é verdade. Paradoxo ?

  if (not condicaonula.IsTrue) = condicaonula.IsFalse then

    MessageBox.Show('Esta mensagem nunca será exibida.')

  else

  begin

    MessageBox.Show('not IsTrue é diferente de IsFalse ?! Um paradoxo ??');

    // Na verdade não existe paradoxo, NullableBoolean é que tem 3 estados.

    if condicaonula.IsNull then

    begin

      MessageBox.Show('Ok, nada de paradoxos. Esta é uma condição nula...' );

      MessageBox.Show('... e NullableBoolean tem 3 estados. :) ');

    end;

  end;

end;

 

Links

http://nullabletypes.sourceforge.net/

Site Oficial do NullableTypes

 

Marcelo Pereira Rocha (mrocha@borland.com) é Borland Delphi 7 Certified Developer, Técnico em Informática Gerencial formado pelo Colégio Cotemig  e Tecnólogo em Processamento de Dados formado pelo Centro Universitário da União de Negócios e Administração de Minas Gerais (UNA-MG). Atualmente desenvolve sistemas web em .Net voltados para a área governamental. Atua como consultor para a Borland Latin America (Professional Services Organization).

 

Vitor Ciaramella (vitorc@gmail.com) é Analista e Arquiteto .Net.  Possui ampla experiência com definição de requisitos, análise, arquitetura e desenvolvimento de sistemas em .Net (Delphi e C#). É instrutor Certificado em Borland Delphi 7, RAVE, ModelMaker e Intraweb e está cursando Análise de Sistemas pela PUC-Campinas. Atualmente trabalha como consultor para a Borland Latin America (Professional Services Organization).