Cast direto versus Cast com o operador "AS" #AQuemPossaInteressar

Categoria: Artigos
Categoria Pai: Addicted 2 Delphi!
Acessos: 1098
Imagem meramente ilustrativa

Se você caiu de paraquedas por aqui pode estar se perguntando o que é um typecast. Se você fez esta pergunta eu recomendo que você não leia este artigo, mas em todo caso um typecast nada mais é do que uma conversão entre tipos de dados compatíveis, normalmente objetos, portanto, os typecasts estão intimamente ligados a programação orientada a objetos.

Note que eu usei a palavra compatível, pois ela tem tudo a ver com typecasts e com este artigo. Bom, para realizar um typecast no Delphi existem duas formas, a forma direta, "desprotegida" e a forma protegida, usando o operador AS.

A forma direta, como o nome sugere, realiza o typecast utilizando o tipo de destino diretamente, por exemplo, TEdit(Sender) acessa a instância Sender (normalmente um TObject em manipuladores de evento) como um TEdit. Isso significa que você deve ter certeza de que Sender é um TEdit antes de usar este typecast.

A forma protegida utiliza o operador AS para realizar uma verificação de compatibilidade entre os tipos de origem e destino, por exemplo, (Sender As TEdit)

Quando usar um typecast em detrimento do outro? A resposta é tão simples quanto frustrante. Você deve usar o operador AS quando não souber ou quando não puder garantir que em 100% das vezes a instância de origem é compatível com o tipo de destino. Para deixar isso mais claro, considere o evento do tipo TNotifyEvent, o qual tem a assinatura básica procedure (Sender: TObject). Todos os eventos de clique são do tipo TNotifyEvent, portanto é comum escrever código e compartilhar entre os eventos de vários botões, por exemplo:

procedure TForm1.DoClick(Sender: TObject);
begin
  TButton(Sender).ModalResult := mrOk;
end;

O código anterior funciona bem, desde que este método seja usado como manipulador do evento OnClick de um TButton. Por outro lado como todos os eventos OnClick tem a mesma assinatura, você pode se sentir tentado a usar este manipulador no evento OnClick de um TForm[1] e é aí onde os problemas começam a acontecer. Quando este manipulador está associado ao um TButton, o argumento Sender é uma referência ao TButton que foi clicado, mas quando este manipulador está associado a um TForm, o argumento Sender é uma referência ao TForm que foi clicado. A propriedade ModalResult só existe no TButton, sendo assim, usar este código desta forma pode provocar problemas aleatórios difíceis de resolver.

Por que "problemas aleatórios difíceis de resolver"?

No experimento que fiz aqui, eu criei uma aplicação simples, com um TForm, um TButton e um TEdit e compartilhei o método DoClick entre o TForm e o TButton. Adicionalmente eu coloquei no arquivo de projeto (.dpr) a linha ReportMemoryLeaksOnShutdown := True para detectar vazamentos de memória. Ao clicar no TButton, nenhum erro ocorreu e eu fechei a aplicação normalmente, sem qualquer problema. Abri a aplicação novamente e desta vez cliquei no TForm. Nenhum erro ocorreu também, porém ao fechar a aplicação um Access Violation foi levantado e depois dele foram reportados alguns vazamentos de memória. A situação descrita ocorreu em uma aplicação supersimples de exemplo em um ambiente controlado onde eu SABIA onde o erro estava. Agora imagine a mesma situação em uma aplicação real, com dezenas de TForms, centenas de algoritmos e regras de negócio, acesso a recursos internos e externos, etc. Imagine que ao fechar esta aplicação um Access Violation foi levantado. Como você conseguiria imaginar que o problema foi causado por typecast errado em um clique qualquer do seu sistema? Isso é quase impossível de resolver! Se você acha isso nefasto, vai começar a achar mais ainda agora.

Um typecast cru "formata" um espaço de memória de acordo com o tipo usado no typecast. Imagine um texto impresso que contém uma mensagem secreta dentro dele, isto é, algumas palavras deste texto formam uma frase quando, por cima deste texto, você coloca uma máscara com espaços vazados. A mensagem secreta é a mensagem que se forma ao se ler as palavras que aparecem nos espaços vazados. Para que essa brincadeira funcione, o texto precisa ser escrito de forma especial para que as palavras da mensagem secreta fiquem em posições específicas tal que, quando a máscara for aplicada, a mensagem oculta seja corretamente exibida. O que você acha que vai acontecer se a máscara da mensagem secreta for aplicada a um texto SEM A MENSAGEM SECRETA? A mensagem obtida vai ser muito provavelmente ininteligível. É exatamente isso que acontece quando se faz um typecast direto de algo que não é compatível com o tipo do typecast. Nessa analogia, o texto é a memória (sempre uma sequência de Bytes) e a máscara com espaços vazados é o tipo especificado no typecast. Ao se utilizar um tipo compatível o resultado obtido é válido e ao se utilizar um tipo incompatível o resultado é muito provavelmente inválido!

"Muito provavelmente" significa que às vezes dá certo?

Não! O fato de um erro não acontecer, não significa que o erro não existe. Por se tratar de uma formatação de memória, um typecast errado pode provocar efeitos imprevisíveis ou não provocar nada até que seu programa mude de alguma forma, pois ao modificar seu programa a representação dele na memória muda e é possível que um erro provocado por um typecast inválido comece a acontecer "do nada", até mesmo anos depois de você ter implementado o typecast! Essa é a característica mais nefasta de um typecast inválido. Considere agora o seguinte exemplo:

procedure TForm1.FormShow(Sender: TObject);
begin
  // (Sender as TEdit).Undo;
  // TEdit(sender).Undo;
end;

No código anterior, execute o programa com cada uma das linhas descomentadas individualmente para ver o efeito que cada uma delas tem. Observe que em ambos os casos estamos chamando o método Undo. Este método existe para os descendentes de TCustomEdit (como TEdit), mas evidentemente não faz qualquer sentido em qualquer descendente de TCustomForm. Como estamos no manipulador do evento OnShow, significa que Sender é uma referência a TForm1, logo, em ambas as linhas estamos tentando executar o método Undo de TForm1 que NÃO EXISTE! Ao descomentar a primeira linha e executar o programa, você notará que o programa inicia normalmente, sem qualquer erro. Ao fechar o programa, ele também fecha normalmente sem qualquer erro ou vazamento de memória. Seu programa está certo? É CLARO QUE NÃO! Você formatou um espaço de memória de forma completamente errada e EXECUTOU parte dessa memória ao chamar o método Undo que NÃO EXISTE em TForm! É praticamente IMPOSSÍVEL saber o que foi feito e se isso vai causar algum problema GRAVE no futuro.

Agora, comente a primeira linha e descomente a segunda. Ao executar o programa ele vai TENTAR converter Sender em TEdit e não vai conseguir, levantando assim uma exceção EInvalidTypecast e lhe informando a respeito desse erro sem executar Undo (que não existe em TForm). Dessa forma você tem a chance de corrigir o problema pois ele fica fácil de achar ao se usar o depurador.

TObject e o operador AS

Em ambos os exemplos a conversão de tipo passou por um objeto genérico do tipo TObject (o argumento Sender dos manipuladores de evento). Este objeto, que é o ancestral comum de todo e qualquer outro objeto no Delphi é comumente usado em argumentos de subrotinas que aparecem na seção interface de uma unit (como os manipuladores de evento) para se evitar a declaração de units na cláusula uses que contenham tipos específicos e assim evitar problemas de recursividade entre units, que ocorrem quando uma unit contém em sua cláusula uses da seção interface uma referência a uma outra unit que também "usa" a primeira unit em sua cláusula uses da seção interface.

Por ser um objeto genérico é preciso convertê-lo para um tipo útil. Nos exemplos anteriores houve a tentativa de conversão de Sender em TButton e TEdit e isso só é possível porque todos os objetos, incluindo TButton e TEdit têm como objeto mais ancestral o TObject. Objetos filhos estendem as classes pais, o que significa que na memória, objetos filhos possuem toda a hierarquia até o objeto ancestral mais básico (TObject). Em outras palavras objetos tem seus dados mais os dados de todos os seus ancestrais. Ao examinar um objeto na memória se pode comprovar que, primeiramente vem os dados dos objetos pais, até chegar aos dados exclusivos dos objetos filhos. É justamente esta característica de trazer consigo toda uma hierarquia, que usar o objeto mais básico (TObject) é a escolha natural para situações onde se deseja permitir que um tipo de dado seja convertido, como ocorre nos manipuladores de evento usados como exemplo aqui. É também por conta disso que na maioria das vezes que se vê o operador AS para conversão de objetos, um dos objetos sempre é um TObject[2].

Lembre-se

O typecast direto é bruto. Ele faz a conversão sem verificar nada e pode levar a comportamentos imprevisíveis porque este tipo de conversão pode expor partes do objeto original que não deveriam aparecer. Fazer um typecast é formatar um espaço de memória de outro modo e evidentemente que após esta formatação você precisa obter um resultado válido e não dados incoerentes. Tudo na memoria é uma sequencia de Bytes. Um objeto formata estes Bytes de forma que eles façam algum sentido. Quando se faz um typecast, estamos formatando uma parte da memoria de outro modo.

Se você precisa converter um objeto genérico como TObject em algum outro tipo, certifique-se de usar o tipo correto no typecast ou use o operador AS para ser avisado sempre que uma conversão de tipo for inválida.


1 Como um programador minimamente decente é evidente que você não vai querer fazer esse tipo de coisa, contudo, errar é humano, e você pode certamente atribuir o método DoClick ao evento OnClick de um TForm sem querer
2 O operador AS também serve para converter interfaces em objetos que as implementam ou outras interfaces, mas isso é outro assunto.