NullableTypes - Tratando valores nulos no Delphi 2005
VITOR CIARAMELLA E MARCELO PEREIRA ROCHA
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.
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
, onde “” é 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, onde “” é 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