Escolar Documentos
Profissional Documentos
Cultura Documentos
Primeiro temos que herdar nossa classe da classe TComponent em vez da classe TOb-ject padrão . Aqui está o
código:
tipo
TDate = classe(TComponent)
...
público
construtor Criar (AOwner: TComponent); sobrecarga; sobrepor;
construtor Criar (Y, M, D: Inteiro); reintroduzir; sobrecarga;
Como você pode ver, a segunda etapa foi adicionar um novo construtor à classe, substituindo o construtor padrão
dos componentes para fornecer um valor inicial adequado. Por haver uma versão sobrecarregada, também
precisamos usar a diretiva reintroduce para ela, para evitar uma mensagem de aviso do compilador. O código do
novo construtor simplesmente define a data para a data de hoje, após chamar o construtor da classe base:
Feito isso, precisamos adicionar à unidade que define nossa classe (a unidade Dates do exemplo DateComp ) um
procedimento Register . (Certifique-se de que esse identificador comece com R maiúsculo, caso contrário ele não
será reconhecido.) Isso é necessário para adicionar o componente ao IDE.
Simplesmente declare o procedimento, que não requer parâmetros, na parte de interface da unidade, e então escreva
este código na seção de implementação :
Cadastro de procedimento;
começar
RegistrarComponentes(end; 'Amostra' , [TDate]);
Este código adiciona o novo componente à página Sample da paleta Tools, criando a página, se necessário.
A última etapa é instalar o componente. Para isso precisamos criar um pacote, que é um tipo especial de componentes
de hospedagem de projetos de aplicativos. Tudo o que tem a fazer é:
· Selecione o Arquivo | Novo | Outro menu do IDE, abrindo a caixa de diálogo Novos Itens
· Selecione “Pacote”
· Salve o pacote com um nome (possivelmente na mesma pasta da unidade com o código real do componente)
· No projeto de pacote recém-criado, no painel Gerenciador de Projetos, clique com o botão direito no
Contém o nó para adicionar uma nova unidade ao projeto e selecionar a unidade com o TDate
classe de componente
· No Gerenciador de Projetos você pode clicar com o botão direito no nó do pacote e emitir um comando
Build e, após uma compilação bem-sucedida, selecionar o item de menu Instalar, para instalar o
componente no ambiente de desenvolvimento
· Se você começar com o código que acompanha o livro, tudo o que você precisa fazer é o último passo
da sequência acima: Abra o projeto DatePackage da pasta DateComponent e compile e instale-o
Se você criar agora um novo projeto e passar para a paleta de ferramentas, deverá ver o novo componente
em Amostra. Basta começar a digitar seu nome para procurá-lo. Isso será mostrado usando o ícone
padrão dos componentes. Neste ponto você pode colocar o componente em um formulário e começar a
manipular suas propriedades no Object Inspector, como você pode
veja na Figura 10.2. Você também pode manipular o evento OnChange de uma maneira muito mais fácil
do que no último exemplo.
Figura 10.2: As
propriedades do nosso
novo componente TDate no
Object Inspector
Além de tentar construir seu próprio aplicativo de exemplo usando este componente (algo que eu realmente
sugiro que você faça), agora você pode abrir o exemplo DateComponent , que é uma versão atualizada
do componente que construímos passo a passo nas últimas seções. deste capítulo. Esta é basicamente uma
versão simplificada do exemplo DateEvent , porque agora o manipulador de eventos está diretamente
disponível no Object Inspector.
note Se você abrir o exemplo DateCompTest antes de compilar e instalar o componente específico
pacote (o exemplo DatePackage ), o IDE não reconhecerá o componente ao abrir o formulário e exibirá uma mensagem
de erro. Você não será capaz de compilar o programa ou fazê-lo funcionar corretamente até instalar o novo componente.
Observe que a mensagem de erro exibida pelo IDE neste caso oferece duas opções: Cancelar apenas ignora o
componente indefinido, enquanto Ignorar irá removê-lo do código.
No Capítulo 3 vimos como você pode usar um loop for-in como alternativa ao loop for clássico . Nessa
seção, descrevi como você pode usar loops for-in para arrays, strings, conjuntos e alguns outros tipos
de dados do sistema. É possível aplicar tal loop a qualquer classe, desde que defina suporte de
enumeração. Embora o exemplo mais óbvio sejam as classes que contêm listas de elementos, do
ponto de vista técnico esse recurso é bastante
aberto.
Para suportar a enumeração dos elementos de uma classe em Object Pascal você deve
adicionar um método chamado GetEnumerator que retorna uma classe (a classe enumeradora
real) e definir esta classe enumeradora com um método MoveNext e uma propriedade Current – a primeira
para navegar entre os elementos e o segundo para retornar um elemento real. Feito isso (e mostrarei
como em um exemplo real em um segundo), o compilador pode resolver um loop for-in no qual o destino
é nossa classe e os elementos individuais devem ser do mesmo tipo da propriedade Current do
recenseador.
Embora não seja estritamente necessário, parece uma boa ideia implementar a classe de suporte do
enumerador como um tipo aninhado (um recurso da linguagem abordado no Capítulo 7) porque
realmente não faz sentido usar o tipo específico usado para a enumeração por si só.
tipo
TNumbersRange = classe
público
tipo
TNumbersRangeEnum = classe
privado
NPos: Inteiro;
FRange: TNumbersRange;
público
construtor Criar (ARange: TNumbersRange);
função MoveNext: Booleano;
função GetCurrent: Inteiro;
propriedade Atual: leitura inteira GetCurrent;
fim;
privado
FNStart: Inteiro;
FNEnd: Inteiro;
público
função GetEnumerator: TNumbersRangeEnum;
procedimento Set_NEnd (valor const: inteiro);
O método GetEnumerator cria um objeto do tipo aninhado que armazena informações de status para
iteração dos dados.
Observe como o construtor do enumerador mantém uma referência ao objeto real que está
enumerando (objeto que é passado como parâmetro usando Self) e define a posição inicial bem no
início:
construtor TNumbersRange.TNumbersRangeEnum.
Criar(ARange: TNumbersRange);
começar
herdado Criar;
FRange := ARange;
NPos := FRange.NStart - 1;
fim;
note Por que o construtor define o valor inicial como o primeiro valor menos 1, em vez do primeiro valor, como esperado? Acontece
que o código gerado pelo compilador para o loop for in corresponde à criação da enumeração e à execução do código
enquanto MoveNext usa Current. O teste é realizado antes de obter o primeiro valor, pois a lista pode não ter valor. Isso
implica que MoveNext é chamado antes do primeiro elemento ser usado. Em vez de implementar isso com uma lógica
mais complexa, simplesmente defini o valor inicial como um antes do primeiro , de modo que na primeira vez que MoveNext
for chamado, o enumerador será posicionado no primeiro valor.
Como você pode ver no código acima, o método MoveNext serve a dois propósitos diferentes;
passando para o seguinte elemento da lista e verificando se o enumerador atingiu
o final, caso em que o método retorna False.
Depois de todo esse trabalho, agora você pode usar o loop for-in para iterar pelos valores do objeto
range:
era
ARange: TNumbersRange;
Eu: Inteiro;
começar
ARange := TNumbersRange.Create;
ARange.NStart := 10;
ARange.NEEnd := 23;
note Há muitos casos em que as bibliotecas RTL e VCL definem enumeradores, por exemplo, cada
TComponent pode enumerar o componente que possui. O que falta é a enumeração dos controles filho. Em
Capítulo 12, na seção “Adicionando uma enumeração com um auxiliar de classe”, veremos como você pode criar
um. A razão pela qual o exemplo não está aqui é porque primeiro precisamos discutir os ajudantes de classe.
listei algumas dicas e sugestões sobre como unir as duas abordagens. Outra maneira de descrevê-lo é
considerá-lo uma seção sobre “OOP além do RAD”.
nota O material nesta seção final do capítulo foi publicado originalmente na edição 17 do “The Delphi
Magazine” (julho de 1999) com o título “20 regras para OOP em Delphi”. Agora cortei algumas regras
e reformulei outras, mas a essência permanece.
O que é confuso é que o IDE cria uma variável global padrão e (dependendo das suas configurações) também
pode criar um objeto de formulário na inicialização para cada classe de formulário que você definir em seu
projeto. Isto é certamente útil para os recém-chegados, mas geralmente é um mau hábito para qualquer
aplicação não trivial.
Claro, é muito importante dar um nome significativo a cada formulário (e sua classe) e a cada unidade.
Infelizmente, os dois nomes devem ser diferentes, mas você pode usar uma convenção para mapear os
dois de maneira consistente (como AboutForm e About.pas).
À medida que você avança nas etapas a seguir, verá os efeitos práticos desse conceito de “um formulário é
uma classe”.
Isso torna o código mais legível, especialmente quando você chama o manipulador de eventos de outro
método da classe, e ajuda os desenvolvedores a anexar o mesmo método a vários eventos ou a diferentes
componentes; embora eu deva dizer que usar Actions é a escolha preferida para anexar um único evento
a vários elementos da interface do usuário em qualquer programa não trivial.
note Actions e o componente ActionList são recursos arquitetônicos muito interessantes das bibliotecas VCL e Fire-
Monkey UI, oferecendo uma separação conceitual das operações do usuário (e seu status) dos controles da
interface do usuário aos quais estão associados. Ativando o controle, executa a ação. Na verdade, se você
desabilitar logicamente a ação, os elementos da UI associados a essa ação também serão desabilitados.
Este tópico está além do escopo deste livro, mas vale a pena investigar se você usa as bibliotecas VCL ou FMX.
público
construtor Criar(const AText: string); reintroduzir; sobrecarga;
insetos.
Se você precisar de armazenamento extra de dados para um formulário, por exemplo, adicione alguns campos privados
a ele. Neste caso, cada instância do formulário terá sua própria cópia dos dados.
Você pode usar variáveis de unidade (declaradas na parte de implementação da unidade) para dados compartilhados
entre múltiplas instâncias da classe de formulário, mas é muito melhor usar dados de classe (explicados no Capítulo
12).
deveria se livrar de qualquer maneira. Alternativamente, você pode excluir a linha correspondente usada para
criar o formulário a partir da origem do projeto.
Escusado será dizer que, se o formulário não for criado automaticamente, você precisará de algum código em
seu aplicativo para criá-lo e, possivelmente, de alguma outra variável para se referir a ele.
Acho que remover o objeto de formulário global é muito útil para os novatos em Object Pascal, que não
ficarão mais confusos entre a classe e o objeto global. Na verdade, após a remoção do objeto global, qualquer
referência a ele resultará em erro.
Como já discuti na seção “Adicionando propriedades a formulários”, neste capítulo, quando você precisar de
dados para um formulário, adicione um campo privado. Se você precisar acessar esses dados de outras
classes, adicione propriedades ao formulário. Com esta abordagem você poderá alterar o código do
formulário e seus dados (incluindo sua interface de usuário) sem precisar
alterar o código de outros formulários ou classes. Você também deve usar propriedades ou métodos para
inicializar um formulário secundário ou caixa de diálogo e ler seu estado final. A inicialização também pode
ser realizada através de um construtor, conforme já descrevi.
Quando você precisar acessar o status de outro formulário, não deverá consultar diretamente seus componentes.
Isso vincularia o código de outros formulários ou classes à interface do usuário, que é uma das partes de
uma aplicação sujeita à maioria das alterações, mas também evitaria o encapsulamento da lógica que envolve
o gerenciamento desses componentes. Em vez disso, declare uma propriedade de formulário mapeada
para a propriedade do componente: isso é feito com um método Get que lê o status do componente e
um método Set que o grava. Suponha que você agora altere a interface do usuário, substituindo o
componente por outro. Tudo o que você precisa fazer é corrigir os métodos Get e Set relacionados à
propriedade – você não precisará verificar e modificar o código-fonte de todos os formulários e classes que
possam se referir a esse componente.
Se precisar lidar com uma série de valores em um formulário, você pode declarar uma propriedade de array.
Caso esta seja uma informação importante para o formulário, você também pode tornar a propriedade
array padrão do formulário, para que possa acessar diretamente seu valor escrevendo Self[3], ou, de outro
formulário, FDataForm[3]. Isso já foi abordado para um caso mais genérico do que formulários na seção
“Usando propriedades indexadas”.
Outro exemplo relacionado é usar getters de propriedade para implementar a criação atrasada.
Em vez de criar um subobjeto no construtor da classe, você pode criá-lo na primeira vez que for
necessário, para códigos como este:
privado
FBitmap: TBitmap;
público
propriedade Bitmap: TBitmap lê GetBitmap;
Se o IDE colocar todos os componentes na seção publicada, isso se deve à forma como
esses campos estão vinculados aos componentes criados a partir do arquivo de streaming (DFM ou
FMX). Quando você define o nome de um componente, a VCL anexa automaticamente o objeto do
componente à sua referência no formulário. Isso só é possível se a referência for publicada, pois
o sistema de streaming utiliza o RTTI tradicional e alguns TObject
métodos para realizar a operação.
Então o que acontece é que ao mover as referências dos componentes do publicado para o
seção privada você perde esse comportamento automático. Para corrigir o problema, basta torná-lo
manual, adicionando o seguinte código para cada componente no manipulador de eventos OnCreate do
formulário:
A segunda operação que você deve fazer é registrar as classes dos componentes no sistema, então
que suas informações de RTTI sejam incluídas no programa compilado e disponibilizadas para
o sistema. Isto é necessário apenas uma vez para cada classe de componente e somente se você mover
todas as referências de componentes deste tipo para a seção privada. Você pode adicionar esta
chamada mesmo se não tiver certeza se ela é necessária para seu aplicativo, pois uma chamada extra
ao método Reg-isterClasses para uma classe já registrada não tem efeito. A chamada
RegisterClasses geralmente é adicionada à seção de inicialização da unidade que hospeda o formulário:
RegisterClasses([TEdit]);
Como obter o assistente? Você pode encontrá-lo como parte dos “Cantools Wizards” em:
https://github.com/marcocantu/cantools
note Existem alguns livros que cobrem arquitetura de aplicações com Delphi, mas nenhum melhor do que a série
de volumes escritos por Nick Hodges, incluindo “Coding in Delphi”, “More Coding in Delphi” e
“Dependency Injection in Delphi”. Você pode encontrar mais informações sobre esses livros em
http://www.codingindelphi.com/
11: interfaces
Ao contrário do que acontece em C++ e em algumas outras linguagens, o modelo de herança Object Pascal
não suporta herança múltipla. Isso significa que cada classe pode ter apenas uma única classe base.
A utilidade da herança múltipla é um tema de debate entre os especialistas em OOP. A ausência dessa
construção no Object Pascal pode ser considerada uma desvantagem porque você não tem o poder do C+
+, mas também uma vantagem porque você obtém uma linguagem mais simples e evita alguns dos
problemas que a herança múltipla introduz. Uma maneira de compensar a falta de herança múltipla em
Object Pascal é pelo uso de interfaces, que permitem definir uma classe que implementa múltiplas
abstrações ao mesmo tempo.
note A maioria das linguagens de programação orientadas a objetos atuais não suporta herança múltipla, mas usa interfaces,
incluindo Java e C#. As interfaces fornecem a flexibilidade e o poder de declarar suporte para múltiplas interfaces
implementadas em uma classe, evitando ao mesmo tempo os problemas de herança de múltiplas
implementações. O verdadeiro suporte à herança múltipla permanece principalmente confinado à linguagem C++.
Algumas linguagens dinâmicas orientadas a objetos suportam mix-ins, uma maneira diferente e mais simples
de conseguir algo semelhante à herança múltipla.
Em vez de abrir um debate, assumirei simplesmente que é útil tratar um único objeto a partir de múltiplas
“perspectivas”. Mas antes de construir um exemplo para explicar esse princípio, temos que apresentar o
papel das interfaces no Object Pascal e descobrir como elas funcionam.
De um ponto de vista mais geral, as interfaces suportam um modelo de programação orientada a objetos
ligeiramente diferente das classes. Objetos que implementam interfaces estão sujeitos a
polimorfismo para cada uma das interfaces que eles suportam, já que você pode invocar qualquer um dos
métodos de interface que o objeto implementa. Comparadas às classes, as interfaces favorecem o encapsulamento
e fornecem uma conexão mais flexível entre as classes do que a herança (já que nenhuma implementação
é herdada).
note As técnicas abordadas neste capítulo e o suporte geral para interfaces foram originalmente adicionados à linguagem Object
Pascal como uma forma de oferecer suporte e implementar o Windows COM (Component Object
Modelo) arquitetura. Mais tarde, o recurso foi estendido para ser totalmente utilizável fora desse cenário, mas alguns
dos elementos COM, como identidade de interface por meio de um ID e suporte de contagem de referência, ainda
permanecem na implementação atual de interfaces do Object Pascal, tornando-os ligeiramente diferentes da maioria
dos outros. línguas.
Usando interfaces
Além de declarar classes abstratas (classes com métodos abstratos), em Object Pascal você também pode escrever
uma classe puramente abstrata; isto é, um tipo de classe com apenas métodos abstratos virtuais. Isso é conseguido
usando a palavra-chave interface para definir um conjunto de tipos de dados que chamamos de interfaces.
Tecnicamente, uma interface não é uma classe, embora possa se assemelhar a uma. Embora uma classe possa ter
uma instância, uma interface não pode. Uma interface pode ser implementada por um ou mais
classes, de modo que as instâncias dessas classes acabem apoiando ou implementando a interface.
· Variáveis de tipo de interface são contadas por referência, diferentemente de variáveis de tipo de classe, oferecendo
uma forma de gerenciamento automático de memória
· Uma classe pode herdar apenas uma classe base, mas pode implementar múltiplas interfaces
· Assim como todas as classes herdam de TObject, todas as interfaces descendem de IInterface,
formando uma hierarquia ortogonal separada
· Por convenção, os nomes das interfaces começam com a letra I, em vez da letra T usada por
a maioria dos outros tipos de dados
note Observação: Originalmente, o tipo de interface base no Object Pascal era chamado de IUnknown, pois é isso que o COM exige.
A interface IUnknown foi posteriormente renomeada como Iinterface para sublinhar o fato de que você pode usar a
interface no Object Pascal mesmo fora do domínio COM e em sistemas operacionais onde o COM não existe.
De qualquer forma, o comportamento real do IInterface ainda é idêntico ao anterior do IUnknown.
tipo
ICanFly = interface
função Voar: string;
fim;
Dado que cada interface herda direta ou indiretamente do tipo de interface base, isso corresponde à
escrita:
tipo
ICanFly = interface(IInterface)
função Voar: string;
fim;
Daqui a pouco mostrarei qual é a implicação de herdar de IInterface e o que isso traz para a mesa. Por
enquanto, perceba que o IInterface possui alguns métodos básicos (novamente, semelhantes ao TObject).
tipo
ICanFly = interface
'{D7233EF2-B2DA-444A-9B49-09657417ADB7}' ]
[função Fly: string;
fim;
note Embora você possa compilar e usar uma interface sem especificar um GUID, geralmente você desejará gerar o
GUID porque ele é necessário para executar consultas de interface ou dinâmicas como typecasts usando
esse tipo de interface. O objetivo das interfaces é aproveitar as vantagens do tipo bastante estendido de maneira
flexível em tempo de execução e isso depende de as interfaces terem um GUID.
Implementando a Interface
Qualquer classe pode implementar uma ou mais interfaces listando-as após a classe base da qual herda
e fornecendo uma implementação para cada um dos métodos de cada uma das interfaces:
tipo
Avião = classe(..., ICanFly)
função Voar: string;
fim;
Quando uma classe implementa uma interface ela deve fornecer uma implementação de todos os métodos
da interface com a mesma assinatura. Neste caso a classe TAirplane deve implementar o método
Fly como uma função retornando uma string. Dado que a interface também herda de uma interface
base (IInterface), uma classe que implementa uma interface deve invariavelmente fornecer todos os
métodos da interface e de todas as suas interfaces base.
Por isso é bastante comum implementar interfaces em classes que herdam de uma classe base que já
implementa os métodos da interface base IInterface . A biblioteca de tempo de execução Object Pascal já
fornece algumas classes base para implementar o comportamento básico. A mais simples é a classe
TInterfacedObject , então o código acima poderia se tornar:
tipo
TAirplane = classe(TInterfacedObject, ICanFly)
função Voar: string;
fim;
note Ao implementar uma interface, você pode usar um método estático ou virtual. Se você planeja substituir os métodos em uma
classe herdada, faz sentido usar métodos virtuais. Existe uma abordagem alternativa, porém, que é especificar que a
classe base também herda da mesma interface e então substitui o método dessa interface. Costumo preferir declarar o
método que implementa métodos de interface como métodos virtuais quando necessário.
Agora que definimos uma interface e uma classe que a implementa, podemos criar um objeto desta
classe. Podemos tratar isso como uma aula normal, escrevendo:
era
Avião1: Avião;
começar
Avião1 := TAirplane.Create;
tentar
Avião1.Fly;
finalmente
Avião1.Grátis;
fim;
fim;
Neste caso estamos ignorando o fato de que a classe implementa uma interface. A diferença é que agora
também podemos declarar uma variável do tipo interface. Usar uma variável de tipo de interface ativa
automaticamente o modelo de memória de referência, para que possamos pular o bloco try-finally :
era
Folheto1: ICanFly;
começar
Flyer1 := TAirplane.Create;
Folheto1.Fly;
fim;
Existem algumas considerações relevantes sobre a primeira linha deste trecho de código aparentemente
simples, também parte do exemplo Intf101 . Primeiro, assim que você atribui um objeto a uma variável de
interface, o tempo de execução verifica automaticamente se o objeto implementa essa interface,
usando uma versão especial do operador as . Você pode expressar explicitamente esta operação
escrevendo a mesma linha de código da seguinte maneira:
Segundo, quer usemos a atribuição direta ou a instrução as , o tempo de execução faz uma coisa
extra: chama o método _AddRef do objeto, aumentando sua contagem de referências. Isso é
feito chamando o método que nosso objeto herda da classe base TIn-terfacedObject.
Ao mesmo tempo, assim que a variável Flyer1 sai do escopo (ou seja, ao executar a instrução final ), o
tempo de execução do Delphi chama o método _Release , que diminui a contagem de referências,
verifica se a contagem de referências é zero, e, neste caso, destrói o objeto. É por isso que no código
acima não há necessidade de liberar manualmente o objeto que criamos no código e não há necessidade
de escrever um bloco try-finally .
note Embora o código-fonte acima não tenha nenhum bloco try-finally , o compilador adicionará automaticamente ao
método um bloco try-finally implícito com a chamada implícita para _Release. Isso ocorre em muitos casos
no Object Pascal: basicamente toda vez que um método tem um ou mais tipos gerenciados (como
strings, interface ou arrays dinâmicos), há um bloco try-finally automático e implícito adicionado pelo
compilador.
inseguro, conforme explicado posteriormente). Vimos também que eles podem ser automaticamente
desalocados quando nenhuma variável de interface se refere mais a eles.
O que é importante reconhecer é que, embora haja alguma mágica do compilador envolvida (as chamadas
_AddRef e _Release ocultas ), o mecanismo real de contagem de referências está sujeito a uma
implementação específica fornecida pelo desenvolvedor ou pela biblioteca de tempo de execução.
No último exemplo, a contagem de referências está realmente em jogo, por causa do código nos
métodos da classe TInterfacedObject (aqui listado em uma versão ligeiramente simplificada):
Agora considere uma classe base diferente que implementa IInterface e que também é encontrada no
RTL, TNoRefCountObject. Esta classe basicamente desativa o mecanismo real de contagem de
referência:
note A nova classe TNoRefCountObject , que define objetos que ignoram o mecanismo de contagem de
referência, foi introduzida no Delphi 11 para substituir o nome estranho TSingletonImplementation
classe (definida na unidade Generics.Defaults ). A antiga classe TSingletonImplementation ainda existe como
um alias da classe TNoRefCountObject mais recente . O código das duas classes é basicamente idêntico.
O principal motivo da mudança foi que o nome da classe original era um nome impróprio, visto que não
tinha nada a ver com o padrão singleton. Veremos um exemplo desse padrão no próximo capítulo.
Embora TNoRefCountObject não seja comumente usado, existe outra classe que implementa
interfaces e desabilita o mecanismo de contagem de referência, apenas porque tem seu
próprio modelo de gerenciamento de memória, e essa é a classe TComponent .
Se você deseja ter um componente personalizado que implemente uma interface, não precisa se
preocupar com contagem de referências e gerenciamento de memória. Veremos um exemplo de
Aqui está um exemplo entre muitos casos semelhantes possíveis. Suponha que você tenha uma interface,
uma classe que o implementa e um procedimento global tomando a interface como parâmetro:
tipo
IMyInterface = interface
'{F7BEADFD-ED10-4048-BB0C-5B232CF3F272}' ]
[Mostrar procedimento;
fim;
O código parece bastante trivial e está 100% correto. O que pode estar errado é a forma como você
chama o procedimento (esse código faz parte do exemplo IntfError ):
O que acontece neste código é que estou passando um objeto simples para uma função que espera
uma interface. Dado que o objeto suporta a interface, o compilador não tem problemas em fazer a
chamada. O problema está na forma como a memória é gerenciada.
Inicialmente, o objeto possui contagem de referência zero, pois não há interface referente a ele.
Ao entrar no procedimento ShowThat , a contagem de referência aumenta para 1. Tudo bem e a chamada
é realizada. Agora, ao sair do procedimento, a contagem de referência diminui e vai para zero,
então o objeto é destruído. Em outras palavras, o AnObj será destruído quando você o passar para um
procedimento, o que é realmente bastante estranho. Se você executar este código, ele falhará com um
erro de memória.
Poderia haver múltiplas soluções. Você poderia aumentar artificialmente a contagem de referência e usar
outros truques de baixo nível. Mas a solução real é não misturar interfaces e referências de objetos e
usar apenas interfaces para se referir a um objeto (este código é usado novamente
do exemplo IntfError ):
Neste caso específico a solução está próxima, mas em muitas outras circunstâncias é muito difícil
encontrar o código correto. Novamente, a regra geral é evitar misturar referências de tipos diferentes.
Mas continue lendo a próxima seção para algumas abordagens alternativas recentes.
A partir do Delphi 10.1 Berlin, a linguagem Object Pascal oferece algumas melhorias no gerenciamento de
referências de interface. Na verdade, a linguagem oferece diferentes tipos de referências:
· Referências fracas (marcadas com o modificador [weak] ) não aumentam a contagem de referências
do objeto ao qual se referem. Essas referências são totalmente gerenciadas, portanto, são
automaticamente definidas como nulas se o objeto referenciado for destruído.
· Referências inseguras (marcadas com o modificador [inseguro] ) não aumentam a contagem de
referências do objeto ao qual se referem e não são gerenciadas – algo não muito diferente de um
ponteiro básico.
nota O uso de referências fracas e inseguras foi originalmente introduzido como parte do suporte ao gerenciamento de
memória ARC para plataformas móveis. Como o ARC está descontinuado, esse recurso permanece disponível apenas para
referências de interface.
Nos cenários comuns em que a contagem de referências está ativa, você pode ter um código como o
seguinte, que depende da contagem de referências para descartar o objeto temporário:
E se o objeto tiver uma implementação de contagem de referência padrão e você quiser criar uma
referência de interface que seja mantida fora da contagem total de referências? Agora você pode
conseguir isso adicionando o atributo [unsafe] à declaração da variável de interface, alterando o
código acima para:
Não que isso seja uma boa ideia, pois o código acima causaria vazamento de memória. Ao desabilitar
a contagem de referência, quando a variável sai do escopo nada acontece. Há
alguns cenários em que isso é benéfico, pois você ainda pode usar interfaces e não acionar a referência
extra. Em outras palavras, uma referência insegura é tratada como um ponteiro, sem suporte extra
do compilador.
Agora, antes de pensar em usar o atributo unsafe para ter uma referência sem aumentar a
contagem, considere que, na maioria dos casos, existe outra opção melhor: o uso de referências
fracas. Referências fracas também evitam o aumento da contagem de referências, mas são
gerenciadas. Isso significa que o sistema monitora as referências fracas e, caso o objeto real seja
excluído, ele definirá a referência fraca como nula. Em vez disso, com uma referência insegura, você
não tem como saber o status do objeto de destino (um cenário chamado referência pendente).
Em quais cenários as referências fracas são úteis? Um caso clássico é o de dois objetos com
referências cruzadas. Nesse caso, de fato, o objeto aumentaria artificialmente a contagem de
referências dos outros objetos, e eles basicamente permaneceriam na memória para sempre (com
a contagem de referências definida como 1), mesmo quando se tornassem inacessíveis.
Como exemplo, considere a seguinte interface aceitando uma referência a outra interface do mesmo
tipo e uma classe que a implementa com uma referência interna:
tipo
ISimpleInterface = procedimento de
interface DoSomething;
procedimento AddObjectRef(Simple: ISimpleInterface); fim;
Se você criar dois objetos e fazer referência cruzada a eles, ocorrerá um vazamento de memória:
era
Um, Dois: ISimpleInterface; começar
um :=
TObjectOne.Create; Dois :=
TObjectOne.Create;
One.AddObjectRef(Dois);
Dois.AddObjectRef(Um);
Agora a solução disponível no Delphi é marcar o campo privado AnotherObj como uma referência de
interface fraca:
privado
[fraco] AnotherObj: ISimpleInterface;
Com essa alteração, a contagem de referências não é modificada ao passar o objeto como parâmetro
para a chamada AddObjectRef , ela fica em 1 e volta a zero quando as variáveis saem do escopo, liberando
os objetos da memória.
Agora, existem muitos outros casos em que esse recurso se torna útil e há alguma complexidade real
na implementação subjacente. É um ótimo recurso, mas exige algum esforço para ser totalmente dominado.
Além disso, tem algum custo de tempo de execução, pois as referências fracas são gerenciadas
(enquanto as inseguras não são).
Para obter informações adicionais sobre referências fracas para interfaces e como elas funcionam, você
pode consultar a seção “Gerenciamento de memória e interfaces” no Capítulo 13, “Objetos e memória”.
como implementar múltiplas interfaces ou como implementar um método de interface com um método com um
nome diferente (no caso de conflito de nome).
Outro recurso importante que as interfaces possuem são as propriedades. Para demonstrar todos esses recursos
mais avançados relacionados às interfaces, escrevi o exemplo IntfDemo .
Propriedades da interface
O código nesta seção é baseado em duas interfaces diferentes, IWalker e IJumper, ambas
dos quais definem alguns métodos e uma propriedade.
Uma propriedade de interface é apenas um nome mapeado para um método de leitura e gravação . Ao contrário
de uma classe, você não pode mapear uma propriedade de interface para um campo, simplesmente porque uma
interface não pode ter nenhum código associado a ela.
IWalker = interface
{0876F200-AAD3-11D2-8551-CCA30C584521}' ]
['função Caminhada: string;
função Executar: string;
procedimento SetPos(Valor: Inteiro);
função GetPos: Inteiro;
IJumper = interface
{0876F201-AAD3-11D2-8551-CCA30C584521}' ]
['função Salto: string;
função Caminhada: string;
procedimento SetPos(Valor: Inteiro);
função GetPos: Inteiro;
Ao implementar uma interface com uma propriedade, tudo o que você precisa implementar são os métodos de
acesso reais:
O código de implementação não é complexo (você pode encontrá-lo no exemplo IntfDemo ), com métodos
computando a nova posição e exibindo o que está sendo executado:
Delegação de interface
De forma semelhante, posso definir uma classe simples implementando a interface IJumper :
Esta implementação difere da anterior pela utilização de uma classe base específica, TAggregatedObject. Esta é uma classe
de propósito especial para a definição dos objetos usados internamente para suportar uma interface, com a sintaxe que
mostrarei daqui a pouco.
Vou usá-lo de uma maneira diferente. Na classe a seguir, TMyJumper, não quero repetir a
implementação da interface IJumper com métodos semelhantes. Em vez disso, quero delegar a
implementação dessa interface para uma classe que já a implementa. Isto não pode ser feito
através de herança (já que não podemos ter duas classes base); em vez disso, você pode usar
um recurso específico da linguagem: delegação de interface. A classe a seguir implementa uma
interface referindo-se a um objeto de implementação com uma propriedade, em vez de
implementar os métodos reais da interface:
Esta declaração indica que a interface IJumper está implementada para o TMyJumper
classe pelo campo FJumpImpl . Este campo, é claro, deve implementar todos os métodos da
interface. Para fazer isso funcionar, você precisa criar um objeto adequado para o campo quando
um objeto TMyJumper é criado (o parâmetro construtor é exigido pela classe base
TAggregatedObject ):
construtor TMyJumper.Create;
começar
FJumpImpl := TJumperImpl.Create(Self);
fim;
A classe também possui um destruidor para liberar o objeto interno, que é referenciado por um
campo regular e não por uma interface (já que a contagem de referências não funcionará neste cenário).
Este exemplo é simples, mas em geral as coisas ficam mais complexas à medida que você começa
a modificar alguns dos métodos ou adicionar outros métodos que ainda operam nos dados do objeto
FJumpImpl interno . O conceito geral aqui é que você pode reutilizar a implementação de uma
interface em múltiplas classes. O código que utiliza esta interface implementada indiretamente
é idêntico ao código padrão que se escreveria:
Esta declaração indica que a classe implementa o método Walk da interface IWalker com
um método chamado Walk1 (em vez de um método com o mesmo nome).
Por fim, na implementação de todos os métodos desta classe, precisamos nos referir à
propriedade Position do objeto interno FJumpImpl .
Ao declarar uma nova implementação para a propriedade Position , teremos duas posições
para um único atleta, uma situação bastante estranha. Aqui estão alguns exemplos:
FJumpImpl.Position; fim;
Como podemos criar uma interface para este objeto TAthlete e referir-nos a ambas as operações nas
interfaces IWalker e IJumper ? Bem, não podemos fazer isso exatamente, porque não existe uma
interface base que possamos usar. As interfaces permitem uma verificação e conversão de tipo mais
dinâmica, portanto, o que podemos fazer é converter uma interface em uma diferente, como
desde que o objeto ao qual nos referimos suporte ambas as interfaces, algo que o compilador poderá
descobrir apenas em tempo de execução. Este é o código para esse cenário:
Claro, poderíamos ter escolhido qualquer uma das duas interfaces e convertido para a outra. Usar um
as cast é uma forma de fazer uma conversão em tempo de execução, mas há mais opções quando você
está lidando com interfaces, como veremos na próxima seção.
Polimorfismo de Interface
Na seção anterior vimos como você pode definir múltiplas interfaces e ter um
classe implementa dois deles. Claro, isso poderia ser estendido a qualquer número. Você também
pode criar uma hierarquia de interfaces, pois uma interface pode herdar de outra:
ITripleJumper = interface(IJumper)
'{0876F202-AAD3-11D2-8551-CCA30C584521}' ]
[função TripleJump: string;
fim;
Este novo tipo de interface possui todos os métodos (e propriedades) de seu tipo de interface base
e adiciona um novo método. É claro que existem regras de compatibilidade de interface, tanto quanto
o mesmo que para as aulas.
O que quero focar nesta seção, porém, é um tópico um pouco diferente, que é o polimorfismo
baseado em interface. Dado um objeto de classe base geral, podemos invocar um método virtual e ter
certeza de que a versão correta será chamada. O mesmo pode acontecer com interfaces. Com
interfaces, entretanto, podemos ir um passo além e muitas vezes temos código dinâmico que consulta
uma interface. Dado que o relacionamento objeto-interface pode ser
bastante complexo (um objeto pode implementar múltiplas interfaces e indiretamente também
implementar suas interfaces base) é importante ter uma ideia melhor do que é possível neste cenário.
Como ponto de partida, vamos supor que temos uma referência IInterface genérica. Como sabemos se ele
suporta uma interface específica? Na verdade, existem várias técnicas, que são ligeiramente diferentes de
suas contrapartes de classe:
· Use uma instrução is para teste (e possivelmente uma conversão as para a conversão seguinte). Isto
pode ser usado para verificar se um objeto suporta uma interface, mas não se um objeto
referenciado com uma interface também suporta outra (ou seja, você não pode aplicar is a interfaces).
Observe que, em qualquer caso, a conversão as é necessária, pois uma conversão direta para um tipo
de interface quase invariavelmente resultará em erro.
· Chame a função de suporte global , usando uma de suas muitas versões sobrecarregadas. Você pode
passe para esta função o objeto ou a interface a ser testada, a interface alvo (usando o
GUID ou o nome do tipo), e você também pode passar uma variável de interface onde a interface
real será armazenada, se a função for bem-sucedida.
· Chamar diretamente o método QueryInterface da interface base IInterface , que é um nível um pouco
inferior, sempre requer uma variável de tipo de interface como resultado extra e usa um valor HRESULT
numérico em vez de um resultado booleano.
Aqui está um trecho retirado do mesmo exemplo do IntfDemo mostrando como você pode aplicar as duas
últimas técnicas a uma variável IInterface genérica:
Eu recomendo totalmente o uso de uma das versões sobrecarregadas da função Supports , em comparação
com a chamada QueryInterface . Em última análise, o Supports irá chamá-lo, mas oferece opções mais
simples e de nível superior.
Outro caso em que você deseja usar polimorfismo com interfaces é quando você tem uma matriz de
interfaces de um tipo de interface de nível superior (mas também possivelmente uma matriz de objetos,
alguns dos quais podem suportar a interface fornecida).
Na linguagem atual, você pode converter referências de interface de volta ao objeto original ao qual foram
atribuídas. Existem três operações separadas que você pode usar:
· Você pode escrever um teste is para verificar se um objeto de um determinado tipo pode realmente ser extraído
da referência da interface:
IntfVar é TMyObject
· Você pode escrever um as cast para realizar o type cast, gerando uma exceção no caso de um
erro:
· Você pode escrever um hard type cast para realizar a mesma conversão, retornando nulo
ponteiro em caso de erro:
TMyObject(IntfVar)
note Em todos os casos, a operação de conversão de tipo funciona somente se a interface foi originalmente obtida de um
objeto Object Pascal e não de um servidor COM. Observe também que você pode não apenas converter para a classe
exata do objeto original, mas também para uma de suas classes base (seguindo as regras de compatibilidade de tipos).
Como exemplo, considere ter a seguinte interface simples e classe de implementação (parte do exemplo
ObjFromIntf ):
tipo
ITestIntf = interface(IInterface)
'{2A77A244-DC85-46BE-B98E-A9392EF2A7A7}' ]
[procedimento DoSomething;
fim;
Com essas definições agora você pode definir uma variável de interface, atribuir um objeto a ela e usá-la também
para invocar o método que não está na interface, com a nova conversão:
era
Intf: ITestIntf;
começar
Intf := TTestImpl.Create;
Intf.DoAlguma coisa;
(Intf como TTestImpl).DoSomethingElse;
Você também pode escrever o código da seguinte maneira, usando um teste is e uma conversão direta, e você
sempre pode converter para uma classe base da classe real do objeto:
era
Intf: ITestIntf;
Original: TObject;
começar
Intf := TTestImpl.Create;
Intf.DoAlguma coisa;
se Intf for TObject então
Original := TObject(Intf);
(Original como TTestImpl).DoSomethingElse;
Considerando que uma conversão direta retorna nulo se não for bem-sucedida, você também pode escrever
o código da seguinte maneira (sem o teste is ):
Original := TObject(Intf);
se atribuído (original) então
(Original como TTestImpl).DoSomethingElse;
Observe que atribuir o objeto extraído da interface a uma variável expõe você a problemas de contagem de
referências: quando a interface é definida como nula ou sai do escopo, o objeto é realmente excluído e a
variável referente a ele se tornará inválida. Você encontrará o código destacando o problema no manipulador de
eventos BtnRefCountIssueClick do exemplo.
O padrão pode ser implementado criando uma nova hierarquia de classes que faça o mapeamento ou
estendendo as classes existentes para que exponham uma nova interface. Isso pode ser feito por herança
múltipla (nas linguagens que a suportam) ou usando interfaces.
Neste último caso, que é o que usarei aqui, uma nova classe herdada implementará a interface dada e
mapeará para seu método o comportamento existente.
No cenário específico, o adaptador fornece uma interface comum para consultar valores de vários componentes,
que possuem interfaces inconsistentes (como geralmente acontece nas bibliotecas de UI). Esta é a interface,
chamada ITextAndValue porque permite
tipo
ITextAndValue = interface
'[51018CF1-OD3C-488E-81B0-0470B09013EB]'
procedimento SetText(const Valor: string);
procedimento SetValue (valor const: inteiro);
função GetText: string;
função GetValue: Inteiro;
O próximo passo é criar uma nova subclasse para cada um dos componentes que queremos usar com
a interface. Por exemplo, poderíamos escrever:
tipo
TAdapterLabel = classe(TLabel, ITextAndValue)
protegido
procedimento SetText(const Valor: string);
procedimento SetValue (valor const: inteiro);
função GetText: string;
função GetValue: Inteiro;
fim;
A implementação destes quatro métodos é bastante simples, pois eles podem ser mapeados para a
propriedade Text realizando uma conversão de tipo caso o valor (ou o texto) seja um número. Agora
que você tem um novo componente, entretanto, você terá que instalá-lo (como mencionamos no
capítulo anterior) e substituir os componentes existentes em seus formulários por este novo. Repetir o
mesmo processo para cada um dos componentes que você deseja adaptar consumiria muito
tempo.
Uma alternativa muito mais simples seria usar o idioma da classe interposer (ou seja, definir um
classe com o mesmo nome da classe base, mas em uma unidade diferente). Isto será corretamente
reconhecido pelo compilador e pelo sistema de streaming em tempo de execução, de modo que, em
tempo de execução, você acabará com um objeto da nova classe específica. A única diferença é
que em tempo de design você verá e interagirá com instâncias da classe de componente base.
note As classes Interposer foram mencionadas pela primeira vez e receberam esse nome há muitos anos no The Delphi Maga-
zine. Eles certamente são um pouco complicados, mas às vezes úteis. Considero classes intermediárias, ou seja,
classes com o mesmo nome de uma classe base, mas definidas em uma unidade diferente, mais como uma
linguagem Object Pascal. Observe que para que esse mecanismo funcione é fundamental que a unidade com a
classe interposta esteja listada na instrução uses após aquela com a classe regular que ela deve substituir. Em
outras palavras, os símbolos definidos nas últimas unidades da declaração de usos substituem um símbolo
idêntico definido em unidades anteriormente incluídas. Claro, você sempre pode discriminar os símbolos prefixando-
os com o nome da unidade, mas isso realmente anularia toda a ideia deste hack, que é aproveitar as vantagens
das regras globais de resolução de nomes.
Para definir uma classe intermediária, você geralmente escreveria uma nova unidade com uma
classe com o mesmo nome de uma classe base existente. Para se referir à classe base, você
deve prefixá-la com o nome da unidade (ou o compilador irá considerá-la uma definição recursiva):
tipo
TLabel = classe (StdCtrls. TLabel, ITextAndValue)
protegido
procedimento SetText(const Valor: string);
procedimento SetValue (valor const: inteiro);
função GetText: string;
função GetValue: Inteiro;
fim;
Neste caso você não terá que instalar componentes ou mexer nos programas existentes, mas apenas
adicionar uma declaração extra de uso a eles no final da lista. Em ambos os casos (no aplicativo
de demonstração que escrevi que usa classes intermediárias), você pode consultar os componentes
do formulário para esta interface do adaptador e, por exemplo, escrever código para definir todos os
valores como 50, o que afetará diferentes propriedades de diferentes componentes:
era
Intf: ITextAndValue;
Eu: inteiro;
começar
para I := 0 para ComponentCount - 1 faça
se Supports(Components[I], ITextAndValue, Intf) então
Intf.Valor := 50;
fim;
No exemplo específico, este código afetará o valor de uma barra de progresso ou caixa numérica e
o texto de um rótulo ou edição. Ele também ignorará totalmente alguns outros componentes para os
quais não defini a interface do adaptador. Embora este seja apenas um caso muito específico, se você
examinar outros padrões de design, descobrirá facilmente que alguns deles
pode ser melhor implementado aproveitando a flexibilidade extra que as interfaces têm ao longo
classes em Object Pascal (como em Java e C#, apenas para citar outras linguagens populares que
fazem uso extensivo de interfaces).
12: manipulação
Aulas
Nos últimos capítulos você viu os fundamentos do lado do objeto da linguagem Object Pas-cal: classes, objetos,
métodos, construtores, herança, ligação tardia, interfaces e muito mais. Agora precisamos dar um passo
adiante, examinando alguns recursos mais avançados e bastante específicos da linguagem relacionados ao
gerenciamento de classes.
De referências de classe a auxiliares de classe, este capítulo cobre muitos recursos não encontrados em outras
linguagens OOP, ou pelo menos implementados de maneira significativamente diferente.
O foco são as classes, e a manipulação de classes em tempo de execução, um tópico que ampliaremos ainda mais
ao cobrir reflexão e atributos no Capítulo 16.
Para declarar um método de classe em Object Pascal, basta adicionar a palavra-chave class na frente dele, como
antes da palavra-chave de procedimento ou função :
tipo
TMyClass = turma
função de classe MeanValue: Inteiro;
Dado um objeto MyObject da classe TMyClass, você pode chamar o método aplicando-o a um objeto ou à classe
como um todo:
era
MeuObjeto: TMyClass;
começar
...
I := TMyClass.MeanValue;
J := MeuObjeto.ValorMédio;
Esta sintaxe implica que você pode chamar o método da classe mesmo sem ter uma instância da classe. Existem
cenários de classes feitas apenas de métodos de classe, com a ideia implícita de que você nunca criará objetos
dessas classes (algo que você pode impor declarando o construtor Create como privado).
note O uso de métodos de classe em geral e de classes compostas apenas de métodos de classe em particular é mais
comum em linguagens OOP que não permitem o uso de funções globais. Object Pascal ainda permite declarar
funções globais antiquadas , mas, nos últimos anos, as bibliotecas do sistema e o código escrito pelos
desenvolvedores têm se movido cada vez mais em direção a um uso consistente de métodos de classe.
A vantagem de usar métodos de classe é que eles ficam logicamente vinculados a uma classe, que atua como
uma espécie de espaço de nomes para um grupo de funções relacionadas.
Dados de classe
Os dados da classe são dados compartilhados entre todos os objetos da classe, oferecendo armazenamento
global, mas acesso específico da classe (incluindo limitações de acesso). Como você declara dados de classe?
Simplesmente definindo uma seção da classe marcada com a combinação de palavras-chave class var :
tipo
TMyData = classe
privado
a aula era
FCommonCount: Inteiro;
público
função de classe GetCommon: Inteiro;
A seção class var introduz um bloco de uma ou mais declarações de campos de dados de classe. Você pode
usar uma seção var para declarar outros campos de instância regulares na mesma seção (privado abaixo):
tipo
TMyData = classe
privado
a aula era
FCommonCount: Inteiro;
era
MaisObjectData: string;
público
função de classe GetCommon: Inteiro;
Além de declarar dados de classe, você também pode definir propriedades de classe, como veremos em uma
seção posterior.
À primeira vista, o fato de um método de classe possuir um parâmetro oculto que se refere à própria
classe pode parecer bastante inútil. Afinal, o compilador conhece a classe de um método. Porém, há uma
característica peculiar da linguagem que explica isso: diferentemente da maioria das outras linguagens, na
classe Object Pascal os métodos podem ser virtuais. Em uma classe derivada, você pode substituir um
método de classe de tipo base, como faria com um método regular.
note O suporte para método de classe virtual está conectado com o suporte para construtores virtuais (que são
uma espécie de método de classe de propósito especial). Ambos os recursos não são encontrados em muitas linguagens OOP
compiladas e fortemente tipadas.
Aqui está um exemplo simples com algumas declarações incorretas comentadas, retiradas do exemplo
ClassStatic:
tipo
TBase = classe
privado
FTmp: Inteiro;
público
procedimento de classe Um;
procedimento de classe Dois; estático;
fim;
procedimento de classe TBase.One;
começar
// Erro: membro da instância 'FTmp' inacessível aqui
// Mostrar(FTmp);
'Um'
Mostrar();
Show(Self.ClassName);
fim;
Em ambos os casos você pode chamar esses métodos de classe diretamente ou invocá-los através de
um objeto:
TBase.Um;
TBase.Dois;
Base := TBase.Create;
Base.Um;
Base.Dois;
Existem dois recursos interessantes que tornam os métodos estáticos de classe úteis no Object Pas-cal. A
primeira é que eles podem ser usados para definir propriedades de classe, conforme descrito na
próxima seção. A segunda é que os métodos estáticos de classe são totalmente compatíveis com a linguagem
C, conforme explicado abaixo.
O fato de não terem nenhum parâmetro Self oculto implica que métodos de classe estáticos podem ser
passados para o sistema operacional (por exemplo, no Windows) como funções de retorno de chamada. Na
prática, você pode declarar um método de classe estático com a convenção de chamada stdcall e usá-lo como
um retorno de chamada direto da API do Windows, como fiz para o método TimerCallBack do exemplo
StaticCallBack :
tipo
TFormCallBack = classe(TForm)
ListBox1: TListBox;
procedimento FormCreate(Remetente: TObject);
privado
a aula era
NTimerCount: Inteiro;
público
procedimento AddToList (const AMessage: string);
procedimento de classe TimerCallBack(hwnd: THandle;
uMsg, idEvent, dwTime: Cardeal); estático; chamada padrão;
fim;
Os dados da classe são usados pelo retorno de chamada como contador. O manipulador OnCreate chama a API
Set-Timer passando o endereço do procedimento de classe estática:
note O parâmetro para TFNTimeProc é um ponteiro de método e é por isso que o nome do método de classe
estática é precedido por um @ ou usando a função Addr . Isso porque precisamos obter o endereço do método,
não execute o método.
Agora a função de retorno de chamada real aumenta o cronômetro e atualiza o formulário, referindo-se a
usando a variável global correspondente – o que deve ser evitado, mas exigiria a adição de alguma
complexidade significativa à demonstração – já que um método de classe não pode se referir ao formulário
como Self:
Propriedades de classe
Uma das razões para usar métodos de classe estáticos é implementar propriedades de classe.
O que é uma propriedade de classe? Como uma propriedade padrão, é um símbolo anexado aos mecanismos de
leitura e gravação. Ao contrário de uma propriedade padrão, ela se relaciona com a classe e deve ser implementada
mente usando dados de classe ou métodos de classe estáticos. A classe TBase (novamente, do
exemplo ClassStatic ) possui duas propriedades de classe definidas de duas maneiras possíveis:
tipo
TBase = classe
privada
classe var
FMyNome: string; função
de classe
pública GetMyName: string; estático; procedimento de classe
SetMyName(Valor: string); estático; propriedade de classe MeuNome: string
lida GetMyName escreve SetMyName; propriedade de classe DirectName: string lida FMyName
escreve FMyName; fim;
tipo
TCountedObj = classe(TObject) privado
FTotal: Inteiro;
FCatual: Inteiro; construtor
público
Criar; destruidor Destruir;
sobrepor; valor da propriedade: leitura inteira
FValue gravação FValue; função de classe pública GetTotal: Inteiro; função de
classe
GetCurrent: Inteiro; fim;
Cada vez que um objeto é criado, o programa incrementa ambos os contadores. Cada vez que um
objeto é destruído, o contador atual diminui:
Inc(FTtotal); Inc(FCorrente);
fim;
destruidor TCountedObj.Destroy;
começar
Dez(FCorrente);
Destruição herdada;
fim;
As informações da classe podem ser acessadas sem a necessidade de referência a um objeto específico. Na
verdade, pode muito bem acontecer que num determinado momento não existam objetos na memória:
Na demonstração, isso é executado em um timer, que atualiza um rótulo, portanto não precisa se referir a nenhuma
instância específica do objeto nem é acionado diretamente por nenhuma operação manual. Os botões
no exemplo, simplesmente crie e libere alguns dos objetos ou deixe alguns na memória
(na verdade, o programa tem alguns possíveis vazamentos de memória).
No Object Pascal, um construtor de classe é uma alternativa ao código de inicialização da unidade. Caso ambos
existam (em uma unidade), será executado primeiro o construtor da classe e depois o código de inicialização da
unidade. Ao contrário, você pode definir um destruidor de classe que será executado após o código de finalização.
Uma diferença significativa, entretanto, é que, embora o código de inicialização da unidade seja invariavelmente
executado se a unidade for compilada no programa, o construtor e o destruidor da classe serão vinculados apenas
se a classe for realmente usada. Isso significa que o uso do construtor de classe é muito mais amigável ao vinculador
do que o uso do código de inicialização.
note Em outras palavras, com construtores e destruidores de classe, se o tipo não estiver vinculado, a inicialização
o código não faz parte do programa e não é executado. No caso tradicional, o oposto é verdadeiro, o código de
inicialização pode até fazer com que o vinculador traga parte do código da classe, mesmo que ele nunca
seja realmente usado em nenhum outro lugar. Em termos práticos, isso foi introduzido junto com a estrutura de
gestos, uma quantidade bastante grande de código que não é compilado no executável se não for usado.
tipo
TTestClass = classe
público
a aula era
HoraInicial: TDateTime;
EndTime: TDateTime;
público
construtor de classe Criar;
destruidor de classe Destruir;
fim;
A classe possui dois campos de dados de classe, inicializados pelo construtor da classe e modificados por
um destruidor de classe, enquanto as seções de inicialização e finalização da unidade usam estes
campos de dados:
inicialização
ShowMessage(TimeToStr(TTestClass.StartTime));
finalização
ShowMessage(TimeToStr(TTestClass.EndTime));
O que acontece é que a sequência de inicialização funciona conforme o esperado, com os dados da
classe já disponíveis conforme você vai mostrando as informações. Ao fechar, em vez disso, o ShowMessage
a chamada é executada antes que o valor seja atribuído pelo destruidor de classe.
Observe que você pode dar qualquer nome ao construtor e ao destruidor da classe, embora Cre-ate e
Destroy sejam padrões muito bons. Entretanto, você não pode definir vários construtores ou destruidores
de classes. Se você tentar, o compilador emitirá um erro semelhante a este:
[Erro DCC] ClassCtorMainForm.pas(34): E2359 Vários construtores de classe na classe TTestClass: Create e Foo
Em geral, acho que usar construtores e destruidores de classe é melhor do que usar inicialização e
finalização de unidade. Na maioria dos casos, isso é apenas um açúcar sintático, então talvez você
não queira voltar e alterar o código existente. Entretanto, se você enfrentar o risco de inicializar
estruturas de dados que você nunca usará (porque nenhuma classe desse tipo é criada), mudar para
construtores de classe fornecerá uma vantagem definitiva. Este é claramente o caso com mais
frequência em uma biblioteca geral, da qual você não usa todos os recursos, do que no código do
aplicativo.
note Um caso muito específico de uso de construtores de classes é no caso de classes genéricas. Vou cobrir isso no
capítulo focado em genéricos.
O padrão singleton pode ser implementado de várias maneiras, mas uma abordagem clássica é chamar
a função usada para acessar a única instância exatamente como Instance. Em muitos casos, a
implementação também segue a regra de inicialização lenta, de modo que esta instância global não é
criada quando o programa é iniciado, mas apenas na primeira vez que é necessária.
Na implementação abaixo aproveitei dados de classe, métodos de classe, mas também destruidores
de classe para a limpeza final. Aqui está o código relevante:
tipo
TSingleton = classe(TObject)
público
função de classe Instância: TSingleton;
privado
classe var TheInstance: TSingleton;
destruidor de classe Destruir;
fim;
Você pode pegar a instância única da classe (independentemente de ela já ter sido criada ou não)
escrevendo:
era
ASingle: TSingleton;
começar
ASingle := TSingleton.Instance;
Além disso, você pode ocultar o construtor da classe regular, declarando-o privado, de forma que será muito
difícil criar um objeto da classe sem seguir o padrão.
Referências de classe
Tendo examinado vários tópicos relacionados a métodos, podemos agora passar para o tópico de referências
de classes e estender ainda mais nosso exemplo de criação dinâmica de componentes.
O primeiro ponto a ter em mente é que uma referência de classe não é uma classe, não é um objeto e não
é uma referência a um objeto; é simplesmente uma referência a um tipo de classe. Em outras palavras,
uma variável de um tipo de dados de referência de classe tem uma classe como valor.
Um tipo de referência de classe determina o tipo de uma variável de referência de classe. Parece confuso?
Algumas linhas de código podem tornar isso mais claro. Suponha que você tenha definido a classe
TMyClass. Agora você pode definir um novo tipo de referência de classe relacionado a essa classe:
tipo
TMyClassRef = classe de TMyClass;
Agora você pode declarar variáveis de ambos os tipos. A primeira variável refere-se a um objeto, a segunda
a uma classe:
era
AClassRef: TMyClassRef;
UmObjeto: TMyClass;
começar
AClassRef := TMyClass;
AnObject := TMyClass.Create;
Você pode estar se perguntando para que servem as referências de classe. Em geral, as referências de classe permitem
manipular um tipo de dados de classe em tempo de execução. Você pode usar uma referência de classe em qualquer
expressão onde o uso de um tipo de dados seja legal. Na verdade, não existem muitas expressões desse tipo, mas os
poucos casos são interessantes. O caso mais simples é a criação de um objeto. Podemos reescrever as duas linhas acima
da seguinte forma:
AClassRef := TMyClass;
AnObject := AClassRef.Create;
Desta vez apliquei o construtor Create à referência de classe em vez de a uma classe real; Usei uma referência de classe
para criar um objeto dessa classe.
note As referências de classe estão relacionadas ao conceito de metaclasse disponível em outras linguagens OOP. Em
Object Pascal, entretanto, uma referência de classe não é em si uma classe, mas apenas um tipo específico que define
uma referência aos dados da classe. Portanto, a analogia com metaclasses (classes que descrevem outras classes) é um pouco
errôneo. Na verdade, TMetaclass também é o tipo de dados usado no C++Builder.
Quando você tem uma referência de classe, você pode acessar qualquer método de classe da classe. Então, se TMyClass
tivesse um método de classe chamado Foo, você seria capaz de escrever:
TMyClass.Foo
AClassRef.Foo
Isso não seria muito útil se as referências de classe não suportassem a mesma regra de compatibilidade de tipo que se aplica
aos tipos de classe. Ao declarar uma variável de referência de classe, como MyClassRef acima, você pode atribuir a ela
aquela classe específica e qualquer subclasse.
Então, se MyNewClass for uma subclasse da minha classe, você também pode escrever
AClassRef := MinhaNovaClasse;
Para entender por que isso pode realmente ser interessante, você deve lembrar que os métodos de classe que você
pode chamar para uma referência de classe podem ser virtuais, portanto, a subclasse específica pode substituí-
los. Usando referências de classe e métodos de classe virtuais, você pode implementar uma forma de polimorfismo
no nível do método de classe que poucas (se houver) das outras linguagens OOP estáticas suportam.
Considere também que cada classe herda de TObject, então você pode aplicar a cada referência de classe
alguns dos métodos de TObject, incluindo Instance-Size, ClassName, ParentClass e InheritsFrom. Discutirei esses
métodos de classe e outros métodos da classe TObject no Capítulo 17.
Em particular, o tipo de referência de classe TClass pode ser usado para armazenar uma referência a
qualquer classe que você escreve em Object Pascal, porque cada classe é, em última análise, derivada de TObject.
A referência TFormClass , em vez disso, é usada no código-fonte do projeto Object Pas-cal padrão baseado
em FireMonkey ou VCL. O método CreateForm do objeto Application para ambas as bibliotecas,
de fato, requer como parâmetro a classe do formulário a ser criado:
Application.CreateForm(TMyForm1, MeuFormulário);
O primeiro parâmetro é uma referência de classe, o segundo é uma variável que receberá uma referência à
instância do objeto criado.
Para lhe dar uma ideia melhor de como funcionam as referências de classe, criei um exemplo chamado
ClasseRef. O formulário exibido neste exemplo é bastante simples. Possui três botões de rádio
colocados dentro de um painel na parte superior do formulário. Ao selecionar um desses botões de
opção e clicar no formulário, você poderá criar novos componentes dos três tipos indicados pelos rótulos
dos botões: botões de opção, botões normais e caixas de edição. Para que este programa funcione
corretamente, ele precisa alterar os nomes dos três componentes, pois os nomes dos componentes devem
ser exclusivos. O formulário também deve ter um campo de referência de classe:
privado
FControlType: TControlClass;
FControlNo: Inteiro;
O primeiro campo armazena um novo tipo de dados toda vez que o usuário clica em um dos três botões
de opção, alterando seu status. Aqui está um dos três métodos:
Os outros dois botões de opção possuem manipuladores de eventos OnChange semelhantes a este,
atribuindo o valor TEdit ou TButton ao campo FControlType . Uma atribuição semelhante também está
presente no manipulador do evento OnCreate do formulário, usado como método de inicialização. A
parte interessante do código é executada quando o usuário clica em um TLayout
controle que cobre a maior parte da superfície do formulário. Escolhi o evento OnMouseDown do formulário
para manter a posição do clique do mouse:
// Agora mostre
NewCtrl.Visible := Verdadeiro;
fim;
A primeira linha do código deste método é a chave. Ele cria um novo objeto do tipo de dados de classe
armazenado no campo FControlType . Conseguimos isso simplesmente aplicando o construtor Create à
referência de classe.
Agora você pode definir o valor da propriedade Parent , definir a posição do novo componente, dar-lhe
um nome (que também é usado automaticamente como Texto) e torná-lo visível.
Observe em particular o código usado para construir o nome: para imitar a convenção de nomenclatura
padrão do Object Pascal, peguei o nome da classe com a expressão FControlType-
.ClassName, usando um método de classe da classe TObject . Depois adicionei um número no final do
nome e removi a letra inicial da string.
Para o primeiro botão de opção, digamos que FControlNo seja 1 e então FControlType seja
TRadioButton; portanto, FControlType.ClassName retorna a string 'TRadioButton' e
FControlNo.AsString retorna '1' que anexamos ao final antes de copiarmos a string menos o primeiro
caractere, ou seja, menos o 'T' do início da string, para forneça a string final 'RadioButton1' que usaremos
para a nova instância. Parece familiar?
Figura 12.1:
Um exemplo da saída
do aplicativo ClassRef em
execução na janela
Você pode ver um exemplo da saída deste programa na Figura 12.1. Observe que a nomenclatura não
é exatamente a mesma usada pelo IDE, que usa um contador separado para cada tipo de controle.
Este programa usa um único contador para todos os componentes. Portanto, se você colocar um botão,
seguido por um botão de opção, depois duas edições e depois outro botão, ao executar o aplicativo
ClassRef, seus nomes serão Button1, RadioButton2, Edit3, Edit4, Button5 como mostra a imagem.
Observe que, diferentemente do designer, os controles de edição criados não têm visibilidade de seus
nomes.
À parte, considere que uma vez criado um componente genérico, você pode acessar suas propriedades de uma forma muito
dinâmica, usando reflexão, um tópico abordado em detalhes no Capítulo 16. Nesse mesmo capítulo veremos que existem
outras maneiras para se referir a informações de tipo e classe ao lado de referências de classe.
Embora a herança de tipo seja um mecanismo extremamente poderoso, há cenários em que ela não é
ideal. O fato é que, ao trabalhar com bibliotecas existentes e complexas, você pode querer aumentar um tipo
de dados sem herdar um novo. Isto é particularmente verdadeiro quando os objetos são criados de forma
automática e a substituição da sua criação pode ser extremamente complexa.
Um cenário bastante óbvio para desenvolvedores de Object Pascal é o uso de componentes. Se você quiser
adicionar um método a uma classe de componente, para fornecer algum novo comportamento a ele, você
pode de fato usar herança, mas isso implica que se crie o novo tipo derivado, crie
um pacote para instalá-lo e substituir todos os componentes existentes em formulários e outras superfícies de
design pelo novo tipo de componente (uma operação que afeta a definição do formulário e o arquivo de
código-fonte).
A abordagem alternativa é usar um auxiliar de classe ou registro. Esses tipos de dados para fins especiais
podem estender um tipo existente com novos métodos. Mesmo que tenham algumas limitações, os auxiliares
de classe permitem lidar com um cenário como o que acabei de descrever simplesmente adicionando
novos métodos a um componente existente, sem a necessidade de modificar o tipo real do componente.
note Na verdade, já vimos uma abordagem alternativa para estender uma classe de biblioteca sem substituir
completamente suas referências, usando o idioma da classe interposer , ou seja, usando herança e com uma
classe de mesmo nome. Abordei esse idioma na seção final do capítulo anterior. Os ajudantes de classe
oferecem um modelo mais limpo; entretanto, eles não podem ser usados para substituir métodos virtuais ou
implementar uma interface extra, como fiz na aplicação do capítulo anterior.
Ajudantes de classe
Um auxiliar de classe é uma forma de adicionar métodos e propriedades a uma classe que você não tem
poder de modificar (como uma classe de biblioteca). Usar um auxiliar de classe para estender uma classe
em seu próprio código é realmente incomum, pois nesse caso você geralmente deve seguir em frente e alterar
a classe real.
Embora a ideia de auxiliares de classe seja específica do Object Pascal do Delphi, outras linguagens de
programação possuem conceitos como métodos de extensão ou classes implícitas que oferecem um recurso
semelhante.
O que você não pode fazer em um auxiliar de classe é adicionar dados de instância, já que os dados devem
residir nos objetos reais e estes são definidos por sua classe original, ou tocar nos métodos virtuais,
novamente definidos na estrutura física da classe original. Em outras palavras, uma classe auxiliar só pode
adicionar ou substituir métodos não virtuais de uma classe existente. Desta forma você poderá aplicar o
novo método a um objeto da classe original, mesmo que isso
classe não tem ideia da existência do método.
Se isso não estiver claro, e provavelmente não está, vejamos um exemplo retirado do
Exemplo ClassHelperDemo – uma demonstração do que você não deve fazer, ou seja, usar auxiliares de classe
para aumentar sua própria classe:
tipo
TMyObject = classe
protegido
Valor: Inteiro;
Texto: sequência;
público
procedimento Aumento;
fim;
O código anterior declara uma classe e um auxiliar para esta classe. Isso significa que para um objeto do tipo
TMyObject, você pode chamar os métodos da classe original, bem como os métodos da classe auxiliar:
Obj := TMyObject.Create;
Obj.Texto := ; 'Foo'
Obj.Mostrar;
Os métodos da classe auxiliar tornam-se parte da classe e podem usar Self como qualquer outro
método para se referir ao objeto atual (da classe ajuda porque os auxiliares de classe não são instanciados),
como este código demonstra:
procedimento TMyObjectHelper.Show;
começar
' ' ' -- '
Show(Texto + + IntToStr(Valor) + +
' -- '
NomeDaClasse + ToString);
+ fim;
Finalmente, observe que um método de classe auxiliar pode substituir o método original. No código, adicionei um
método Show à classe e à classe auxiliar, mas apenas o método da classe auxiliar é chamado!
É claro que faz muito pouco sentido declarar uma classe e uma extensão para a mesma classe usando a
sintaxe auxiliar de classe na mesma unidade ou mesmo no mesmo programa. Fiz isso na demonstração apenas
para facilitar a compreensão dos detalhes técnicos dessa sintaxe.
Os auxiliares de classe não devem ser usados como uma construção de linguagem geral para o desenvolvimento
de arquiteturas de aplicativos, mas destinam-se principalmente a estender classes de biblioteca para as quais
você não possui o código-fonte ou não deseja alterá-lo para evitar conflitos futuros.
Existem mais algumas regras que se aplicam aos auxiliares de classe, ou seja, métodos auxiliares de classe:
· podem ser métodos virtuais, que podem ser substituídos em uma classe derivada (embora eu ache isso um pouco
estranho em termos práticos)
· pode introduzir construtores extras
O único recurso que falta por design são os dados de instância. Observe também que os auxiliares de classe são
habilitados à medida que se tornam visíveis no escopo. Você precisa adicionar uma instrução de uso referente à
unidade que declara o auxiliar de classe para ver seus métodos, e não apenas incluí-lo uma vez
no processo de compilação.
note Por algum tempo, ocorreu um erro no compilador Delphi que acabou permitindo que os auxiliares de classe
acessar campos privados da classe que ajudaram, independente da unidade em que a classe foi declarada.
Esse “hack” basicamente quebrou as regras de encapsulamento da programação orientada a objetos. Para impor a
semântica de visibilidade, os auxiliares de classe e registro nas versões mais recentes dos compiladores Object Pascal
(começando com Delphi 10.1 Berlin) não podem acessar membros privados das classes ou registros que eles estendem.
Na verdade, isso fez com que o código existente não funcionasse mais, código que aproveitava esse hack (que
nunca foi planejado para ser um recurso de linguagem).
Um uso prático dos auxiliares de classe é fornecer métodos extras para classes de biblioteca. A razão é que
você não deseja alterar essas classes diretamente (mesmo se você tiver o código-fonte, você realmente não
deseja editar as fontes da biblioteca principal) ou herdar delas (pois isso forçaria você a substituir os componentes
no formulários em tempo de design).
Como exemplo, considere este caso simples: você deseja uma maneira simples de obter o texto da seleção atual
de uma caixa de listagem. Em vez de escrever o código clássico:
ListBox1.Items[ListBox1.ItemIndex]
você pode definir um auxiliar de classe da seguinte maneira (retirado do projeto ControlHelper ):
tipo
TListBoxHelper = auxiliar de classe para TListBox
função ItemIndexValue: string;
fim;
Show(ListBox1.ItemIndexValue);
Este é apenas um caso muito simples, mas que mostra a ideia em termos muito práticos.
Uma solução parcial para esse problema vem do fato de que você pode introduzir um auxiliar de classe
para uma classe e adicione mais um auxiliar de classe para uma classe herdada; mas você não pode
herdar diretamente um auxiliar de classe de outro auxiliar de classe. Eu realmente não incentivo entrar em
estruturas auxiliares de classe complexas, porque elas podem realmente transformar seu código em uma
bagunça muito complicada.
Os auxiliares realmente fazem sentido quando você adiciona itens a uma biblioteca ou estrutura de dados
definida pelo sistema. Um exemplo seria o registro TGUID , uma estrutura de dados do Windows que você
pode usar em várias plataformas no Object Pascal, que possui um auxiliar que adiciona alguns recursos
comuns:
tipo
TGUIDHelper = auxiliar de registro para TGUID
função de classe Criar (const B: TBytes): TGUID; sobrecarga; estático;
função de classe Criar (const S: string): TGUID; sobrecarga; estático;
// ... Várias sobrecargas de criação omitidas
função de classe NewGuid: TGUID; estático;
função ToByteArray: TBytes;
função ToString: string;
fim;
Você deve ter notado que TGUIDHelper é um auxiliar de registro em vez de um auxiliar de classe.
Sim, os registros podem ter ajudantes assim como as classes.
Outra operação comum é navegar pelos controles filhos, que incluem apenas componentes visuais (excluindo
componentes não visuais como um TMainMenu) que possuem o formato como
pai direto (excluindo controles hospedados por um controle filho, como um botão em um painel). Uma
técnica que podemos usar para escrever um código mais simples para alternar entre controles filhos é
adicionar uma nova enumeração à classe TWinControl escrevendo o seguinte auxiliar de classe:
tipo
TControlsEnumHelper = auxiliar de classe para o tipo TWinControl
NPos: Inteiro;
FControl: TWinControl; construtor
público
Criar (AControl: TWinControl); função MoveNext: Booleano; função
GetCurrent: TControl; propriedade Atual:
TControl lê GetCurrent; fim; função pública
GetEnumerator: TControlsEnum; fim;
note A razão pela qual o auxiliar é para TWinControl e não para TControl é que apenas controles com um identificador
de janela podem ser pais de outros controles. Isso basicamente exclui controles gráficos.
Este é o código completo do helper, incluindo seu único método e aqueles da classe aninhada
TControlsEnum:
{ TControlsEnumHelper }
{ TControlsEnumHelper.TControlsEnum }
FControl.Controls[NPos]; fim;
Resultado := NPos
< FControl.ControlCount; fim;
Agora, se criarmos um formulário como o da Figura 12.2, poderemos testar a enumeração em vários
cenários. O primeiro caso é para o qual escrevemos especificamente o código, ou seja, enumerar os
controles filhos do formulário:
Figura 12.2: O
formulário usado para
testar o auxiliar de
enumeração de controle
em tempo de design
no Delphi IDE
Esta é a saída da operação no controle memo, listando os controles filho do formulário, mas não outros
componentes ou controles criados pelo painel:
Formulário Filho
Memo1
BtnFormChild
Editar1
Caixa de seleção1
RadioButton1
Painel1
BtnFormComps
BtnButtonChild
Componentes do formulário
Memo1
BtnFormChild
Editar1
Caixa de seleção1
RadioButton1
Painel1
BtnPanelChild
Caixa de combinação1
BtnFormComps
BtnButtonChild
Menu Principal1
No exemplo ControlsEnum também adicionei código para enumerar os controles filhos do painel e os de um dos
botões (principalmente para testar se o enumerador funciona corretamente quando a lista está vazia).
note Os auxiliares de registro são usados atualmente para aumentar e adicionar operações semelhantes a métodos a tipos de
dados nativos, mas isso também pode mudar no futuro. A biblioteca de tempo de execução de hoje define alguns auxiliares
nativos que podem desaparecer no futuro, preservando a maneira como você escreve o código que usa esses auxiliares...
mas quebrando a compatibilidade no código que os define. É por isso que você não deve abusar desse recurso, mesmo
que seja certamente muito agradável e prático.
Como os auxiliares de tipo intrínseco funcionam na prática? Vamos considerar a seguinte definição de auxiliar para o
tipo Integer :
tipo
TIntHelper = auxiliar de registro para Integer
função AsString: string;
fim;
N.AsString;
Como você define esse pseudométodo e como ele pode se referir ao valor da variável? Ampliando o
significado do identificador próprio para se referir ao valor ao qual a função é aplicada:
Observe que você também pode aplicar métodos a constantes, como em:
Legenda:= 400000.AsString;
No entanto, você não pode fazer o mesmo para um valor pequeno, pois o compilador interpreta as
constantes como sendo do menor tipo possível. Portanto, se você deseja obter o valor da string 4,
que corresponde a um byte, você deve usar a segunda forma:
Ou você pode fazer a primeira instrução a ser compilada definindo um auxiliar diferente:
tipo
TByteHelper = auxiliar de registro para Byte...
Como já vimos no Capítulo 2, você realmente não precisa escrever o código acima para tipos como
Integer e Byte, pois a biblioteca de tempo de execução define uma lista bastante abrangente de
auxiliares de classe para a maioria dos tipos de dados principais, incluindo os seguintes, que são definidos
na unidade System.SysUtils :
Existem alguns outros auxiliares de tipo intrínseco atualmente definidos em outras unidades, como:
//Sistema.Character:
TCharHelper = auxiliar de registro para Char
//System.Classes:
TUInt32Helper = auxiliar de registro para UInt32
//System.DateUtils
TDateTimeHelper = auxiliar de registro para TDateTime // adicionado em Delfos 11
Dado que abordei o uso desses auxiliares em muitos exemplos na parte inicial do livro, não há necessidade de
reiterá-los aqui. O que esta seção adicionou é uma descrição de como você pode definir um auxiliar de tipo
intrínseco.
Uma solução que gosto é a definição de um alias de tipo. Um alias de tipo é visto como um tipo totalmente novo
pelo compilador, portanto ele pode ter seu próprio auxiliar sem substituir o auxiliar do tipo original. Agora, como
os tipos são separados, você ainda não pode aplicar métodos de ambas as classes
ajudantes para a mesma variável, mas um dos conjuntos será um tipo rejeitado. Deixe-me explicar
isso em termos de código. Suponha que você crie um alias de tipo como:
tipo
MeuInt = tipo Inteiro;
tipo
TMyIntHelper = auxiliar de registro para MyInt
função AsString: string;
fim;
Se você declarar uma variável deste novo tipo, o acesso padrão permitirá que você invoque o auxiliar do novo tipo,
mas você ainda poderá acessar o auxiliar do tipo Integer com uma conversão - este código está no exemplo
TypeAliasHelper , para você experimentar mais variações:
Mostrar(MI.AsString);
// Mostrar(MI.ToString); //Isso não funciona
Mostrar(Integer(MI).ToString);
fim;
13: objetos e
memória
Este capítulo focou em um tópico muito específico e bastante importante, que é o gerenciamento de
memória na linguagem Object Pascal. A linguagem e seu ambiente de execução oferecem uma
solução única que fica entre o gerenciamento manual de memória no estilo C++ e a coleta
automática de lixo no estilo Java ou C#.
A razão para esta abordagem intermediária é que ela ajuda a evitar a maioria dos problemas de
gerenciamento manual de memória (mas claramente não todos), sem as restrições e os problemas
causados pela coleta automática de lixo, desde a alocação extra de memória até o descarte não
determinístico.
note Não tenho nenhuma intenção particular de me aprofundar nos problemas das estratégias de GC (Coleta de Lixo) e como
elas são implementadas nas diversas plataformas. Este é mais um tema de pesquisa. O que é relevante
é que em dispositivos restritos, como os móveis, o GC está longe de ser ideal, mas alguns dos mesmos
problemas se aplicam a todas as plataformas. A tendência de ignorar o consumo de memória dos aplicativos do
Windows nos trouxe pequenos utilitários que consomem grandes quantidades de memória cada.
O que torna as coisas um pouco mais complicadas no Object Pascal, porém, é o fato de que uma
variável usa memória dependendo de seu tipo de dados, com alguns tipos usando contagem de
referência e alguns uma abordagem mais tradicional, incluindo o modelo de propriedade baseado em
componentes da VCL, e um poucas outras opções, tornam o gerenciamento de memória uma tarefa complexa
tema. Este capítulo está aqui para abordar isso, começando com alguns dos fundamentos do gerenciamento
de memória em linguagens de programação modernas e os conceitos por trás do modelo de referência de
objeto.
note Por muitos anos, os compiladores móveis Delphi ofereceram um modelo de memória diferente chamado ARC, ou Auto-
Contagem de referência automática. Promovido pela Apple em linguagens próprias, o ARC adiciona suporte do
compilador para rastrear e contar referências a um objeto, destruindo-o quando ele não for mais necessário, ou seja,
a contagem de referências chega a zero). Isto é muito semelhante ao que acontece com referências de interface no
Delphi em todas as plataformas. A partir do Delphi 10.4 o suporte ao ARC foi removido da linguagem para todas
as plataformas.
É mais interessante olhar para a parte dos dados. Os dados de um programa Object Pascal (como os de
programas escritos na maioria das outras linguagens) são armazenados em três áreas claramente
distintas: a memória global, a pilha e o heap.
A Memória Global
Quando o compilador Object Pascal gera o arquivo executável, ele determina o espaço necessário para
armazenar variáveis que existem durante todo o tempo de vida do programa. Variáveis globais declaradas na
interface, ou nas porções de implementação de uma unidade, enquadram-se neste
categoria. Observe que, se uma variável global for de um tipo de classe (mas também uma string ou uma
matriz dinâmica), apenas uma referência de objeto de 4 ou 8 bytes será armazenada na memória global.
Você pode determinar o tamanho da memória global usando o projeto | Item de menu de informações após
compilar um programa. O campo específico que você deseja examinar é o tamanho dos dados. A Figura
13.1 mostra o uso de quase 50K de dados globais (48.956 bytes), que inclui dados globais do seu
programa e das bibliotecas que ele usa.
Figura 13.1: As
informações sobre um
programa compilado
A memória global às vezes é chamada de memória estática, pois depois que o programa é carregado,
as variáveis permanecem em seu local original, pois a memória nunca é liberada durante a vida útil
do programa.
A pilha
A pilha é uma área de memória dinâmica, que é alocada e desalocada seguindo a ordem LIFO, ou
seja, Último a Entrar, Primeiro a Sair. Isso significa que o último objeto de memória alocado será o
primeiro a ser excluído. Você pode ver uma representação da pilha de memória na Figura 13.2.
A memória de pilha normalmente é usada por chamadas de procedimentos, funções e métodos para
passar parâmetros e seus valores de retorno e para as variáveis locais que você declara em uma
função ou método. Depois que uma chamada de rotina é encerrada, sua área de memória na pilha é
liberada. Lembre-se que Object Pascal por padrão usa convenção de chamada de registro, que
ou seja, os parâmetros são passados nos registros da CPU em vez da pilha sempre que possível.
Observe também que, para economizar tempo, a memória da pilha geralmente não é inicializada nem limpa.
É por isso que se você declarar, digamos, um número inteiro como uma variável local e apenas ler seu valor,
não saberá qual será seu valor. É por isso que todas as variáveis locais precisam ser inicializadas antes
de serem usadas.
O tamanho da pilha geralmente é fixo e determinado pelo processo de compilação. Você pode definir esse
parâmetro na página do vinculador do Projeto | Opções. No entanto, o padrão geralmente é OK. Se você
receber uma mensagem de erro de “estouro de pilha”, provavelmente é porque você tem uma
função que se autodenomina recursivamente para sempre, e não porque o espaço da pilha é muito limitado.
O tamanho inicial da pilha é outra informação fornecida pelo Projeto | Diálogo de informações.
A pilha
O heap é a área na qual a alocação e desalocação de memória acontecem em ordem aleatória. Isso
significa que, se você alocar três blocos de memória em sequência, eles poderão ser destruídos
posteriormente em qualquer ordem. O gerenciador de heap cuida de todos os detalhes, então você
simplesmente solicita uma nova memória com a função GetMem de baixo nível ou chamando um
construtor para criar um objeto, e o sistema retornará um novo bloco de memória para você
(possivelmente reutilizando blocos de memória já descartado). Object Pascal usa o heap para alocar a
memória de cada objeto, o texto das strings, para matrizes dinâmicas e para a maioria das outras
estruturas de dados.
Por ser dinâmico, o heap é a área da memória onde os programas geralmente apresentam mais problemas:
· Cada vez que um objeto é criado, ele precisa ser destruído. Deixar de fazer isso é um cenário
chamado “vazamento de memória”, que não causará muitos danos, a menos que seja repetido
indefinidamente até que a memória heap seja consumida.
· Cada vez que um objeto é destruído, você deve garantir que ele não seja mais usado e que o programa
não tente destruí-lo uma segunda vez.
· O mesmo se aplica a qualquer outra estrutura de dados criada dinamicamente, mas a linguagem
O tempo de execução cuida de strings e arrays dinâmicos de maneira quase automática, então você
quase nunca precisa se preocupar com isso.
para acessar a tabela de métodos virtuais do objeto, mas isso está fora do escopo deste capítulo (apresentarei
isso em breve na seção “Este ponteiro é uma referência de objeto?” do Capítulo 13).
Também vimos como atribuir um objeto a outro apenas faz uma cópia da referência, então você terá duas
referências para um único objeto na memória. Para ter dois objetos completamente separados,
você precisa criar um segundo e copiar os dados do primeiro objeto para ele (uma operação não disponível
automaticamente, pois seus detalhes de implementação podem variar dependendo da estrutura de dados real).
Em termos de codificação, se você escrever o código a seguir, não criará um segundo objeto, mas sim uma
nova referência a um objeto existente:
era
Botão2: Botão T;
começar
Botão2 := Botão1;
Por exemplo, escrevendo este procedimento e chamando-o da seguinte forma, você modificará a legenda
de Button1, ou AButton se preferir:
// Chamar...
ChangeCaption(Button1, 'Olá')
E se você precisar criar um novo objeto? Basicamente, você terá que criá-lo e depois copiar cada uma das
propriedades relevantes. Algumas classes, notadamente a maioria das classes derivadas de TPersistent, e não de
TComponent, definem um método Assign para copiar os dados
de um objeto. Por exemplo, você pode escrever:
ListBox1.Items.Assign(Memo1.Lines);
Mesmo se você atribuir essas propriedades diretamente, o Object Pascal executará código semelhante para você.
Na verdade, o método SetItems conectado à propriedade items da caixa de listagem chama o método Assign
da classe TStringList que representa os itens reais da caixa de listagem.
Então, vamos tentar recapitular o que os vários modificadores de passagem de parâmetros fazem quando aplicados
a objetos:
· Se não houver modificador, você pode fazer qualquer operação no objeto e na variável referente a ele. Você
pode modificar o objeto original, mas se atribuir um novo objeto ao parâmetro, esse novo objeto não terá nada
a ver com o original e com a variável referente a ele.
· Se houver um modificador const , você poderá alterar valores e chamar métodos do objeto, mas não poderá
atribuir um novo objeto ao parâmetro. Observe que não há vantagem de desempenho em passar um objeto
como const.
· Se houver um modificador var , você pode alterar qualquer coisa no objeto e também substituir o objeto original
por um novo no local de chamada, como acontece com outras var
parâmetros. A restrição é que você precisa passar uma referência para uma variável (não uma expressão
geral) e que o tipo de referência deve corresponder exatamente ao tipo de parâmetro.
· Finalmente existe uma opção bastante desconhecida para passar um objeto como parâmetro, chamada
referência constante e escrita como [ref] const. Quando um parâmetro é passado como referência constante
ele se comporta de forma semelhante a uma passagem por referência (var), mas permite mais flexibilidade no
tipo do parâmetro que você está passando, não exigindo uma correspondência exata de tipo (como passar um
objeto de uma subclasse é permitido).
· Cada vez que você cria um objeto, você também deve destruí-lo. Se você não fizer isso, a memória usada
por aquele objeto não será liberada para outros objetos, até que o programa termine.
· Quando você aloca memória para strings, arrays dinâmicos e objetos referenciados por variáveis de
interface (conforme discutido no Capítulo 11), o Object Pascal libera automaticamente a memória
quando a referência sai do escopo. Você não precisa liberar uma string porque, quando ela se torna
inacessível, sua memória é liberada automaticamente. Caso precise liberá-lo mais cedo para liberar
espaço na memória, você pode atribuir a string
ou variável de matriz dinâmica como nula ou atribua uma string vazia à variável de string.
O código usado para criar e destruir um objeto temporário geralmente é encapsulado em um bloco try-
finally , para que o objeto seja destruído mesmo que algo dê errado ao usá-lo:
MeuObj := TMyClass.Create;
tentar
MeuObj.DoAlguma coisa;
finalmente
MeuObj.Free;
fim;
Outro cenário comum é que um objeto seja utilizado por outro, que passa a ser seu
proprietário:
construtor TMyOwner.Create;
começar
FSubObj := TSubObject.Create;
fim;
destruidor TMyOnwer.Destroy;
começar
FSubObj.Free;
fim;
Existem alguns cenários comuns mais complexos, caso o assunto não seja criado até ser necessário
(inicialização lenta) ou possa ser destruído antes do proprietário, caso não seja mais necessário.
Para implementar a inicialização lenta, você não cria o assunto no construtor do proprietário, mas quando
for necessário:
destruidor TMyOnwer.Destroy;
começar
FSubObj.Free;
fim;
Observe que o que foi dito acima depende do fato de que a memória de um objeto é inicializada quando
ele é criado pela primeira vez, o que significa que FSubObj será nulo. Observe também que você não
precisa testar se o objeto está atribuído antes de liberá-lo, porque é exatamente isso que Free faz, como
veremos na próxima seção.
Destroy é um destruidor virtual da classe TObject . A maioria das classes que exigem código de
limpeza personalizado quando os objetos são destruídos substituem esse método virtual. A
razão pela qual você nunca deve definir um novo destruidor é que os objetos geralmente são
destruídos chamando o método Free , e esse método chama o destruidor virtual Destroy
(possivelmente a versão substituída) para você.
Como acabei de mencionar, Free é simplesmente um método da classe TObject , herdado por todas as
outras classes. O método Free basicamente verifica se o objeto atual (Self) não é nulo antes de chamar
o destruidor virtual Destroy .
note Você pode estar se perguntando por que pode chamar Free com segurança se a referência do objeto for nula, mas não pode chamar
Destruir. A razão é que Free é um método conhecido em um determinado local de memória, enquanto
a função virtual Destroy é determinada em tempo de execução observando o tipo do objeto,
uma operação muito perigosa se o objeto não existir mais.
procedimento TObject.Free;
começar
se Self <> nulo então
Destruir;
fim;
A seguir, podemos voltar nossa atenção para a função Assigned . Quando passamos um ponteiro para
esta função, ela simplesmente testa se o ponteiro é nulo. Portanto, as duas afirmações a seguir
são equivalentes, pelo menos na maioria dos casos:
Observe que essas instruções testam apenas se o ponteiro não é nulo; eles não verificam se é um ponteiro
válido. Se você escrever o seguinte código
MeuObj.Free;
se MyObj <> nulo então
MeuObj.DoAlguma coisa;
o teste será avaliado como True e você receberá um erro na linha com a chamada ao método do
objeto. É importante perceber que chamar Free não define o valor de um objeto
referência a zero.
Não é possível definir automaticamente um objeto como nulo . Você pode ter diversas referências ao
mesmo objeto e o Object Pascal não as rastreia. Ao mesmo tempo, dentro de um método (como
Free) , podemos operar no objeto, mas não sabemos nada sobre a referência do objeto – o endereço
de memória da variável que usamos para chamar o método.
Ou seja, dentro do método Free ou qualquer outro método de uma classe, sabemos o endereço de
memória do objeto (Self), mas não sabemos a localização de memória da variável referente ao objeto,
como MyObj. Portanto, o método Free não pode afetar a variável MyObj .
Entretanto, quando chamamos uma função externa passando um objeto como parâmetro de referência, a
função pode então optar por modificar a referência original do objeto. Isso é exatamente o que faz
o procedimento pronto para uso FreeAndNil , um procedimento que podemos usar no lugar de Free e
depois definir a variável de referência como nil. Aqui está o código atual para FreeAndNil:
No passado, o parâmetro era apenas um ponteiro, mas a desvantagem era que você poderia passar,
para o procedimento FreeAndNil , um ponteiro bruto, referências de interface e outras estruturas de dados
incompatíveis. Isso geralmente causava corrupção de memória e bugs difíceis de encontrar. A partir do Delphi
10.4 o código foi modificado conforme mostrado acima, utilizando um parâmetro de referência const do
tipo TObject , limitando os parâmetros a objetos.
note Alguns especialistas em Delphi argumentariam que FreeAndNil nunca deveria ser usado, porque a visibilidade da
variável referente a um objeto deveria corresponder ao seu tempo de vida. Se um objeto possui outro e frees
está no destruidor, não há necessidade de definir a referência como nil pois faz parte de um objeto que você não
vai mais usar. Da mesma forma, uma variável local com um bloco try-finalmente liberando-a não precisa defini-
la como nil , pois está prestes a sair do escopo.
Como observação lateral, além do método Free , TObject também possui um método DisposeOf que é uma sobra
do suporte ARC que a linguagem teve por alguns anos. Atualmente, o método DisposeOf apenas
chama Free.
Para resumir o uso dessas operações de limpeza de memória, aqui estão algumas diretrizes:
· Sempre chame Free para destruir objetos, em vez de chamar o destruidor Destroy .
· Use FreeAndNil ou defina referências de objeto como nulas após chamar Free, a menos que a referência
A influência sairá do escopo imediatamente depois.
Suponha que duas interfaces se referem uma à outra usando um de seus campos e um campo externo.
variável refere-se à primeira. A contagem de referência do primeiro objeto será 2 (o externo
Figura 13.4:
Referências entre
objetos podem formar ciclos,
algo que as
referências fracas explicam
Agora, à medida que a variável externa sai do escopo, as contagens de referência dos dois objetos
são 1; e, como o objeto principal, Objeto1, que possui o Objeto2, não possui proprietário externo,
os objetos permanecerão na memória indefinidamente. Para resolver este tipo de situação, você
deve quebrar as referências circulares, algo longe de ser simples, visto que você não sabe quando
realizar esta operação (ela deve ser realizada quando a última referência externa sair do escopo,
fato dos quais os objetos não têm conhecimento).
A solução para esta situação, e para muitos cenários semelhantes, é usar uma referência fraca.
Conforme mencionado, uma referência fraca é uma referência a um objeto que não aumenta sua
contagem de referências. Tecnicamente, você define uma referência fraca aplicando o atributo [fraco]
para isso.
note Os atributos são um recurso avançado da linguagem Object Pascal abordado no Capítulo 16. Basta dizer que
eles são uma forma de adicionar algumas informações de tempo de execução sobre um símbolo, para que o código externo possa
determinar como lidar com ele.
Dado o cenário anterior, se a referência do segundo objeto de volta ao primeiro for uma referência
fraca (ver Figura 13.5), à medida que a variável externa sai do escopo, ambos os objetos serão
destruídos.
tipo
IMySimpleInterface = interface
'{B6AB548A-55A1-4D0F-A2C5-726733C33708}' ]
[procedimento DoSomething(BRaise: Boolean = False);
função RefCount: Inteiro;
fim;
IMyComplexInterface = interface
'{5E8F7B29-3270-44FC-B0FC-A69498DA4C20}' ]
[função GetSimple: IMySimpleInterface;
função RefCount: Inteiro;
fim;
tipo
TMySimpleClass = classe(TInterfacedObject, IMySimpleInterface)
privado
[Fraco] FOwnedBy: IMyComplexInterface;
público
construtor Create(Proprietário: IMyComplexInterface);
destruidor Destruir; sobrepor;
procedimento DoSomething(BRaise: Boolean = False);
função RefCount: Inteiro;
fim;
construtor TMyComplexClass.Create;
começar
herdado Criar;
FSimple := TMySimpleClass.Create(Self);
fim;
Lembre-se que o campo FOwnedBy é uma referência fraca, portanto não aumenta a contagem de
referências do objeto ao qual se refere, neste caso o objeto atual (Self). Dada esta estrutura de código,
podemos escrever:
Isso não causará vazamento de memória, desde que a referência fraca seja usada. Por exemplo, com
código como:
era
MeuComplexo: IMyComplexInterface;
começar
MeuComplexo := TMyComplexClass.Create;
'
Log('Complex = + MyComplex.RefCount.ToString);
MyComplex.GetSimple.DoSomething(Falso);
Dado que cada construtor e destruidor registra sua execução, você obterá um log como:
Se você remover o atributo fraco do código, verá um vazamento de memória e também (na execução do
código acima) um valor para a contagem de referência de 2 em vez de 1.
O bom de ter gerenciado referências fracas, em comparação com as tradicionais, é que você pode
verificar se uma referência de interface ainda é válida ou não (ou seja, o objeto ao qual ela se refere foi
destruído). Mas isso significa que, quando você estiver usando uma referência fraca, você deve sempre
testar se ela está atribuída antes de usá-la.
privado
[fraco] MySimple: IMySimpleInterface;
Existe também um botão atribuindo uma referência a esse campo, e outro diferente utilizando-o, após
certificar-se de que ainda é válido:
A menos que você modifique o código, o teste "if Assigned" falhará porque o manipulador de eventos
do primeiro botão cria e libera imediatamente os objetos, de modo que a referência fraca se torna nula
(pois agora é inválida). Mas, como é gerenciado, o compilador ajuda a rastrear seu status real (ao
contrário de uma referência a um objeto).
O atributo inseguro
Existem algumas circunstâncias muito específicas (por exemplo, durante a criação de uma instância)
nas quais uma função pode retornar um objeto com uma contagem de referências definida como zero.
Neste caso, para evitar que o compilador exclua o objeto imediatamente (antes que ele tenha a chance
de ser atribuído a uma variável, o que aumentaria sua contagem de referências para 1), nós
tem que marcar o objeto como inseguro.
A implicação aqui é que sua contagem de referências deve ser temporariamente ignorada, para tornar
o código seguro. Esse comportamento é conseguido usando um novo atributo específico,
[Inseguro], um recurso que você deve precisar apenas em circunstâncias muito específicas.
Esta é a sintaxe:
era
[Inseguro] Intf1: IInterface;
O uso deste atributo pode fazer sentido ao implementar padrões de construção, como o padrão de
fábrica, em bibliotecas de uso geral.
note Para suportar o modelo de memória ARC agora obsoleto, a unidade do sistema usou uma diretiva insegura ,
porque não poderia usar o atributo antes de sua definição (posteriormente na mesma unidade). Isso não deve
ser usado em nenhum código fora dessa unidade e não é mais usado (você pode ver isso em um $IFDEF
diretiva).
Nesta seção estou me concentrando nas técnicas que você pode usar para rastrear o uso de
memória, monitorar situações anormais e encontrar vazamentos de memória. Este é um conhecimento
importante para um desenvolvedor, mesmo que não seja estritamente parte da linguagem, mas mais
do suporte ao tempo de execução. Além disso, a implementação do gerenciador de memória depende da plataforma alvo
e sistema operacional, e você pode até conectar um gerenciador de memória personalizado em um
aplicativo Object Pascal (embora isso não seja comum).
Observe que toda a discussão relacionada ao rastreamento do status da memória, aos gerenciadores
de memória e à detecção de vazamentos se refere apenas à memória heap. A pilha e a memória
global são gerenciadas de maneira diferente e você basicamente não tem poder para intervir, mas
essas também são áreas de memória que raramente causam problemas.
Estado da memória
E quanto ao rastreamento do status da memória heap? O RTL oferece algumas funções úteis,
GetMemoryManagerState e GetMemoryMap. Embora o estado do gerenciador de memória seja uma
indicação do número de blocos alocados de vários tamanhos, o mapa de heap é bastante bom
pois representa o status da memória do aplicativo no nível do sistema.
Você pode examinar o status real de cada bloco de memória escrevendo um código como:
csReservado: ...
csSysAlocado: ...
csSysReservado: ...
fim;
fim;
Este código é usado no exemplo ShowMemory para criar uma representação gráfica do status da memória do
aplicativo.
RápidoMM4
Na plataforma Windows, o atual gerenciador de memória Object Pascal é chamado FastMM4 e foi
desenvolvido como um projeto de código aberto principalmente por Pierre La Riche.
Em outras plataformas, o Delphi utiliza o gerenciador de memória nativo da plataforma.
FastMM4 otimiza a alocação de memória, acelerando-a e liberando mais RAM para uso posterior. FastMM4
é capaz de fazer verificações extensivas de memória em
limpeza de memória, no uso incorreto de objetos excluídos, incluindo acesso baseado em interface
para esses dados, em sobregravações de memória e em saturação de buffer. Ele também pode fornecer
algum feedback sobre objetos restantes, ajudando a rastrear vazamentos de memória.
Alguns dos recursos mais avançados do FastMM4 estão disponíveis apenas na versão completa da biblioteca
(abordada na seção “Buffer Overruns no FastMM4 completo”), não na versão incluída no RTL padrão. É por
isso que se você quiser ter os recursos da versão completa, você deve baixar o código-fonte completo em:
https://github.com/pleriche/FastMM4
note Há uma nova versão desta biblioteca chamada FastMM5, que foi otimizada especificamente para
aplicativos multithread e pode funcionar muito melhor em grandes sistemas multinúcleo. A nova versão da
biblioteca está disponível com licença GPL (para projetos de código aberto) ou com licença comercial paga.
licença (vale totalmente o custo) para qualquer outra pessoa. Mais informações estão disponíveis no
leia-me do projeto em https://github.com/pleriche/FastMM5.
A configuração mais fácil de usar é a variável global ReportMemoryLeaksOnShutdown , que permite rastrear
facilmente um vazamento de memória. Você precisa habilitá-lo no início da execução do programa. Então,
quando o programa termina, ele informa se há algum vazamento de memória no seu código (ou em qualquer
uma das bibliotecas que você está usando).
Para demonstrar o relatório e registro padrão de vazamento de memória, escrevi o exemplo simples do
LeakTest . Possui um botão com este manipulador OnClick :
era
P: Ponteiro;
começar
ObterMem(P, 100); fim; // Vazamento de memória!
Este código aloca 100 bytes perdidos ou vazados. Se você executar o programa LeakTest de dentro do
IDE e pressionar o primeiro botão uma vez; então, ao fechar o programa, você receberá uma
mensagem como a da parte superior da Figura 13.6.
Figura 13.6: Os
vazamentos de memória
relatados pelo gerenciador de memória
no Windows após o
encerramento do
aplicativo LeakTest
O programa também aloca alguma memória para um ponteiro global que nunca será liberado, mas ao
registrar esse vazamento potencial conforme esperado, ele não será relatado:
começar
GetMem(GlobalPointer, 200);
RegisterExpectedMemoryLeak(GlobalPointer);
fim;
Novamente, este relatório básico de vazamento está disponível apenas na plataforma Windows, onde
FastMM4 é usado por padrão.
Se você quiser ter mais controle sobre relatórios de vazamentos (como ativar o registro baseado em arquivo),
para ajustar a estratégia de alocação e usar as verificações de memória fornecidas pelo
FastMM4, você precisa baixar a versão completa. Isso consiste no arquivo FastMM4.pas mais o
arquivo de configuração FastMM4Options.inc.
É o último arquivo que você precisa editar para ajustar as configurações, simplesmente comentando e
descomentando um grande número de diretivas. Por convenção, isso é feito colocando
um ponto antes da instrução $DEFINE , transformando-o em um comentário simples, como na primeira destas
duas linhas retiradas do arquivo de inclusão:
Para esta demonstração, ativei as seguintes configurações relevantes, relatadas aqui para dar uma ideia dos
tipos de definições disponíveis:
{$DEFINE FullDebugMode} {$DEFINE LogErrorsToFile} {$DEFINE EnableMemoryLeakReporting} {$DEFINE HideExpectedLeaksRegisteredByPointer} {$DEFINE RequireDebuggerPresenceForLeakReporting}
O programa de teste (na pasta FastMMCode, que também inclui o código-fonte completo da versão do
FastMM4 que utilizei, para sua comodidade) ativa a versão customizada do gerenciador de memória no arquivo
de código-fonte do projeto, configurando-o como a primeira unidade :
programa FastMMCode;
usa
FastMM4 em 'FastMM4.pas',
Formulários,
FastMMForm em 'FastMMForm.pas'; {Formulário 1}
Você também precisará de uma cópia local do arquivo FastMM_FullDebugMode.dll para que funcione.
Este programa de demonstração causa uma saturação do buffer ao obter mais texto do que cabe no
buffer local, pois Length(Caption) é maior que os 5 caracteres fornecidos:
O gerenciador de memória aloca bytes extras no início e no final de cada bloco de memória com valores especiais e
verifica esses valores quando você libera cada bloco de memória. É por isso que você recebe o erro na chamada do
FreeMem . Ao pressionar o botão (no depurador), você verá uma mensagem de erro muito longa, que também é registrada
no arquivo:
FastMMCode_MemoryManager_EventLog.txt
Esta é a saída do erro de saturação com rastreamentos de pilha no momento das operações de alocação e
liberação, além do rastreamento de pilha atual e um despejo de memória (parcial aqui):
FastMM detectou um erro durante uma operação FreeMem. O rodapé do bloco foi corrompido.
O tamanho do bloco é: 5
Rastreamento de pilha de quando este bloco foi alocado (endereços de retorno): 40305E [System]
[System.@GetMem]
44091A [Controles][Controls.TControl.Click]
44431B [Controles][Controls.TWinControl.WndProc]
42D959 [StdCtrls][StdCtrls.TButtonControl.WndProc]
44446C [Controles][Controls.DoControlMsg]
44431B [Controles][Controls.TWinControl.WndProc]
45498A [Formulários][Forms.TCustomForm.WndProc]
443A43 [Controles][Controls.TWinControl.MainWndProc]
41F31A [Classes][Classes.StdWndProc]
76281A10 [GetMessageW]
Rastreamento de pilha de quando o bloco foi liberado anteriormente (endereços de retorno): 40307A [System]
[System.@FreeMem]
42DB8A [StdCtrls][StdCtrls.TButton.CreateWnd]
443863 [Controles][Controls.TWinControl.UpdateShowing]
44392B [Controles][Controls.TWinControl.UpdateControlState]
44431B [Controles][Controls.TWinControl.WndProc]
45498A [Formulários][Forms.TCustomForm.WndProc]
44009F [Controles][Controls.TControl.Perform]
43ECDF [Controles][Controls.TControl.SetVisible]
45F770
76743833 [BaseThreadInitThunk]
Não que isso seja extremamente óbvio, mas deve fornecer informações suficientes para você começar a perseguir
o bug.
Observe que sem essas configurações no gerenciador de memória, você basicamente não verá nenhum erro e
o programa continuará em execução - embora você possa enfrentar bugs aleatórios caso a saturação do buffer
afete uma área da memória na qual algo mais está armazenado. Nesse ponto, você pode obter alguns erros
estranhos e muito difíceis de rastrear.
Por exemplo, uma vez vi a substituição parcial da parte inicial dos dados de um objeto, onde a referência de classe
está armazenada. Através dessa corrupção de memória, a classe tornou-se indefinida e toda e qualquer chamada
para uma de suas funções virtuais travaria gravemente – algo muito difícil de relacionar com uma operação de
gravação de memória em uma área totalmente diferente do programa.
Considerando como o gerenciamento de memória funciona em compiladores Object Pascal, vale a pena considerar
algumas das opções que você tem para garantir que tudo esteja sob controle em outras plataformas. Antes de
prosseguirmos, é importante observar que, em plataformas não Windows, o Delphi não usa o gerenciador de
memória FastMM4, portanto, definir o sinalizador global ReportMemoryLeaksOnShutdown para
verificar vazamentos de memória quando o programa fecha é inútil. Há também outra razão, que é que
geralmente não há um
maneira de fechar um aplicativo no celular, determinados aplicativos permanecem na memória até serem
removidos à força pelo usuário ou pelo sistema operacional.
Nas plataformas macOS, iOS e Android, o Object Pascal RTL chama diretamente o malloc
e funções gratuitas da biblioteca libc nativa . Uma maneira de monitorar o uso de memória em
esta plataforma dependerá de ferramentas de plataforma externa. Por exemplo, em iOS (e macOS)
você pode usar a ferramenta Instruments da Apple, que é um sistema de rastreamento completo que monitora
todos os aspectos de seus aplicativos em execução em um dispositivo físico.
A vantagem é que você pode fazer isso independentemente dos construtores (já que você pode ter mais de
um) e do destruidor, separando claramente o código de rastreamento de memória do código padrão de
inicialização e finalização do objeto.
Embora este seja um caso extremo (provavelmente vale a pena fazer apenas para algumas estruturas de
memória grandes), você pode substituir esses métodos para contar o número de objetos de uma determinada
classe que são criados e destruídos, calcular o número de instâncias ativas e, pelo menos ao final, verifique
se a contagem chega a zero conforme o esperado.
Nesta seção final de um capítulo focado no gerenciamento de memória, decidi listar alguns tópicos um
pouco mais avançados, que ampliam a cobertura anterior. Mesmo que o uso de blocos try-finalmente e
destruidores de chamada já tenha sido abordado, os cenários destacados aqui são um pouco mais
complexos e envolvem o uso conjunto de vários recursos de linguagem.
Esta não é realmente uma seção avançada, mas algo que todos os desenvolvedores Object Pascal
deveriam realmente dominar para serem capazes de escrever aplicações robustas. Apenas a última
subseção sobre ponteiros e referências a objetos é definitivamente mais avançada em escopo, pois se
aprofunda na estrutura interna da memória de um objeto e em uma referência de classe.
note Para seguir o que é uma boa prática de codificação, você geralmente deve adicionar uma chamada de construtor de
classe base em cada construtor do seu código Object Pascal, mesmo que isso não seja obrigatório e a chamada
extra possa ser inútil (como ao herdar TObject.Create) .
Nesta seção quero focar especificamente no que acontece quando um construtor falha em um
cenário clássico como:
MeuObj := TMyClass.Create;
tentar
MeuObj.DoAlguma coisa;
finalmente
MeuObj.Free;
fim;
Isso não é simples de entender na teoria, então vamos dar uma olhada em uma demonstração prática em código. O
O exemplo SafeCode contém uma classe com um construtor e um destruidor que geralmente estará correto – a
menos que o próprio construtor falhe:
tipo
TUnsafeDestructor = classe
privado
Lista FL: ListaTL;
público
construtor Criar (PositiveNumber: Integer);
destruidor Destruir; sobrepor;
fim;
fim;
destruidor TUnsafeDestructor.Destroy;
começar
FList.Limpar;
FList.Free;
herdado;
fim;
O problema não está nos casos em que o objeto foi totalmente criado, mas nos casos em que o
campo FList ainda está definido como nulo, o que aconteceria se o construtor recebesse
um número negativo. Nesse caso, o construtor falharia e o destruidor acionaria e tentaria invocar
a chamada Clear em FList, que terá sido inicializada como nil antes mesmo de o construtor ser
invocado. A tentativa de acessar Clear on nulo gerará uma exceção de “violação de acesso”.
destruidor TUnsafeDestructor.Destroy;
começar
se atribuído (FList) então
FList.Limpar;
FList.Free;
herdado;
fim;
A moral da história, novamente, é que, em um destruidor, nunca tome como certo que o construtor
correspondente inicializou completamente o objeto. Você pode fazer essa suposição para qualquer
outro método, mas não para o destruidor.
finalmente
A2.Livre;
A1.Grátis;
fim;
fim;
Esta é uma versão mais segura e correta do mesmo código (extraído novamente do
Exemplo de SafeCode):
A conversão de tipos de objetos para ponteiros, de e para referências de classe, de objetos para interfaces,
de e para strings é potencialmente muito perigosa, mas difícil de evitar em algumas circunstâncias
especiais. Por exemplo, você pode querer salvar a referência do objeto na propriedade Tag de um
componente, que é um número inteiro, para ser forçado a fazer hardcast.
Outro caso é quando você salva objetos em uma lista de ponteiros usando o método antigo
TList (em vez de uma lista genérica com segurança de tipo, abordada no próximo capítulo).
tentar
List.Add(Ponteiro(Remetente));
List.Add(Ponteiro(23422));
// Elenco direto
TButton(Lista[0]).Caption := 'Ai ';
TButton(List[1]).Caption := finalmente 'Ai' ;
Lista.Grátis;
fim;
fim;
note que escrevi geralmente porque quando você acessa a memória aleatoriamente, você nunca sabe o efeito real. No
Às vezes, os programas simplesmente sobrescrevem a memória sem causar um erro imediato, mas você terá
dificuldade em descobrir por que outros dados foram corrompidos.
Você deve evitar situações semelhantes sempre que possível, mas se não tiver outra alternativa, o que
pode fazer para corrigir esse código? A abordagem natural seria usar
um elenco as safe ou uma verificação de tipo is, como nos seguintes trechos:
// "como" elenco
(TObject(Lista[0]) como TButton).Caption := 'Ai' ;
(TObject(List[1]) como TButton).Caption := 'Ai' ;
// "é" elenco
se TObject(List[0]) for TButton então
TButton(List[0]).Caption := se 'Ai' ;
TObject(List[1]) for TButton então
TButton(Lista[1]).Caption := 'Ai' ;
No entanto, esta não é a solução, você continuará recebendo violações de acesso. O problema é que
ambos is e as acabam chamando TObject.InheritsFrom, uma operação difícil de realizar em um número!
A solução? A solução real é evitar situações semelhantes em primeiro lugar (esse tipo de código
honestamente faz pouco sentido), por exemplo, usando um TObjectList ou alguma outra técnica segura
(novamente, consulte o próximo capítulo para classes genéricas de contêineres). Se você realmente
gosta de hacks de baixo nível e gosta de brincar com ponteiros, você pode tentar descobrir se um
determinado “valor numérico” é realmente uma referência a um objeto ou não. Esta não é uma operação
trivial, no entanto. Há um lado interessante nisso, que tomo como desculpa para a demonstração a
seguir explicar a estrutura interna de um objeto e de uma referência de classe.
pode fornecer aos leitores mais experientes alguns insights interessantes, então decidi manter
este material que escrevi no passado para um artigo avançado sobre gerenciamento de memória.
Observe também que a implementação específica abaixo é realmente específica do Windows em
termos de verificações de memória.
Há momentos em que você tem ponteiros por perto (um ponteiro é apenas um valor numérico que se refere à
localização da memória física de alguns dados). Esses ponteiros podem, na verdade, ser referências a
objetos, e você geralmente sabe quando eles o são e os usa como tal. Mas toda vez que você faz uma conversão
de baixo nível você está prestes a estragar um programa inteiro. Existem técnicas para tornar esse tipo de
gerenciamento de ponteiros um pouco mais seguro, mesmo que não as garanta 100 por cento.
O ponto de partida que você pode querer considerar antes de trabalhar com um ponteiro é se
na verdade, é uma indicação legal ou não. A função Assigned apenas verifica se um ponteiro não é
nulo, o que não ajuda neste caso. No entanto, a função pouco conhecida FindHIn-stance do Object Pascal RTL
(na unidade System , disponível no Windows
plataforma) retorna o endereço base do bloco heap incluindo o objeto passado como parâmetro, ou zero se o
ponteiro se referir a uma página inválida (evitando
mas extremamente difícil rastrear erros de página de memória). Se você pegar um número quase
aleatoriamente, é provável que ele não se refira a uma página de memória válida.
Este é um bom ponto de partida, mas podemos fazer melhor, pois não ajudará se o valor for uma referência
de string ou qualquer outro ponteiro válido e não uma referência de objeto. Agora, como você sabe se um
ponteiro é realmente uma referência a um objeto? Eu criei o seguinte teste empírico. Os primeiros 4 bytes de
um objeto são o ponteiro para sua classe. Se considerarmos a estrutura de dados interna de uma
referência de classe, ela possui em sua posição vmtSelfPtr um ponteiro para si mesma. Isso é representado
aproximadamente na imagem da Figura 13.7.
Resultado := Falso;
se FindHInstance(Endereço) > 0 então
começar
VmtPointer := PByte(Endereço^);
ClassPointer := VmtPointer + vmtSelfPtr;
se atribuído (VmtPointer) e
(FindHInstance(VmtPointer) > 0) então
começar
TamanhoInst := (PInteger(
VmtPointer + VmtInstanceSize))^;
// Verifique o auto-ponteiro e tamanho de instância "razoável"
if Ponteiro(Ponteiro(ClassPointer)^ =
Ponteiro(VmtPointer)) e
(InstSize > 0) e (InstSize < 10000) então
Resultado := Verdadeiro;
fim;
fim;
fim;
note Há uma probabilidade muito alta de que esta função retorne um valor correto, mas não 100% com certeza.
Infelizmente, pode acontecer que dados aleatórios na memória passem nos testes.
Tendo esta função em mãos, no exemplo anterior do SafeCode , podemos adicionar uma verificação de
ponteiro para objeto antes de fazer uma conversão segura:
se IsPointerToObject(Lista[0]) então
(TObject(Lista[0]) como TButton).Caption:= 'Ai' ;
se IsPointerToObject(Lista[1]) então
(TObject(Lista[1]) como TButton).Caption:= 'Ai' ;
A mesma ideia também pode ser aplicada diretamente a referências de classe para implementar
conversões seguras entre elas. Novamente, é melhor tentar evitar problemas semelhantes
escrevendo um código mais seguro e limpo, mas, caso você não possa evitá-lo, o método IsPointerToObject
função pode ser útil. De qualquer forma, esta seção deveria ter explicado um pouco
os internos dessas estruturas de dados do sistema.
Agora que nos aprofundamos nos fundamentos da linguagem e no paradigma da programação orientada a objetos,
é hora de descobrir alguns dos recursos mais recentes e avançados da linguagem Object Pascal. Genéricos,
métodos anônimos e reflexão abrem-se para o desenvolvimento de código usando novos paradigmas que
estendem a programação orientada a objetos de maneiras significativas.
Na verdade, alguns desses recursos de linguagem mais avançados permitem que os desenvolvedores adotem
novas maneiras de escrever código, oferecendo ainda mais tipos e abstrações de código, além de permitir uma
abordagem mais dinâmica à codificação.
A última parte da seção expandirá esses recursos de linguagem, oferecendo uma visão geral dos principais
elementos da biblioteca de tempo de execução, que são tão essenciais para o modelo de
desenvolvimento Object Pascal que tornam a distinção entre linguagem e biblioteca bastante confusa. Iremos
inspecionar, por exemplo, a classe TObject que, como vimos anteriormente, é a base
class de todas as classes que você escreve e tem uma função muito proeminente para ser confinada a um detalhe
de implementação da biblioteca.
14: genéricos
A forte verificação de tipo fornecida pelo Object Pascal é útil para melhorar a correção do código, um
tópico que enfatizei bastante neste livro. A verificação forte de tipo, entretanto, também pode ser um
incômodo, já que você pode querer escrever um procedimento ou uma classe que possa agir de
forma semelhante em diferentes tipos de dados. Esse problema é resolvido por um recurso da
linguagem Object Pas-cal, também disponível em linguagens semelhantes como C# e Java, chamados genéricos
Você pode declarar uma classe sem especificar o tipo de um ou mais membros de dados:
esta operação pode ser adiada até que um objeto daquela classe seja realmente
declarado. Da mesma forma, você pode definir uma função sem especificar o tipo de um ou
mais de seus parâmetros até que a função seja chamada.
note O texto foi extraído do livro “Borland C++ 4.0 Object-Oriented Programming” que escrevi com
Steve Tendon no início dos anos 90.
Este capítulo se aprofunda no tema, começando pelos fundamentos, mas também cobrindo alguns
cenários de uso avançados e até mesmo indicando como os genéricos podem ser aplicados aos padrões.
programação visual.
tipo
TKeyValue = classe
privado
FKey: string;
FValor: TObject;
procedimento SetKey (valor const: string);
procedimento SetValue (valor const: TObject);
público
chave de propriedade: string lida FKey escreve SetKey;
valor da propriedade: TObject lê FValue escreve SetValue;
fim;
Para usar esta classe você pode criar um objeto, definir sua chave e valor e usá-lo, como nos seguintes
trechos de vários métodos do formulário principal do exemplo KeyValueClassic :
// Criar Formulário
Kv := TKeyValue.Create;
// Botão1Clique
Kv.Key := 'minhachave';
Kv.Valor := Remetente;
// Botão2Clique
Kv.Valor := Próprio; // O forma
// Botão3Clique
Mostrar mensagem( '[' + Kv.Chave + ',' + Kv.Valor.ClassName + ']' );
E se você precisar de uma classe semelhante contendo um número inteiro em vez de um objeto? Bem,
ou você faz uma conversão de tipo muito pouco natural (e perigosa) ou cria uma classe nova e separada
para conter uma chave de string com um valor numérico. Embora copiar e colar da classe original
possa parecer uma solução, você acaba com duas cópias de código que serão basicamente iguais e que
vão contra os bons princípios de programação e precisarão de um pesadelo de manutenção, pois você
terá que atualizar cada cópia com novos recursos ou
para corrigir os mesmos bugs em duas, três ou vinte cópias quase idênticas.
Os genéricos tornam possível usar uma definição muito mais ampla para o valor, escrevendo uma única
classe genérica. Depois de instanciar a classe genérica de valor-chave, ela se torna uma classe específica,
vinculada a um determinado tipo de dados. Portanto, você ainda terá duas, três ou vinte classes compiladas
em seu aplicativo, mas terá uma única definição de código-fonte para todas elas que lidam com verificação
de tipo nativa para tipo e sem sobrecarga adicional de tempo de execução.
Mas estou me adiantando. Vamos começar com a sintaxe usada para definir a classe genérica para
nosso par chave-valor:
tipo
TKeyValue<T> = classe
privado
FKey: string;
Valor F: T;
procedimento SetKey (valor const: string);
procedimento SetValue (valor const: T);
público
chave de propriedade: string lida FKey escreve SetKey;
valor da propriedade: T read FValue write SetValue;
fim;
Nesta definição de classe, há um tipo não especificado, indicado pelo espaço reservado T, colocado
entre colchetes angulares. O símbolo T é freqüentemente usado por convenção, mas no que diz respeito
ao compilador você pode usar qualquer símbolo que desejar. Usar T geralmente torna o código mais
legível quando a classe genérica usa apenas um tipo paramétrico; em
caso a classe precise de múltiplos tipos paramétricos é comum nomeá-los de acordo com
sua função real, em vez de usar uma sequência de letras (T, U, V) como era feito em C++ durante
os primeiros dias.
note “T” tem sido o nome padrão, ou espaço reservado, para um tipo genérico desde os dias em que a linguagem C+
+ introduziu modelos no início dos anos 1990. Dependendo dos autores, o “T” significa “Tipo” ou “Tipo de
modelo”. Esta convenção funciona bem no mundo Delphi já que os tipos são em geral prefixados
com T, então faz sentido usar “T” para significar “Tipo”.
A classe genérica TKeyValue<T> usa o tipo não especificado como o tipo de um de seus dois campos,
o valor da propriedade e o parâmetro do método setter. Os métodos são definidos normalmente, mas
observe que, independente de terem a ver com o tipo genérico, sua definição contém o nome
completo da classe, incluindo o tipo genérico:
Em vez disso, para usar a classe, você precisa qualificá-la totalmente, fornecendo o tipo real do tipo
genérico. Por exemplo, agora você pode declarar um objeto de valor-chave que hospeda botões como
valores escrevendo:
era
Kv: TKeyValue<TButton>;
O nome completo também é necessário ao criar uma instância, porque este é o tipo real
nome (enquanto o nome do tipo genérico e não instanciado é como um mecanismo de construção de tipo).
Usar um tipo específico de valor do par chave-valor torna o código muito mais robusto, pois agora você
só pode adicionar objetos TButton (ou derivados) ao par chave-valor e pode então acessar os vários métodos
e propriedades do extraído objeto.
// Criar Formulário
Kv := TKeyValue<TButton>.Create;
// Botão1Clique
Kv.Key := 'a minha chave' ;
Kv.Value := Remetente como TButton;
// Botão2Clique
Kv.Value := Remetente como TButton; // Era "Eu", mas isso é agora inválido!
// Botão3Clique
Mostrar mensagem ( '[' + Kv.Chave + ',' + Kv.Valor.Nome + ']' );
Claro, também podemos imitar o programa original declarando o par chave-valor com um tipo de objeto,
como:
era
Exemplo: TKeyValue<TObject>;
Nesta versão da classe genérica de par chave-valor, podemos adicionar qualquer objeto como valor.
Entretanto, não seremos capazes de fazer muito nos objetos extraídos, a menos que os convertamos
para um tipo mais específico. Para encontrar um bom equilíbrio, você pode querer algo entre botões
específicos e qualquer objeto, solicitando que o valor seja um componente:
era
Kvc: TKeyValue<TComponent>;
era
Kvi: TKeyValue<Inteiro>;
começar
Kvi := TKeyValue<Integer>.Create;
tentar
Kvi.Chave := 'Objeto' ;
kvi.Valor := 100;
Kvi.Value := Esquerda;
'['
ShowMessage (+ Kvi.Key + IntToStr ',' +
(Kvi.Value) + ); ']'
finalmente
Kvi.Grátis;
fim;
começar
var Kvi := TKeyValue<Integer>.Create;
tentar
...
Neste código você não precisa repetir a declaração completa do tipo genérico duas vezes. Isso é
particularmente útil ao usar contêineres, como veremos mais tarde.
tipo
TSimpleGeneric<T> = classe
Valor: T;
fim;
ao declarar um objeto específico com um determinado tipo, você não pode atribuir um tipo diferente ao
campo Valor . Dados os dois objetos a seguir, algumas das tarefas abaixo (parte do
o exemplo TypeCompRules ) estão incorretos:
era
Sg1: TSimpleGeneric<string>;
Sg2: TSimpleGeneric<Integer>;
começar
Sg1 := TSimpleGeneric<string>.Create;
Sg2 := TSimpleGeneric<Integer>.Create;
Sg1.Valor := 'Foo' // ;
Sg1.Valor := 10; Erro
Depois de definir um tipo específico na declaração genérica, isso é imposto pelo compilador,
como seria de esperar de uma linguagem fortemente tipada como Object Pascal. A verificação
de tipo também existe para objetos genéricos como um todo. Ao especificar o parâmetro
genérico para um objeto, você não pode atribuir a ele um tipo genérico semelhante com base em
uma instância de tipo diferente e incompatível. Se isso parece confuso, este exemplo deve ajudar
a esclarecer as coisas:
Como veremos na seção “Regras de compatibilidade de tipos genéricos”, neste caso peculiar, a
regra de compatibilidade de tipos é por estrutura e não por nome de tipo. Você não pode atribuir
um tipo diferente e incompatível a um tipo genérico depois de declarado.
Vimos que quando você define uma classe você pode adicionar um “parâmetro” extra entre
colchetes para ocupar o lugar de um tipo a ser fornecido posteriormente:
tipo
TMyClass<T> = classe
fim;
O tipo genérico pode ser usado como o tipo de um campo (como fiz no exemplo anterior), como o
tipo de uma propriedade, como o tipo de um parâmetro ou valor de retorno de uma função e
muito mais. Observe que não é obrigatório o uso do tipo para um campo local (ou array), pois
há casos em que o tipo genérico é usado apenas como resultado, um parâmetro, ou não é usado
na declaração da classe, mas apenas na definição de alguns de seus métodos.
Essa forma de declaração de tipo estendida ou genérica não está disponível apenas para classes, mas também
para registros (que, como abordei no Capítulo 5, também podem ter métodos, propriedades e
operadores sobrecarregados). Uma classe genérica também pode ter vários tipos parametrizados, como no caso
a seguir, em que você pode especificar um parâmetro de entrada e um valor de retorno de um tipo diferente para
um método:
tipo
TPWGeneric<TInput, TReturn> = classe
público
função AnyFunction(Valor: TInput): TReturn;
fim;
A implementação de genéricos em Object Pascal, como em outras linguagens estáticas, não é baseada em
suporte de tempo de execução. Ele é tratado pelo compilador e pelo vinculador, não deixando quase nada para o
mecanismo de tempo de execução. Ao contrário das chamadas de funções virtuais, que são vinculadas em
tempo de execução, os métodos de classe genéricos são gerados uma vez para cada tipo genérico que você
instancia e são gerados em tempo de compilação! Veremos as possíveis desvantagens desta abordagem,
mas pelo lado positivo ela implica que as classes genéricas são tão eficientes quanto as classes simples.
classes, ou ainda mais eficiente à medida que a necessidade de verificações de tempo de execução é
reduzida. Antes de examinarmos alguns dos aspectos internos, deixe-me focar em algumas regras muito
significativas que quebram as regras tradicionais de compatibilidade de tipos de linguagem Pascal.
Este é um exemplo clássico de incompatibilidade de tipo com matrizes estáticas (parte do exemplo TypeCom-
pRules ):
tipo
TArrayOf10 = array[1..10] de Inteiro;
Matriz3 := Matriz4;
Matriz4 := Matriz1; // Erro
// E2010 Tipos incompatíveis: 'Array' e 'TArrayOf10'
fim;
Como você pode ver no código acima, todos os quatro arrays são estruturalmente idênticos. No entanto, o
O compilador permitirá que você atribua apenas aqueles que são compatíveis com o tipo, seja porque
seu tipo tem o mesmo nome explícito (como TArrayOf10) ou porque eles têm o mesmo nome de
tipo implícito (gerado pelo compilador) que os arrays declarados em uma única instrução.
Esta regra de compatibilidade de tipos tem exceções muito limitadas, como aquelas relacionadas a
classes derivadas. Outra exceção à regra, e significativa, é a compatibilidade de tipos para tipos
genéricos, que provavelmente também é usada internamente pelo compilador para determinar
quando gerar um novo tipo a partir do genérico, com todos os seus métodos.
A nova regra afirma que os tipos genéricos são compatíveis quando compartilham o mesmo nome genérico.
definição de classe e tipo de instância, independentemente do nome do tipo associado a esta definição.
Em outras palavras, o nome completo da instância do tipo genérico é uma combinação do tipo genérico
e do tipo de instância.
tipo
TGenericArray<T> = classe
AnArray: array[1..10] de T;
fim;
TIntGenericArray = TGenericArray<Integer>;
Embora o uso de tipos genéricos para definir classes seja provavelmente o cenário mais comum, os
tipos genéricos também podem ser usados em classes não genéricas. Em outras palavras, uma classe
regular pode ter um método genérico. Nesse caso, você não especifica apenas um tipo para o espaço
reservado genérico ao criar uma instância da classe, mas também ao invocar o método. Aqui está um
exemplo de classe com um método genérico do GenericMethod
exemplo:
tipo
TGenericFunction = classe
público
função WithParam<T>(T1:T): string;
fim;
note Quando escrevi este código pela primeira vez, provavelmente com uma reminiscência dos meus dias de C++, escrevi o parâmetro como
(t: T). Escusado será dizer que em uma linguagem que não diferencia maiúsculas de minúsculas como Object Pascal, esta não é
uma boa ideia. O compilador irá realmente deixá-lo ir, mas emitirá erros toda vez que você se referir ao tipo genérico T.
Não há muito que você possa fazer dentro de um método de classe semelhante (pelo menos a menos que você
use restrições, abordadas posteriormente neste capítulo), então escrevi alguns códigos usando funções especiais
de tipo genérico (novamente abordadas posteriormente) e uma função especial para converta o tipo em uma
string, que não é relevante discutir aqui:
Como você pode ver, esse método nem usa o valor real passado como parâmetro, mas apenas captura algumas
informações de tipo. Mais uma vez, não saber o tipo de T1 torna
bastante complexo para usá-lo no código.
Você pode chamar várias versões desta “função genérica global” da seguinte forma:
era
GF: TGenericFunction;
começar
GF := TGenericFunction.Create;
tentar
Mostrar(GF.WithParam<string>()); 'Foo'
Mostrar(GF.WithParam<Integer>(122));
Mostrar(GF.WithParam()); 'Olá'
Mostrar(GF.WithParam(122));
Mostrar(GF.WithParam(Botão1));
Mostrar(GF.WithParam<TObject>(Button1));
finalmente
GF.Livre;
fim;
Todas as chamadas acima estão corretas, pois o tipo paramétrico pode estar implícito nessas chamadas.
Observe que o tipo genérico é exibido (conforme especificado ou inferido) e não o tipo real do parâmetro, o que
explica esta saída:
corda
Inteiro
corda
CurtoInt
Botão T
TObject
Se você chamar o método sem indicar o tipo entre colchetes angulares, o tipo real será inferido do tipo do
parâmetro. Se você chamar o método com um tipo e um
parâmetro, o tipo do parâmetro deve corresponder à declaração de tipo genérico. Portanto, as três linhas
abaixo não serão compiladas:
Show(GF.WithParam<Integer>()); 'Foo'
Mostrar(GF.WithParam<string>(122));
Mostrar(GF.WithParam<TButton>(Self));
Com exceção de algumas otimizações, toda vez que você instancia um tipo genérico, seja em um
método ou em uma classe, um novo tipo é gerado pelo compilador. Este novo tipo não compartilha
código com diferentes instâncias do mesmo tipo genérico (ou diferentes versões do mesmo método).
Vejamos um exemplo (que faz parte do exemplo GenericCodeGen ). O programa possui uma classe
genérica definida como:
tipo
TSampleClass<T> = classe
privado
Dados F: T;
público
procedimento Um;
função LeituraT: T;
procedimento SetT(Valor: T);
fim;
Os três métodos são implementados da seguinte forma (observe que o método One é absolutamente
independente do tipo genérico):
procedimento TSampleClass<T>.One;
começar
'UmT'
Form30.Mostrar(fim; );
função TSampleClass<T>.ReadT: T;
começar
Resultado := DadosF;
fim;
Agora, o programa principal usa o tipo genérico principalmente para descobrir o endereço na
memória de seus métodos, uma vez que uma instância é gerada pelo compilador. Este é o código
T2 := TSampleClass<string>.Create;
T2.SetT(); 'Olá'
T2.Um;
' +
'T1.SetT:
Show(IntToHex(PInteger(@TSampleClass<Integer>.SetT)^,
' + 8));
'T2.SetT:
Show(IntToHex(PInteger(@TSampleClass<string>.SetT)^, 8));
' +
'T1.Um:
Show(IntToHex(PInteger(@TSampleClass<Integer>.One)^,
' + 8));
'T2.Um:
Show(IntToHex(PInteger(@TSampleClass<string>.One)^, 8));
fim;
T1.ConjuntoT: C3045089
T2.ConjuntoT: 51EC8B55
T1.Um: 4657F0BA
T2.Um: 46581CBA
Como previ, não apenas o método SetT obtém uma versão diferente na memória gerada pelo
compilador para cada tipo de dados usado, mas até mesmo o método One obtém, apesar de
serem todos idênticos.
Além disso, se você declarar novamente um tipo genérico idêntico, obterá um novo conjunto de
funções de implementação. Da mesma forma, a mesma instância de um tipo genérico usada
em unidades diferentes força o compilador a gerar o mesmo código repetidamente, possivelmente
causando um inchaço significativo do código. Por esta razão se você tiver uma classe genérica
com muitos métodos que não dependem do tipo genérico, é recomendado definir uma classe base
não genérica com esses métodos comuns e uma classe genérica herdada com os métodos
genéricos: desta forma o os métodos da classe base são compilados e incluídos no executável apenas uma
note Atualmente há trabalho de compilador, vinculador e RTL de baixo nível sendo feito para reduzir o aumento de tamanho
causados por genéricos em cenários como os descritos nesta seção. Veja, por exemplo, as
considerações em http://delphisorcery.blogspot.it/2014/10/new-language-feature-in-xe7.html.
· Default (T) é na verdade uma nova função introduzida junto com os genéricos que retorna o valor
vazio ou “valor zero” ou nulo para o tipo atual; pode ser zero, uma string vazia, nil e assim
por diante; a memória inicializada com zero tem o mesmo valor de uma variável global do
mesmo tipo (diferentemente das variáveis locais, na verdade, as globais são inicializadas em
“zero” pelo compilador)
· TypeInfo (T) retorna o ponteiro para as informações de tempo de execução da versão atual
do tipo genérico; você encontrará muito mais informações sobre informações de tipo em
Capítulo 16
· SizeOf (T) retorna o número de bytes do tipo (que no caso de uma referência-
tipo, como uma string, ou um objeto, seria o tamanho da referência, ou seja, 4 bytes para um
compilador de 32 bits e 8 bytes para um compilador de 64 bits)
· IsManagedType(T) indica se o tipo é gerenciado em memória, como acontece com
strings e matrizes dinâmicas
· HasWeakRef(T) está vinculado a compiladores habilitados para ARC e indica se o tipo de destino
possui referências fracas; que requer suporte específico de gerenciamento de memória
· GetTypeKind(T) é um atalho para acessar o tipo de tipo a partir das informações de tipo; que é
uma definição de tipo de nível ligeiramente superior ao retornado por TypeInfo
note Todos esses métodos retornam constantes avaliadas pelo compilador em vez de chamar funções reais em tempo
de execução. A importância disso não está no fato de essas operações serem muito rápidas, mas sim no fato
de possibilitar ao compilador e ao vinculador otimizar o código gerado, removendo ramificações não utilizadas. Se
você tem um case ou uma instrução if baseada no valor de retorno de uma dessas funções, o compilador pode
descobrir que, apenas para um determinado tipo, uma das ramificações será executada, removendo o código
inútil. Quando o mesmo método genérico é compilado para um tipo diferente, ele pode acabar usando uma
ramificação diferente, mas novamente o compilador pode descobrir antecipadamente e otimizar o tamanho do método.
O exemplo GenericTypeFunc possui uma classe genérica que mostra as três funções de tipo genérico em
ação:
tipo
TSampleClass<T> = classe
privado
Dados F: T;
público
procedimento Zero;
função GetDataSize: Inteiro;
função GetDataName: string;
fim;
procedimento TSampleClass<T>.Zero;
começar
FData := Padrão(T);
fim;
T2 := TSampleClass<string>.Create;
'Dados: '
Mostrar(+T2.FDados);
T3 := TSampleClass<Duplo>.Create;
'
'Dados:
Mostrar(+ FloatToStr(T3.FData));
Dados: 0
Tipo: Inteiro
Tamanho: 4
TSampleClass<string>
Dados:
Tipo: string
Tamanho: 4
TSampleClass<Duplo>
Dados: 0
Tipo: Duplo
Tamanho: 8
Observe que você também pode usar funções de tipo genérico em tipos específicos, fora do contexto
de classes genéricas. Por exemplo, você pode escrever:
era
Eu: Inteiro;
s: corda;
começar
I := Padrão (Inteiro); '
Mostrar( 'Inteiro padrão : + IntToStr(I));
s := Padrão(string); '
Mostrar( 'Sequência padrão : +s);
'
Mostrar( 'String TypeInfo : + GetTypeNome(TypeInfo(string));
Inteiro padrão: 0
Sequência padrão:
String TypeInfo: string
note Você não pode aplicar a chamada TypeInfo a uma variável, como TypeInfo(s) no código acima, mas apenas a um
tipo de dados.
Um caso muito interessante surge quando você define um construtor de classe para uma classe
genérica. Na verdade, um desses construtores é gerado pelo compilador e chamado para cada classe genérica
instância, isto é, para cada tipo real definido usando o modelo genérico. Isso é bastante interessante,
porque seria muito complexo executar código de inicialização para cada instância real da classe
genérica que você criará em seu programa sem um construtor de classe.
Por exemplo, considere uma classe genérica com alguns dados de classe. Você obterá uma instância
desses dados de classe para cada instância de classe genérica. Se você precisar atribuir um valor
inicial aos dados desta classe, não poderá usar o código de inicialização da unidade, pois na unidade
que define a classe genérica você não sabe quais classes reais serão necessárias.
A seguir está um exemplo básico de uma classe genérica com um construtor de classe usado para
inicialize o campo da classe DataSize , obtido do exemplo GenericClassCtor :
tipo
TGenericWithClassCtor<T> = classe
privado
Dados F: T;
procedimento SetData (valor const: T);
público
construtor de classe Criar;
Dados de propriedade: T ler FData escrever SetData;
a aula era
DataSize: Inteiro;
fim;
Este é o código do construtor de classe genérico, que usa uma lista de strings interna (veja o código-
fonte completo para detalhes de implementação) para controlar quais construtores de classe são
realmente chamados:
O programa de demonstração cria e usa algumas instâncias da classe genérica e também declara o tipo
de dados para uma terceira, que é removida pelo vinculador:
era
GenInt: TGenericWithClassCtor<SmallInt>;
GenStr: TGenericWithClassCtor<string>;
tipo
TGenDouble = TGenericWithClassCtor<Duplo>;
Se você pedir ao programa para mostrar o conteúdo da lista de strings ListSequence , você verá apenas
os tipos que foram realmente inicializados:
TGenericWithClassCtor<System.SmallInt>
TGenericWithClassCtor<System.string>
Entretanto, se você criar instâncias genéricas baseadas no mesmo tipo de dados, em unidades
diferentes, o vinculador poderá não funcionar conforme o esperado e você terá vários construtores de
classes genéricas para o mesmo tipo.
observação Não é fácil resolver esse problema. Para evitar uma inicialização repetida, você pode querer
verificar se o construtor da classe já foi executado. Em geral, porém, esse problema é parte de uma
limitação mais abrangente das classes genéricas e da incapacidade dos linkers de otimizá-las.
Adicionei um procedimento chamado Useless na segunda unidade deste exemplo que, quando
descomentado, irá destacar o problema, com a seguinte sequência de inicialização:
TGenericWithClassCtor<System.string>
TGenericWithClassCtor<System.SmallInt>
TGenericWithClassCtor<System.string>
Restrições genéricas
Como vimos, há muito pouco que você possa fazer nos métodos da sua classe genérica em relação ao
valor do tipo genérico. Você pode distribuí-lo (ou seja, atribuí-lo) e executar as operações limitadas
permitidas pelas funções de tipo genérico que abordei acima.
Para poder executar algumas operações reais do tipo genérico de classe, geralmente é necessário colocar
uma restrição nela. Por exemplo, se você limitar o tipo genérico a uma classe, o compilador permitirá que
você chame todos os métodos TObject nele. Você também pode restringir ainda mais a classe para que
faça parte de uma determinada hierarquia ou para implementar uma interface específica, tornando
possível chamar a classe ou o método de interface em uma instância do tipo genérico.
Restrições de classe
A restrição mais simples que você pode adotar é uma restrição de classe. Para usá-lo, você pode declarar
um tipo genérico como este:
tipo
TSampleClass<T:classe> =classe
Ao especificar uma restrição de classe, você indica que só pode usar tipos de objetos como tipos
genéricos. Com a seguinte declaração (retirada do exemplo ClassConstraint ):
tipo
TSampleClass<T:classe> =classe
privado
Dados F: T;
público
procedimento Um;
função LeituraT: T;
procedimento SetT(T1:T);
fim;
Amostra1: TSampleClass<TButton>;
Amostra2: TSampleClass<TStrings>;
Amostra3: TSampleClass<Integer>; // Erro
Qual é a vantagem de indicar essa restrição? Nos métodos de classe genéricos agora você
pode chamar qualquer método TObject , inclusive os virtuais! Este é o método One da classe
genérica TSampleClass :
procedimento TSampleClass<T>.One;
começar
se atribuído (FData) então
começar
'
'Nome da classe: + FData.ClassName);
'
'Tamanho: + IntToStr(FData.InstanceSize));
'
'Para sequenciar: + FData.ToString);
Form30.Show(Form30.Show(Form30.Show(end;
fim;
note Dois comentários aqui. A primeira é que InstanceSize retorna o tamanho real do objeto, ao contrário da
função genérica SizeOf que usamos anteriormente, que retorna o tamanho do tipo de referência.
Segundo, observe o uso do método ToString da classe TObject .
Você pode brincar com o programa para ver seu efeito real, pois ele define e usa algumas
instâncias do tipo genérico, como no seguinte trecho de código:
era
Amostra1: TSampleClass<TButton>;
começar
Amostra1 := TSampleClass<TButton>.Create;
tentar
Sample1.SetT(Remetente como TButton);
Amostra1.Um;
finalmente
Amostra1.Grátis;
fim;
Observe que ao declarar uma classe com um método ToString customizado , esta versão será
chamada quando o objeto de dados for do tipo específico, independentemente do tipo real
fornecido ao tipo genérico. Em outras palavras, se você tiver um descendente de TButton como:
tipo
TMyButton = classe(TButton)
público
função ToString: string; sobrepor;
fim;
Você pode passar este objeto como valor de um TSampleClass<TButton> ou definir uma
instância específica do tipo genérico, e em ambos os casos chamar One acaba executando a
versão específica de ToString:
era
Amostra1: TSampleClass<TButton>;
Amostra2: TSampleClass<TMyButton>;
MB: TMyButton;
começar
...
Amostra1.SetT(Mb);
Amostra1.Um;
Amostra2.SetT(Mb);
Amostra2.Um;
Da mesma forma que uma restrição de classe, você pode ter uma restrição de registro, declarada como:
tipo
TSampleRec<T: registro> = classe
Entretanto, há muito pouco que registros diferentes tenham em comum (não há ancestral comum), portanto esta
declaração é um tanto limitada.
tipo
TCompClass<T: TComponent> = classe
instâncias desta classe genérica podem ser aplicadas apenas a classes de componentes, ou seja, qualquer classe
descendente de TComponent . Isso permite que você tenha um tipo genérico muito específico (sim, parece
estranho, mas é o que realmente é) e o compilador permitirá que você use todos os métodos da classe
TComponent enquanto trabalha no tipo genérico.
Se isso parece extremamente poderoso, pense duas vezes. Se você considerar o que pode alcançar com herança
e regras de compatibilidade de tipos, poderá resolver o mesmo problema usando técnicas tradicionais orientadas
a objetos, em vez de usar classes genéricas. Não estou dizendo que uma restrição de classe específica nunca seja
útil, mas certamente não é tão poderosa quanto uma restrição de classe de nível superior ou (algo que considero
muito interessante) uma restrição baseada em interface.
Restrições de interface
Em vez de restringir uma classe genérica a uma determinada classe, geralmente é mais flexível aceitar, como
parâmetro de tipo, apenas classes que implementem uma determinada interface. Isto torna possível chamar a
interface em instâncias do tipo genérico. Esse uso de restrições de interface para genéricos também é muito comum
na linguagem C#. Deixe-me começar mostrando
você um exemplo (do exemplo IntfConstraint ). Primeiro, precisamos declarar uma interface:
tipo
IGetValue = interface
'{60700EC4-2CDA-4CD1-A1A2-07973D9D2444}' ]
[função GetValue: Inteiro; procedimento
SetValue(Valor: Inteiro); valor da propriedade: leitura
inteira GetValue gravação SetValue; fim;
tipo
TGetValue = classe(TNoRefCountObject, IGetValue) privado
As coisas começam a ficar interessantes na definição de uma classe genérica limitada aos tipos que
implementam a interface dada:
tipo
TInftClass<T: IGetValue> = classe privada
Observe que, no código dos métodos genéricos desta classe, podemos, por exemplo, escrever:
Com todas essas definições, podemos agora usar a classe genérica da seguinte forma:
IClass: TInftClass<TGetValue>;
começar
IClass := TInftClass<TGetValue>.Create;
tentar
IClass.Set1(TGetValue.Create(5));
IClass.Set2(TGetValue.Create(25));
'
Mostrar(+IntToStr(IClass.GetAverage));
'Média:
IClass.IncreaseByTen;
'
'Mínimo:
Mostrar(+IntToStr(IClass.GetMin));
finalmente
IClass.FVal1.Free;
IClass.FVal2.Free;
IClass.Free;
fim;
fim;
Para mostrar a flexibilidade desta classe genérica, criei outra implementação totalmente diferente para a
interface:
tipo
TButtonValue = classe(TButton, IGetValue)
público
função GetValue: Inteiro;
procedimento SetValue(Valor: Inteiro);
função de classe MakeTButtonValue (Proprietário: TComponent;
Pai: TWinControl): TButtonValue;
fim;
A função de classe (não mostrada aqui) cria um botão dentro de um controle Parent em uma posição
aleatória e é usada no seguinte código de exemplo:
fim;
fim;
No último exemplo definimos uma classe genérica que funciona com qualquer objeto que implemente
uma determinada interface. Eu poderia ter obtido um efeito semelhante criando uma classe padrão (não
genérica) baseada em referências de interface. Na verdade, eu poderia ter definido uma classe como
(novamente parte do exemplo IntfConstraint ):
tipo
TplainInftClass = classe
privado
FVal1, FVal2: IGetValue;
público
procedimento Set1(Val: IGetValue);
procedimento Set2(Val: IGetValue);
função GetMin: Inteiro;
função GetAverage: Inteiro;
procedimento AumentaByTen;
fim;
O que há de diferente entre essas duas abordagens? Uma primeira diferença é que, na classe acima,
você pode passar dois objetos de tipos diferentes para os métodos setter, desde que suas classes
implementem a interface dada, enquanto na versão genérica você pode passar apenas objetos do tipo
dado ou derivado de esse tipo (para qualquer instância da classe genérica). Portanto a versão genérica
é mais conservadora e rigorosa em termos de verificação de tipo.
Na minha opinião, a principal diferença é que, usar a versão baseada em interface, significa ter o
mecanismo de contagem de referências do Object Pascal em ação, enquanto, usando a versão genérica,
a classe está lidando com objetos simples de um determinado tipo e contagem de referências. não está
envolvido.
Além disso, a versão genérica pode ter múltiplas restrições, como uma restrição de construtor e permite
usar várias funções genéricas (como solicitar o tipo real do tipo genérico), algo que você não pode fazer ao
usar uma interface. (Quando você está trabalhando com uma interface, na verdade, você não tem como
fazer referência aos métodos básicos do TObject ).
Em outras palavras, usar uma classe genérica com uma restrição de interface possibilita obter os
benefícios das interfaces sem seus incômodos. Ainda assim, vale a pena notar que na maioria dos casos
as duas abordagens seriam equivalentes, e noutros a solução baseada em interface seria mais flexível.
Existe outra possível restrição de tipo genérico, chamada construtor padrão ou construtor sem
parâmetros. Se você precisar invocar o construtor padrão para criar um novo objeto do tipo
genérico, por exemplo, para preencher uma lista, poderá usar esta restrição. Em teoria, e de
acordo com a documentação, o compilador deveria permitir que você o usasse apenas para
os tipos com um construtor padrão. Na prática, se não existir um construtor padrão, o
compilador irá deixá-lo ir e chamar o construtor padrão de TObject.
Uma classe genérica com uma restrição de construtor pode ser escrita da seguinte
forma (esta é extraída pelo exemplo IntfConstraint ):
tipo
TConstrClass<T: classe, construtor> = classe
privado
ValorF: T;
público
construtor Criar;
função Obter: T;
fim;
note Você também pode especificar a restrição do construtor sem a restrição da classe, pois possui um construtor
implica que o tipo é uma classe. Listar os dois torna o código mais legível.
Dada esta declaração, você pode usar o construtor para criar um objeto interno genérico,
sem saber seu tipo real antecipadamente, e escrever:
construtor TConstrClass<T>.Create;
começar
FVal := T.Criar;
fim;
Como podemos usar esta classe genérica e quais são as regras reais? No próximo exemplo
defini duas classes, uma com um construtor padrão sem parâmetros, a segunda com
um único construtor com um parâmetro:
tipo
TSimpleConst = classe
público
FValor: Inteiro;
construtor Criar; fim; // Definir Valor para 10
TParamConst = classe
público
FValor: Inteiro;
construtor Criar (I: Inteiro); fim; // Definir Valor para EU
Como mencionei anteriormente, em teoria você só deveria conseguir usar a primeira classe, enquanto na prática
você pode usar ambas:
era
ConstruirObj: TConstrClass<TSimpleCost>;
ParamCostObj: TConstrClass<TParamCost>;
começar
ConstructObj := TConstrClass<TSimpleCost>.Create;
'
'Valor
Mostrar(+ 1:
IntToStr(ConstructObj.Get.FValue));
ParamCostObj := TConstrClass<TParamCost>.Create;
'
'Valor
Mostrar(+ 2:
IntToStr(ParamCostObj.Get.FValue));
Valor 1: 10
Valor 2: 0
note Mesmo que uma chamada direta para TParamConst.Create falhe em tempo de compilação (conforme explicado aqui), uma chamada semelhante
usar uma referência de classe, ou qualquer outra forma de indireção, terá sucesso; o que provavelmente explica o
comportamento do efeito da restrição do construtor.
tipo
TSampleClass<T:classe> =classe
TSampleRec<T: registro> = classe
TCompClass<T: TButton> = classe
TInftClass<T: IGetValue> = classe
TConstrClass<T: construtor> = classe
O que você pode não perceber imediatamente depois de observar as restrições (e levei algum tempo para me
acostumar) é que você pode combiná-las. Por exemplo, você pode definir uma classe genérica limitada a uma
subhierarquia e exigindo também uma determinada interface, como em:
tipo
Nem todas as combinações fazem sentido: por exemplo, você não pode especificar uma classe e um registro
ao mesmo tempo, enquanto usar uma restrição de classe combinada com uma restrição de classe específica
seria redundante. Finalmente, observe que não há nada como uma restrição de método, algo que pode ser
alcançado com uma restrição de interface de método único (embora muito mais complexa de expressar).
Desde os primórdios dos modelos na linguagem C++, um dos usos mais óbvios das classes de modelos tem sido
a definição de contêineres de modelos, ou listas, até o ponto em que a linguagem C++ definiu uma Biblioteca de
Modelos Padrão (ou STL).
Quando você define uma lista de objetos, como o próprio TObjectList do Object Pascal, você tem uma lista que
pode potencialmente conter objetos de qualquer tipo. Usando herança ou composição, você pode definir
contêineres personalizados para um tipo específico, mas essa é uma abordagem tediosa (e potencialmente
propensa a erros).
Os compiladores Object Pascal vêm com um pequeno conjunto de classes de contêiner genéricas que você pode
encontrar na unidade Generics.Collections . As quatro classes principais de contêineres são todas implementadas
de maneira independente (não há herança entre essas classes), todas implementadas de maneira semelhante
(usando um array dinâmico) e todas mapeadas para a classe de contêiner não genérica correspondente da
unidade Contnrs mais antiga :
tipo
TList<T> = classe
TQueue<T> = classe
TStack<T> = classe
TDictionary<K, V> = classe
TObjectList<T: classe> = classe(TList<T>)
TObjectQueue<T: classe> = classe(TQueue<T>)
TObjectStack<T: classe> = classe(TStack<T>)
TObjectDictionary<K, V> = classe(TDictionary<K, V>)
A diferença lógica entre essas classes deveria ser bastante óbvia considerando seus nomes. Uma boa maneira
de testá-los é ver quantas alterações você precisa realizar no código existente que usa uma classe de contêiner
não genérica em comparação com o uso da versão genérica para diferentes tipos de dados.
note O programa discutido a seguir, ListDemoMd2005, usa apenas alguns métodos, portanto não é um ótimo
teste para compatibilidade de interface entre listas genéricas e não genéricas, mas decidi usar um
programa existente em vez de fabricar um. Outra razão para mostrar esta demonstração é que você
também pode ter programas existentes que não usam classes de coleção genéricas e será incentivado a aprimorá-lo
aproveitando esse recurso de idioma.
Usando TList<T>
O programa, denominado ListDemoMd2005, possui uma unidade que define uma classe TDate , e o
formulário principal utilizado para se referir a uma TList de datas. Como ponto de partida, adicionei uma
cláusula de uso referente a Generics.Collections, depois alterei a declaração do campo do formulário principal
para:
privado
FListDate: TList<TDate>;
É claro que o manipulador de eventos OnCreate do formulário principal que cria a lista também precisava
ser atualizado, tornando-se:
Agora podemos tentar compilar o resto do código como está. O programa tem um bug “procurado”,
tentando adicionar um objeto TButton à lista. O código correspondente usado para compilar e agora falha:
A nova lista de datas é mais robusta em termos de verificação de tipo do que a lista genérica original de
ponteiros. Tendo removido essa linha, o programa compila e funciona. Ainda assim, pode
ser melhorado.
Este é o código original usado para exibir todas as datas da lista em um controle ListBox:
era
Eu: Inteiro;
começar
ListBox1.Limpar;
para I := 0 para ListDate.Count - 1 faça
ListBox1.Items.Add((TObject(FListDate[I]) como TDate).Texto);
Observe a conversão do tipo, devido ao fato do programa estar utilizando uma lista de ponteiros
(TList), e não uma lista de objetos (TObjectList).
Outra melhoria neste trecho pode vir do uso de uma enumeração (algo
as listas genéricas predefinidas suportam totalmente) em vez de um loop for simples :
era
Data AD: DataTD;
começar
para ADate em FListDate faça
começar
ListBox1.Items.Add(ADate.Text);
fim;
Finalmente, o programa pode ser melhorado usando um TObjectList genérico que possui os objetos
TDate , mas isso é assunto para a próxima seção.
Como mencionei anteriormente, a classe genérica TList<T> possui um alto grau de compatibilidade. Isto
possui todos os métodos clássicos, como Add, Insert, Remove e IndexOf. As propriedades Capacidade e Contagem
também estão lá. Estranhamente, Items se torna Item, mas sendo a propriedade padrão (acessada usando colchetes
sem o nome da propriedade), você raramente se refere explicitamente a ela de qualquer maneira.
Classificando um TList<T>
O que é interessante entender é como funciona a classificação TList<T> (meu objetivo aqui é adicionar
suporte de classificação ao exemplo ListDemoMd2005 ). O método Sort é definido como:
O que precisamos fazer, em vez disso, é definir uma implementação adequada do IComparer<T>
interface. Para compatibilidade de tipos, precisamos definir uma implementação que funcione na classe
TDate específica .
Existem diversas maneiras de fazer isso, incluindo o uso de métodos anônimos (abordados brevemente
na próxima seção, mas abordados na íntegra no próximo capítulo). Uma técnica interessante, também
porque me dá a oportunidade de mostrar vários padrões de uso de genéricos, é aproveitar o que chamo
de uma classe estrutural que faz parte da unidade Generics.Defaults e se chama TComparer.
note que estou chamando essa classe de estrutural porque ela ajuda a definir a estrutura do código, sua arquitetura, mas não
acrescenta muito em termos de implementação real. Uma classe semelhante é frequentemente chamada de classe
de estrutura, embora isso geralmente implique um design mais complexo.
tipo
TComparer<T> = classe(TInterfacedObject, IComparer<T>)
público
função de classe Padrão: IComparer<T>;
função de classe Construir(
Comparação const: TComparison<T>): IComparer<T>;
função Comparar(
const Esquerda, Direita: T): Inteiro; virtual; abstrato;
fim;
O que precisamos fazer é instanciar essa classe genérica para o tipo de dados específico (TDate, no
exemplo) e também herdar uma classe concreta que implemente o método Compare para o tipo específico.
As duas operações podem ser feitas ao mesmo tempo, usando uma linguagem de codificação que pode
demorar um pouco para ser digerida:
tipo
TDateComparer = class(TComparer<TDate>)
função Comparar(
const Esquerda, Direita: TDate): Inteiro; sobrepor;
fim;
Se você acha que esse código parece muito incomum, você não está sozinho. A nova classe herda de uma
instância específica da classe genérica, algo que você poderia expressar em duas etapas separadas
como:
tipo
TAnyDateComparer = TComparer<TDate>;
TMyDateComparer = classe(TAnyDateComparer)
função Comparar(
const Esquerda, Direita: TDate): Inteiro; sobrepor;
fim;
note Ter as duas declarações separadas pode ajudar a reduzir o código gerado onde você está reutilizando o
tipo base TAnyDateComparer na mesma unidade.
Você pode encontrar a implementação real da função Compare no código-fonte, pois esse não é o ponto principal
que quero enfatizar aqui. Tenha em mente, porém, que mesmo se você ordenar a lista, seu método IndexOf
não tirará vantagem disso (ao contrário do método TStringList
aula).
note que sugiro que você dê uma olhada nesta seção mesmo que não saiba muito sobre métodos anônimos, e
em seguida, leia-o novamente depois de passar pelo próximo capítulo que aborda detalhadamente os métodos anônimos.
O parâmetro IComparer<T> do método Sort da classe TList<T> , na verdade, pode ser usado chamando o método
Construct de TComparer<T>, passando um método anônimo
como um parâmetro definido como:
tipo
TComparison<T> = referência à função(
const Esquerda, Direita: T): Inteiro;
Na prática você pode escrever uma função compatível com o tipo e passá-la como parâmetro:
note O método DoCompare acima funciona como um método anônimo, mesmo que tenha um nome. Veremos
em um trecho de código posterior que isso não é necessário. Tenha paciência até o próximo capítulo para obter mais
informações sobre esta construção da linguagem Object Pascal. Observe também que com um registro TDate eu
poderia ter definido os operadores menor que e maior que, tornando esse código mais simples, mas mesmo com uma
classe eu poderia ter colocado o código de comparação em um método da classe.
Se isso parecer bastante tradicional, considere que você poderia ter evitado a declaração de uma função separada
e passar a implementação da função na forma de código-fonte como parâmetro para o método Construct ,
como segue:
Este exemplo deve ter aguçado seu apetite para aprender mais sobre métodos anônimos! Com
certeza, esta última versão é muito mais simples de escrever do que a comparação original abordada na
seção anterior, embora para muitos desenvolvedores de Object Pascal tenham
uma classe derivada pode parecer mais limpa e mais fácil de entender (a versão herdada separa
melhor a lógica, facilitando a reutilização potencial do código, mas muitas vezes você não a utilizará
de qualquer maneira).
Contêineres de objetos
Além das classes genéricas abordadas no início desta seção, há também quatro classes genéricas
herdadas que são derivadas das classes base definidas na unidade Generic-s.Collections , imitando as
classes existentes da unidade Contnrs (que significa containers) :
tipo
TObjectList<T: classe> = classe(TList<T>)
TObjectQueue<T: classe> = classe(TQueue<T>)
TObjectStack<T: classe> = classe(TStack<T>)
Em comparação com suas classes base, existem duas diferenças principais. Uma é que esses tipos
genéricos podem ser usados apenas para objetos; a segunda é que eles definem um método de
notificação customizado , que, no caso de um objeto ser removido da lista (além de chamar
opcionalmente o manipulador de eventos OnNotify ), irá liberar o objeto.
Em outras palavras, a classe TObjectList<T> se comporta como sua contraparte não genérica
quando a propriedade OwnsObjects é definida. Se você está se perguntando por que isso não é mais
uma opção, considere que TList<T> agora pode ser usado diretamente para trabalhar com tipos de
objetos, ao contrário de sua contraparte não genérica.
Há também uma quarta classe, novamente, chamada TObjectDictionary<K, V>, que é definida de
uma maneira diferente, pois pode possuir o objeto-chave, os objetos de valor ou ambos. Consulte o
conjunto TDictionaryOwnerships e o construtor da classe para obter mais detalhes.
note Dicionário, neste caso, significa uma coleção de elementos, cada um com um valor-chave exclusivo referente a ele. Um
dicionário também é conhecido como array associativo. Em um dicionário tradicional você tem palavras atuando
como chaves para suas definições, mas em termos de programação a chave não precisa ser uma string (mesmo que
este seja um caso bastante frequente). Nas versões anteriores das coleções genéricas do Delphi, os marcadores de
posição do tipo dicionário eram indicados em TKey e TValue. Entretanto, dado que TValue é um tipo de dados
RTL não relacionado (abordado no Capítulo 16), no Delphi 11 eles foram renomeados como K e V, para evitar
confusão. O livro foi atualizado de acordo, como você pode ver acima.
Outras classes são igualmente importantes, mas parecem mais fáceis de usar e entender.
Como exemplo de uso de um dicionário, escrevi um aplicativo que busca dados de um
tabela do banco de dados, cria um objeto para cada registro e usa um índice composto com um
ID do cliente e uma descrição como chave. A razão para esta separação é que uma arquitetura
semelhante pode ser facilmente usada para criar um proxy (inicialização lenta), em que a chave
toma o lugar de uma versão light do objeto real carregado do banco de dados.
A seguir estão as duas classes usadas pelo exemplo CustomerDictionary para a chave e o valor real.
O primeiro possui apenas dois campos relevantes da tabela do banco de dados correspondente,
enquanto o segundo possui a estrutura de dados completa (omiti os campos privados, métodos
getter e métodos setter):
tipo
TCustomerKey = classe
privado
...
Publicados
propriedade CustNo: leitura dupla FCustNo gravação SetCustNo;
propriedade Empresa: string lida FCompany escreve SetCompany;
fim;
TCliente = classe
privado
...
procedimento inicial;
procedimento EnforceInit;
público
construtor Criar (ACustKey: TCustomerKey);
propriedade CustKey: TCustomerKey
ler FCustKey escrever SetCustKey;
Publicados
Enquanto a primeira classe é muito simples (cada objeto é inicializado quando é criado), a classe
TCustomer usa um modelo de inicialização lenta (ou proxy) e mantém em torno de uma referência ao
banco de dados de origem compartilhada (como uma classe var) por todos. objetos.
note Neste exemplo estou usando um objeto derivado de TDataSet para acessar dados do banco de dados, para tornar este cenário
um pouco mais real. Discutir o acesso ao banco de dados em Delphi está realmente além do escopo deste livro, portanto
não fornecerei muitas descrições adicionais do desempenho real das operações nesta classe TDataSet .
Resultado :=
FEmpresa; fim;
O método EnforceInit verifica um sinalizador local, eventualmente chamando Init para carregar dados
do banco de dados para o objeto na memória:
procedimento TCustomer.Init;
começar
RefDataSet.Locate( 'CustoNão' , CustKey.CustNo, []);
Dadas essas duas classes, adicionei um dicionário de propósito especial ao aplicativo. Esse
a classe de dicionário personalizada herda de uma classe genérica instanciada com os tipos apropriados e adiciona a
ela um método específico:
tipo
TCustomerDictionary = classe(
TObjectDictionary<TCustomerKey, TCustomer>)
público
procedimento LoadFromDataSet(DataSet: TDataSet);
fim;
O programa de demonstração possui um formulário principal e um módulo de dados que hospeda um componente
ClientDataSet. O formulário principal possui um controle ListView que é preenchido quando um usuário pressiona o único
botão no formulário.
note Você pode querer substituir o componente ClientDataSet por um conjunto de dados real, expandindo consideravelmente o
exemplo em termos de utilidade, já que você poderia executar uma consulta para as chaves e uma consulta separada
para os dados reais de cada objeto TCustomer . Eu tenho um código semelhante, mas adicioná-lo aqui nos distrairia
muito do objetivo do exemplo, que é experimentar uma classe de dicionário genérica.
procedimento TFormCustomerDictionary.ListView1SelectItem(
Remetente: TObject; Item: TListItem; Selecionado: Booleano);
era
ACliente: TCliente;
começar
ACustomer := CustDict.Items[Item.Data];
Item.SubItems.Add(
Se então(
''
ACustomer.State <> ,
'
ACustomer.State + ', + ACliente.País,
ACustomer.Country));
fim;
O método acima mapeia o objeto para a chave fornecida e usa seus dados. Nos bastidores, na
primeira vez que um objeto específico é utilizado, o método de acesso à propriedade aciona o
carregamento de todos os dados do objeto TCustomer .
Classes de contêineres específicas e focadas são uma opção muito melhor. Por exemplo, um genérico
TDictionary com uma chave de string e um valor de objeto geralmente será melhor que um
TStringList em dois aspectos: código mais limpo e seguro, pois haverá menos conversões de tipo
envolvidas e execução mais rápida, visto que os dicionários usam tabelas hash.
privado
FList: TStringList;
FDict: TDictionary<string, TMyObject>;
As duas listas são preenchidas com entradas aleatórias, mas idênticas, usando um loop que repete este
código:
FList.AddObject(AName, AnObject);
FDict.Add(AName, AnObject);
Dois botões recuperam cada elemento da lista e fazem uma busca por nome em cada um deles.
Ambos os métodos examinam a lista de strings em busca de valores, mas o primeiro localiza os objetos
na lista de strings, enquanto o segundo usa o dicionário. Observe que no primeiro caso você precisa de
um as cast para recuperar o tipo fornecido, enquanto o dicionário já está vinculado a essa classe.
Aqui está o loop principal dos dois métodos:
OTotal := 0;
para I := 0 para SList.Count - 1 faça
começar
ANome := FLista[I];
// Nai procurar isto
AnIndex := FList.IndexOf(AName);
// GE o objeto
AnObject := FList.Objects[AnIndex] as TMyObject;
Inc(TheTotal, AnObject.Value);
fim;
OTotal := 0;
para I := 0 para FList.Count - 1 faça
começar
ANome := FLista[I];
// GE o objeto
AnObject := FDict.Items[AName];
Inc(TheTotal, AnObject.Value);
fim;
Não quero acessar as strings em sequência, mas descobrir quanto tempo leva para pesquisar na lista de
strings classificada (que faz uma pesquisa binária) em comparação com as chaves hash do dicionário.
Não é de surpreender que o dicionário seja mais rápido. Aqui estão os números em milissegundos para
um teste:
Total: 99493811
StringList: 2839
Total: 99493811
Dicionário: 686
Os totais eram obviamente idênticos, mas os tempos são bastante diferentes, com o dicionário
demorando cerca de um quarto do tempo para cerca de 100 milhões de entradas.
Interfaces genéricas
Na seção “Classificando um TList<T>” você deve ter notado o uso um tanto estranho de uma
interface predefinida, que tinha uma declaração genérica. Vale a pena examinar detalhadamente
essa técnica, pois ela abre oportunidades significativas.
O primeiro elemento técnico a notar é que é perfeitamente legal definir uma interface genérica,
como fiz no exemplo GenericInterface :
tipo
IGetValue<T> = interface
função ObterValor: T;
procedimento SetValue(Valor: T);
fim;
note Esta é a versão genérica da interface IGetValue do exemplo IntfContraints , abordada na seção
anterior “Restrições de interface” deste capítulo. Nesse caso a interface tinha um valor Inteiro,
agora tem um valor genérico.
Observe que, diferentemente de uma interface padrão, no caso de uma interface genérica, não
é necessário especificar um GUID para ser usado como Interface ID (ou IID). O compilador irá
gerar um IID para você para cada instância da interface genérica, mesmo que declarada
implicitamente. Na verdade, você não precisa criar uma instância específica da interface genérica
para implementá-la, mas pode definir uma classe genérica que implemente a interface genérica:
tipo
TGetValue<T> = classe(TInterfacedObject, IGetValue<T>)
privado
Valor F: T;
público
construtor Criar (Valor: T);
destruidor Destruir; sobrepor;
função ObterValor: T;
procedimento SetValue(Valor: T);
fim;
Embora o construtor atribua o valor inicial do objeto, o único propósito do destruidor é registrar
que um objeto foi destruído. Podemos criar uma instância desta classe genérica (gerando uma
instância específica do tipo de interface nos bastidores) escrevendo:
Uma abordagem alternativa, como vimos no passado para o exemplo IntfConstraint , é usar uma variável
de interface do tipo correspondente, tornando explícita a definição específica do tipo de interface (e não
implícita como no trecho de código anterior):
Claro, também podemos definir uma classe específica que implemente a interface genérica, como no
cenário a seguir (do exemplo GenericInterface ):
tipo
TButtonValue = classe(TButton, IGetValue<Integer>)
público
função GetValue: Inteiro;
procedimento SetValue(Valor: Inteiro);
função de classe MakeTButtonValue (Proprietário: TComponent;
Pai: TWinControl): TButtonValue;
fim;
Observe que enquanto a classe genérica TGetValue<T> implementa a classe genérica IGetValue<T>
interface, a classe específica TButtonValue implementa a classe específica IGetValue<Integer>
interface. Especificamente, como no exemplo anterior, a interface é remapeada para a esquerda
propriedade do controle:
Na classe acima, a função da classe MakeTButtonValue é um método pronto para usar para criar um
objeto da classe. Este método é utilizado pelo terceiro botão do formulário principal, conforme segue:
IVal: IGetValue<Inteiro>;
começar
IVal := TButtonValue.MakeTButtonValue(Self, ScrollBox1);
'
'Valor do botão:
Mostrar(+IntToStr(IVal.GetValue));
fim;
Embora não tenha nenhuma relação com classes genéricas, aqui está a implementação do
Função da classe MakeTButtonValue :
Agora que exploramos como definir interfaces genéricas e combiná-las com o uso de classes
genéricas e específicas, podemos voltar a dar uma segunda olhada na unidade Generics.Defaults .
Esta unidade define duas interfaces genéricas de comparação:
· IComparer<T> possui um método Compare
· IEqualityComparer<T> possui métodos Equals e GetHashCode
Estas classes são implementadas por algumas classes genéricas e específicas, listadas abaixo
(sem detalhes de implementação):
tipo
TComparer<T> = classe(TInterfacedObject, IComparer<T>)
TEqualidadeComparer<T> = class(
TInterfacedObject, IEqualityComparer<T>)
TCustomComparer<T> = class(TSingletonImplementation,
IComparer<T>, IEqualityComparer<T>)
TStringComparer = classe(TCustomComparer<string>)
Na listagem acima você pode ver que a classe base usada pelas implementações genéricas
das interfaces é a classe TInterfacedObject contada por referência clássica ou a classe
TSingletonImplementation (o alias da classe TNoRefCountObject ). Isto é um
classe com nome estranho que fornece uma implementação básica de IInterface sem contagem
de referências.
note O termo singleton geralmente é usado para definir uma classe da qual você pode criar apenas uma instância, e não
uma sem contagem de referência. Considero isso um nome bastante impróprio.
Como já vimos na seção “Classificando um TList<T>”, anteriormente neste capítulo, essas classes de
comparação são usadas pelos contêineres genéricos. Para tornar as coisas mais complicadas, porém,
a unidade Generics.Default depende bastante de métodos anônimos, então você provavelmente deveria
dar uma olhada nela somente depois de ler o próximo capítulo.
Se você tem experiência em Object Pascal, talvez não tenha ouvido falar de ponteiros inteligentes,
uma ideia que vem da linguagem C++. Em C++ você pode ter ponteiros para objetos, para os quais é
necessário gerenciar a memória direta e manualmente, e variáveis de objetos locais que são gerenciadas
automaticamente, mas têm muitas outras limitações (incluindo a falta de polimorfismo). A ideia de um
ponteiro inteligente é usar um objeto gerenciado localmente para cuidar da vida útil do ponteiro para o
objeto real que você deseja usar. Se este
parece muito complicado, espero que a versão Object Pascal ajude a esclarecer isso.
note O termo polimorfismos em linguagens OOP é usado para denotar a situação em que você atribui a uma
variável de uma classe base um objeto de uma classe derivada e chama um dos métodos virtuais da
classe base, potencialmente acabando chamando a versão do método virtual da subclasse específica.
Antes do Delphi 10.4, os registros Object Pascal não ofereciam nenhuma maneira de executar código
personalizado no momento da destruição, um recurso introduzido nos registros gerenciados. Em vez
disso, o método antigo era usar um campo de interface nos registros, já que esse campo de interface é
gerenciado e o objeto usado para implementar a interface tem sua contagem de referências diminuída.
necessário, o que é suficiente em termos gerais. Com uma versão genérica, porém, você pode obter
duas vantagens:
· Um ponteiro inteligente genérico pode retornar uma referência ao objeto que contém, para que você
não precisa manter as duas referências por perto
· Um ponteiro inteligente genérico pode criar automaticamente o objeto contêiner, usando um
construtor sem parâmetros.
Aqui abordarei apenas dois exemplos de ponteiros inteligentes implementados usando registros
genéricos, mesmo que isso adicione um pouco mais de complexidade. O ponto de partida será um
registro genérico com restrição de objetos, como:
tipo
TSmartPointer<T:class>=registro
estritamente privado
Valor F: T;
função ObterValor: T;
público
construtor Criar (AValue: T);
Valor da propriedade: T read GetValue;
fim;
era
SL: ListaString;
começar
SL := TStringList.Create;
var SmartP: TSmartPointer<TStringList>.Create(SL);
'Foo'
SL.Adicionar();
SmartP.Value.Add( 'Bar' );
Como você deve ter percebido, esse código causa um vazamento de memória exatamente da mesma
maneira que sem o ponteiro inteligente! Isso acontece porque o registro é destruído ao sair do escopo,
mas não libera o objeto interno.
tipo
TSmartPointer<T: classe, construtor> = registro estritamente privado
Valor F: T;
função ObterValor: T; operador
de classe
pública Initialize(out ARec: TSmartPointer<T>); operador de classe Finalize(var
ARec: TSmartPointer<T>); operador de classe Assign(var ADest:
TSmartPointer<T>; const [ref] ASrc: TSmartPointer<T>); construtor Criar
(AValue: T); Valor da propriedade: T read GetValue;
fim;
Observe que, além da restrição de classe, o registro genérico também possui uma restrição
de construtor porque quero ser capaz de criar objetos do tipo de dados genérico. Isso acontece
caso o método GetValue seja chamado e o campo ainda não tenha sido inicializado. Este é
o código completo de todos os métodos:
ARec.FValue.Free; fim;
Este código faz parte do projeto SmartPointersMR , que inclui também um exemplo de como o
ponteiro inteligente pode ser usado. O primeiro se assemelha estritamente ao código de exemplo que
consideramos algumas páginas atrás:
Porém, como o ponteiro inteligente genérico tem suporte para construção automática de um objeto do
tipo indicado, você também pode usar a variável explícita referente à lista de strings e o código para
criá-la:
No programa, você pode verificar se todos os objetos foram realmente destruídos e se não há
vazamento de memória definindo o ReportMemoryLeaksOnShutdown global como True no código de
inicialização. Como contra-teste, existe um botão no programa que causa um vazamento, que
será detectado quando o programa for encerrado.
Como já mencionei que, antes do Delphi 10.4 disponibilizar os registros gerenciados, uma forma
possível de implementar ponteiros inteligentes era a utilização de uma interface, visto que o registro
liberará automaticamente um objeto referenciado por um campo de interface. Embora esta
abordagem seja agora menos interessante, ela oferece alguns recursos adicionais, como
operadores de conversão implícitos.
Dado que este continua a ser um exemplo interessante e complexo, decidi mantê-lo, com uma
descrição um tanto reduzida (este é o projeto SmartPointers na fonte).
Para implementar um ponteiro inteligente com uma interface, você pode escrever uma classe de
suporte interna, vinculada a uma interface, e usar o mecanismo de contagem de referência de
interface para determinar quando liberar o objeto. A classe interna se parece com o seguinte:
tipo
TFreeTheValue = classe(TInterfacedObject) privado
AnObjectToFree; fim;
FObjectToFree.Free; herdado;
fim;
Declarei isso como um tipo aninhado do tipo genérico de ponteiro inteligente. Tudo o que precisamos fazer
no tipo genérico de ponteiro inteligente, para habilitar esse recurso, é adicionar uma referência de
interface e inicializá-la com um objeto TFreeTheValue referente ao objeto contido:
tipo
TSmartPointer<T:class> = registro estritamente
privado
Valor F: T;
FFreeTheValue: IInterface; função
ObterValor: T; construtor público
Criar
(AValue: T); sobrecarga; Valor da propriedade: T read GetValue;
fim;
O pseudoconstrutor se torna:
FFreeTheValue :=
TFreeTheValue.Create(FValue); fim;
Com esse código implementado, agora podemos escrever o seguinte código em um programa sem
causar vazamento de memória (novamente, o código é semelhante ao que listei inicialmente e usei na
versão de registro do gerenciador):
SL := TStringList.Create;
SmartP.Create (SL);
'Foo'
SL.Adicionar();
'Bar'
SL.Adicionar();
'Contar: ' + IntToStr(SL.Count));
Mostrar(fim;
Ao final do método o registro SmartP é descartado, o que faz com que seu objeto de interface
interna seja destruído, liberando o objeto TStringList .
note O código funciona mesmo se uma exceção for gerada. Na verdade, blocos try-finally implícitos estão sendo adicionados
sobre os locais pelo compilador quando você usa um tipo gerenciado, como um registro com um campo de
interface (que é o cenário específico aqui).
Com este código (e aproveitando o campo Valor ) podemos agora escrever uma versão mais
compacta do código, como:
era
SmartP: TSmartPointer<TStringList>;
começar
SmartP := TStringList.Create;
SmartP.Value.Add(); 'Foo'
SmartP.Value.Add(); 'Bar'
'Contar: '
Mostrar(+IntToStr(SmartP.Value.Count));
Como alternativa, podemos usar uma variável TStringList e usar um construtor mais complicado
para inicializar o registro do ponteiro inteligente mesmo sem uma referência explícita a ele:
era
SL: ListaString;
começar
SL := TSmartPointer<TStringList>.Create(TStringList.Create).Value;
SL.Adicionar( 'Foo' );
'Bar' );
'Contar: ' + IntToStr(SL.Count));
SL.Add(Mostrar(
Ao iniciarmos esse caminho, também podemos definir a conversão oposta e usar a notação de conversão
em vez da propriedade Value :
era
SmartP: TSmartPointer<TStringList>;
começar
SmartP := TStringList.Create;
TStringList(SmartP).Add(); 'Bar'
Agora, você também pode notar que sempre usei um pseudoconstrutor no código acima, mas isso
não é necessário em um registro. Tudo o que precisamos é de uma forma de inicializar o objeto interno,
possivelmente chamando seu construtor, na primeira vez que o usarmos.
Não podemos testar se o objeto interno está atribuído, porque os registros (ao contrário das classes) não
são inicializados com zero. Porém podemos realizar esse teste na variável de interface, que é
inicializada. Como alternativa, poderíamos inicializar o registro com zero com código construtor extra.
note Para uma análise mais articulada e soluções mais sofisticadas (fora do escopo deste livro), posso
recomendar a seguinte postagem no blog de Erik van Bilsen: https://blog.grijjy.com/2020/08/12/custom-
owned -registros-para-ponteiros-inteligentes/.
No Object Pascal você não pode ter funções virtuais com valor de retorno diferente, nem sobrecarregar
o tipo de retorno, mas apenas quando estiver usando parâmetros diferentes. Deixe-me mostrar o código
completo de uma demonstração simples que envolve as três classes a seguir:
tipo
TAnimal = classe
privado
NomeF: string;
procedimento SetName(const Valor: string);
público
nome da propriedade: string lida FName escreve SetName;
público
função de classe Get (const AName: string): TAnimal;
função ToString: string; sobrepor;
fim;
TDog = classe(TAnimal)
fim;
TCato = classe(TAnimal)
fim;
A implementação dos dois métodos é bastante simples, uma vez que você percebe que a função da
classe é na verdade utilizada para criar novos objetos, chamando internamente um construtor.
A razão é que não quero criar um construtor diretamente porque esta é uma técnica mais geral, na qual
um método de uma classe pode criar objetos de outras classes (ou hierarquias de classes).
Este é o código:
Agora podemos usar a classe escrevendo o seguinte código, do qual não gosto muito, já que
precisamos converter o resultado para o tipo adequado:
era
ACato: TCato;
começar
'Matisse'
ACat := TCat.Get() de TCat;
Memo1.Lines.Add(ACat.ToString);
AGato.Livre;
Dado que o código garante que Get retorne um objeto TCat , a primeira linha acima também pode
ser escrita como a seguir (executando um pouco mais rápido):
Novamente, o que eu gostaria de fazer é poder atribuir o valor retornado por TCat.Get a uma referência
da classe TCat sem uma conversão explícita. Como podemos fazer isso?
Acontece que os genéricos podem nos ajudar a resolver o problema. Não são tipos genéricos,
que é a forma de genérico mais comumente usada. Mas métodos genéricos para tipos não genéricos,
discutidos anteriormente neste capítulo. O que posso adicionar à classe TAnimal é um método com
um parâmetro de tipo genérico , como:
Este método requer um parâmetro de tipo genérico, que precisa ser uma classe (ou tipo de instância)
e retorna um objeto desse tipo. Este é um exemplo de implementação:
Agora podemos criar uma instância e utilizá-la omitindo o as cast, embora ainda tenhamos que passar
o tipo como parâmetro:
era
ADão: TDog;
começar
ADog := 'Plutão' );
TDog.GetAs<TDog>(Memo1.Lines.Add(ADog.ToString);
ADog.Free;
tipo
TAnimalShop = classe
função de classe GetAs<T: TAnimal, construtor>(
const AName: string): T;
fim;
note Uma classe como esta, usada para criar objetos de uma classe diferente (ou mais de uma classe, dependendo do
parâmetro ou parâmetros) é geralmente chamada de “fábrica de classes”.
Agora podemos usar a restrição de classe específica (algo impossível na própria classe) especificando a
restrição do construtor para poder criar um objeto da classe dada a partir do método genérico:
Observe que agora, na chamada, não precisamos repetir o tipo de classe duas vezes:
15: anônimo
métodos
A linguagem Object Pascal inclui tipos procedurais (ou seja, tipos que declaram ponteiros para
procedimentos e funções) e ponteiros de método (ou seja, tipos que declaram ponteiros para
métodos).
note Caso você queira mais informações, os tipos procedurais foram abordados no Capítulo 4, enquanto eventos e
tipos de ponteiro de método foram descritos no Capítulo 10.
Embora você raramente os use diretamente, esses são os principais recursos do Object Pascal com
os quais todo desenvolvedor trabalha. Na verdade, os tipos de ponteiros de método são a base para
manipuladores de eventos em componentes e controles visuais: toda vez que você declara um
manipulador de eventos, mesmo um simples Button1Click você está na verdade declarando um
método que será conectado a um evento (o evento OnClick , em neste caso) usando um ponteiro de método.
Os métodos anônimos estendem esse recurso permitindo que você passe o código real de um
método como parâmetro, em vez do nome de um método definido em outro lugar. Esta não é a única
diferença, no entanto. O que torna os métodos anônimos muito diferentes de outras técnicas é a
maneira como eles gerenciam o tempo de vida das variáveis locais.
termos são usados por diferentes linguagens de programação e que o compilador C++
produzido pela Embarcadero usa o termo encerramentos para o que Object Pascal
chama de manipuladores de eventos. Métodos anônimos existem em diferentes formas e com
nomes diferentes há muitos anos em algumas linguagens de programação, principalmente
em linguagens dinâmicas. Tenho vasta experiência com encerramentos em JavaScript,
principalmente com a biblioteca jQuery. O recurso correspondente em C# é chamado de delegado anônim
Mas aqui não quero dedicar muito tempo comparando encerramentos e técnicas relacionadas
nas diversas linguagens de programação, mas sim descrever em detalhes como eles
funcionam no Object Pascal.
note De uma perspectiva muito elevada, os genéricos permitem que o código seja parametrizado para um tipo, enquanto os anônimos
métodos permite que o código seja parametrizado para um método.
tipo
TIntProc = referência ao procedimento(N: Inteiro);
Isso é diferente de um tipo de referência de método apenas nas palavras-chave usadas para a declaração:
tipo
TIntMethod = procedimento(N: Inteiro) do objeto;
AnIntProc :=
procedimento(N: Inteiro) começar
Observe a sintaxe usada para atribuir um procedimento real, com código local, à variável local AnIntProc.
AnIntProc(Valor);
Inc(Valor);
AnIntProc(Valor); fim;
A função chama o método passado como parâmetro duas vezes com dois valores inteiros
consecutivos, o passado como parâmetro e o seguinte. Você chama a função passando um
método anônimo real para ela, com um código surpreendente diretamente no local:
procedimento(N: Inteiro)
começar Mostrar(IntToHex(N,
4));
fim); CallTwice(100,
procedimento(N:
Inteiro) começar
Do ponto de vista da sintaxe, observe que o procedimento é passado como parâmetro entre
parênteses e não termina com ponto e vírgula. O efeito real do código é chamar IntToHex com 48 e
49 e FloatToStr na raiz quadrada de 100 e 101, produzindo a seguinte saída:
0030
0031
10
10.0498756211209
Aqui o método, ainda passado para o procedimento CallTwice , utiliza o parâmetro local N, mas
também uma variável local do contexto a partir do qual foi chamado, ANumber. Qual é o efeito? As
duas chamadas do método anônimo modificarão a variável local, adicionando o parâmetro a ela,
10 na primeira vez e 11 na segunda vez. O valor final de ANumber
serão 21.
note Em detalhes um pouco mais técnicos, os métodos anônimos copiam as variáveis e os parâmetros que
usam para o heap quando são criados e os mantêm ativos enquanto a instância específica do anônimo
método.
privado
FAnonMeth: TIntProc;
procedimento SetAnonMeth (valor const: TIntProc);
público
propriedade AnonMeth: TintProc
leia FAnonMeth escreva SetAnonMeth;
Depois adicionei mais dois botões ao formulário do programa. O primeiro salva a propriedade, um método
anônimo que usa uma variável local, mais ou menos como no método BtnLocalValClick anterior:
Quando este método é executado, o método anônimo não é executado, apenas armazenado. A variável
local ANumber é inicializada com três, não é modificada, sai do escopo local (conforme o método termina)
e é deslocada. Pelo menos, é isso que você esperaria do código Object Pascal padrão.
O segundo botão que adicionei ao formulário para esta etapa específica chama o método anônimo
armazenado na propriedade AnonMeth :
Quando esse código é executado, ele chama um método anônimo que usa a variável local ANumber de
um método que não está mais na pilha. No entanto, como os métodos anônimos capturam seu
contexto de execução, a variável ainda está lá e pode ser usada enquanto a instância específica do
método anônimo (ou seja, uma referência ao método) estiver disponível.
Como prova adicional, faça o seguinte. Pressione o botão Store uma vez, o botão Call duas vezes e você
verá que a mesma variável capturada está sendo usada:
5
8
10
13
note A razão para esta sequência é que o valor começa em 3, cada chamada para CallTwice passou seu parâmetro para os métodos
anônimos uma primeira vez (que é 2) e depois uma segunda vez após incrementá-lo (que
é, na segunda vez que passa 3).
Agora pressione Armazenar mais uma vez e pressione Ligar novamente. O que acontece, por que
o valor da variável local é redefinido? Ao atribuir uma nova instância de método anônimo, o
método anônimo antigo é excluído (junto com seu próprio contexto de execução) e um novo contexto
de execução é capturado, incluindo uma nova instância da variável local. A sequência completa
Armazenar – Chamar – Chamar – Armazenar – Chamar produz:
5
8
10
13
5
8
É a implicação desse comportamento, semelhante ao que algumas outras linguagens fazem, que
torna os métodos anônimos um recurso de linguagem extremamente poderoso, que você pode
usar para implementar algo que simplesmente não era possível no passado.
AnonMeth(2)
A diferença é clara; Preciso passar um parâmetro adequado para invocar o método. Coisas
são um pouco mais confusos com métodos anônimos sem parâmetros. Se você declarar:
tipo
TAnyProc = referência ao procedimento;
era
AnyProc: TAnyProc;
A chamada para AnyProc deve ser seguida por parênteses vazios, caso contrário o compilador
pensa que você está tentando obter o método (seu endereço) em vez de chamá-lo:
AnyProc();
Algo semelhante acontece quando você chama uma função que retorna um método anônimo, como
no seguinte caso retirado do exemplo usual do AnonymFirst :
GetShowMethod;
ele compila e executa, mas tudo o que faz é chamar o código de atribuição de método anônimo,
jogando fora o método anônimo retornado pela função.
Como você chama o método anônimo real passando um parâmetro para ele? Uma opção é
para usar uma variável de método anônima temporária:
era
IP: TIntProc;
começar
IP := GetShowMethod(); // Os parênteses são necessários!!!
PI(3);
Observe neste caso os parênteses após a chamada GetShowMethod . Se você os omitir (uma prática
padrão do Pascal), receberá o seguinte erro:
GetShowMethod(3);
não compilará, pois você não pode passar um parâmetro para o método. Você precisa adicionar
o parêntese vazio à primeira chamada e o parâmetro Integer ao método anônimo resultante.
Curiosamente, você pode escrever:
GetShowMethod()(3);
Obter detalhes internos é provavelmente muito complicado e de valor limitado. Basta dizer que a
implementação é muito eficiente, em termos de velocidade, e requer cerca de 500 bytes extras
para cada método anônimo.
Em outras palavras, uma referência de método em Object Pascal é implementada com uma interface
especial de método único, com um método gerado pelo compilador tendo a mesma assinatura da
referência de método que está implementando. A interface aproveita a contagem de referência
para seu descarte automático.
note Embora praticamente a interface usada para um método anônimo se pareça com qualquer outra interface, o
O compilador distingue entre essas interfaces especiais para que você não possa misturá-las no código.
Além dessa interface oculta, para cada invocação de um método anônimo o compilador cria um objeto oculto que contém
a implementação do método e os dados necessários para capturar o contexto da invocação. É assim que você
obtém um novo conjunto de variáveis capturadas para cada chamada do método.
Pascal fornece vários tipos de ponteiros de referência prontos para uso na unidade System.SysU-tils .
Como você pode ver no trecho de código abaixo, a maioria dessas definições de tipo usam
tipos parametrizados, de modo que com uma única declaração genérica você tem um tipo de ponteiro de
referência diferente para cada tipo de dados possível:
tipo
TProc = referência ao procedimento;
TProc<T> = referência ao procedimento(Arg1: T);
TProc<T1, T2> = referência ao procedimento(
Arg1:T1; arg2:T2);
TProc<T1, T2, T3> = referência ao procedimento(
Arg1:T1; Arg2:T2; Arg3:T3);
TProc<T1, T2, T3, T4> = referência ao procedimento(
Arg1:T1; Arg2:T2; Arg3:T3; Arg4:T4);
Usando essas declarações, você pode definir procedimentos que utilizam parâmetros de métodos
anônimos, como a seguir:
No primeiro e no segundo caso você passa um método anônimo sem parâmetros, no terceiro você passa
um método com um único parâmetro Integer:
Da mesma forma, a unidade System.SysUtils define um conjunto de tipos de métodos anônimos com um
valor de retorno genérico:
tipo
TFunc<TResult> = referência à função: TResult;
Tfunc<T, TResult> = referência à função (
Arg1: T): TResult;
TFunc<T1, T2, TResult> = referência à função (
Arg1: T1; Arg2: T2): TResultado;
TFunc<T1, T2, T3, TResult> = referência à função (
Arg1:T1; Arg2:T2; Arg3:T3):TResultado;
TFunc<T1, T2, T3, T4, TResult> = referência à função (
Arg1:T1; Arg2:T2; Arg3:T3; Arg4:T4):TResultado;
TPredicate<T> = referência à função (
Arg1: T): Booleano;
Essas definições são muito amplas, pois você pode usar inúmeras combinações de tipos de dados para
até quatro parâmetros e um tipo de retorno. A última definição é muito parecida com a segunda, mas
corresponde a um caso específico e muito frequente, uma função que recebe um parâmetro genérico
e retorna um booleano.
Como exemplo (o exemplo AnonButton ), adicionei um evento de clique anônimo a um botão, declarando
um tipo de ponteiro de método adequado e adicionando um novo manipulador de eventos a uma classe
de botão personalizada (definida usando uma classe intermediária):
tipo
TAnonNotif = referência ao procedimento(Remetente: TObject);
// Interpor classe
TButton = classe(FMX.StdCtrls.TButton)
privado
FAnonClick: TAnonNotif;
procedimento SetAnonClick (valor const: TAnonNotif);
público
procedimento Clique; sobrepor;
público
propriedade AnonClick: TAnonNotif
leia FAnonClick escreva SetAnonClick;
fim;
note Uma classe intermediária é uma classe derivada que tem o mesmo nome de sua classe base. Tendo duas aulas com
o mesmo nome é possível porque as duas classes estão em unidades diferentes, portanto seu nome completo
(nome-da-unidade.nomedaclasse) é diferente. Declarar uma classe interposer pode ser útil, pois você pode simplesmente
colocar um controle Button no formulário e anexar um comportamento extra a ele, sem precisar instalar um novo componente
no IDE e substitua os controles do seu formulário pelo novo tipo. O único truque que você deve lembrar é que se a
definição da classe interposer estiver em uma unidade separada (não na unidade de formulário como neste exemplo
simples), essa unidade deverá ser listada na instrução usa após a unidade que define a classe base .
O código desta classe é bastante simples, pois o método setter salva o novo ponteiro e o método Click o
chama antes de fazer o processamento padrão (ou seja, chamar o
Manipulador de eventos OnClick , se disponível):
procedimento TButton.Click;
começar
se atribuído (FAnonClick) então
FAnonClick (próprio)
herdado;
fim;
Como você pode usar esse novo manipulador de eventos? Basicamente você pode atribuir um método
anônimo a ele:
Agora, isso parece um tanto inútil, já que o mesmo efeito poderia ser facilmente alcançado usando um
método padrão de manipulador de eventos. O seguinte, em vez disso, começa a fazer a diferença, pois o
método anônimo captura uma referência ao componente que atribuiu o manipulador de eventos, referenciando
o parâmetro Sender .
Isso pode ser feito após atribuí-lo temporariamente a uma variável local, pois o parâmetro Sender do método
anônimo oculta o Sender do método BtnKeepRefClick .
parâmetro:
BtnInvoke.AnonClick :=
procedimento (Remetente: TObject)
começar
Show((Remetente como TButton).Text +
' '
Assinado por + ACompRef.Nome);
fim;
fim;
Ao pressionar o botão BtnInvoke , você verá sua legenda junto com o nome do componente que atribuiu
o manipulador de método anônimo.
T1 := TStopwatch.StartNew;
para I := 1 para MaxLoop faça
Str1 := Str1 + Str2;
T1.Parada; '
'Comprimento: + Str1.Length.ToString);
'
'Concatenação: + T1.ElapsedMilliseconds.ToString);
Mostrar(Mostrar(fim;
Um segundo método possui código semelhante, mas usou a classe TStringBuilder em vez de concatenação
simples.
Agora podemos aproveitar os métodos anônimos para criar um esqueleto de temporização e passar o
código específico como parâmetro, como fiz em uma versão atualizada do código, no exemplo
AnonLargeStrings . Em vez de repetir o código de temporização indefinidamente, você pode escrever uma
função com o código de temporização que invocaria o trecho de código por meio de um método
anônimo sem parâmetros:
Observe, porém, que se você executar a versão padrão e aquela baseada em métodos anônimos,
obterá uma saída ligeiramente diferente, com a versão do método anônimo sofrendo uma
penalidade de aproximadamente 10%.
A razão é que, em vez de executar diretamente o código local, o programa precisa fazer uma
chamada virtual para a implementação do método anônimo. Como essa diferença é consistente, o
código de teste faz todo o sentido, desde que os resultados dos testes das duas implementações não
sejam usados de forma intercambiável.
No entanto, se você precisar extrair desempenho do seu código, usar métodos anônimos não será tão
rápido quanto escrever o código diretamente usando uma função direta. Usar um ponteiro de método
não virtual provavelmente estaria em algum lugar entre os dois em termos de desempenho.
Sincronização de threads
Em aplicativos multithread que precisam atualizar a interface do usuário, você não pode acessar
propriedades de componentes visuais (ou objetos na memória) que fazem parte do thread principal
sem um mecanismo de sincronização. As bibliotecas de componentes visuais do Delphi não são
inerentemente seguras para threads (como acontece com a maioria das bibliotecas de interface de
usuário), o que significa que, se duas ou mais threads estiverem acessando um objeto ao mesmo tempo, o estado do objeto
poderia ser comprometido.
A solução clássica oferecida pela classe TThread em Object Pascal é chamar um método especial,
Synchronize, passando como parâmetro a referência a outro método, aquele a ser executado com
segurança. Este segundo método não pode ter parâmetros, portanto é prática comum adicionar
campos extras à classe de thread para passar as informações de um
método para outro. Como exemplo prático, no livro Mastering Delphi 2005 escrevi uma aplicação WebFind
(programa que executa pesquisas no Google via HTTP e extrai os links resultantes do HTML da página),
com a seguinte classe de thread:
tipo
TFindWebThread = classe(TThread)
protegido
FAddr, FText, FStatus: string;
procedimento Executar; sobrepor;
procedimento AddToList;
procedimento ShowStatus;
procedimento GrabHtml;
procedimento HtmlToList;
procedimento HttpWork(Remetente: TObject;
AWorkMode: TWorkMode; AWorkCount: Int64);
público
FStrUrl: string;
FStrRead: string;
fim;
Os três campos de string protegidos e alguns dos métodos extras foram introduzidos para suportar a
sincronização com a interface do usuário. Por exemplo, o manipulador de eventos HttpWork conectado ao
evento OnReceiveData de um componente THTTPClient interno (uma parte do componente da biblioteca de
cliente HTTP Delphi), costumava ter o seguinte código, que chamava o método ShowStatus :
procedimento TFindWebThread.ShowStatus;
começar
Form1.StatusBar1.SimpleText := FStatus;
fim;
O método Synchronize do Object Pascal RTL tem duas definições sobrecarregadas diferentes:
tipo
TThreadMethod = procedimento do objeto;
TThreadProcedure = referência ao procedimento;
TThread = classe
procedimento Sincronizar(AMethod: TThreadMethod); sobrecarga;
procedimento Sincronizar(AThreadProc: TThreadProcedure); sobrecarga;
Usando a mesma abordagem em todo o código da classe, a classe de thread se torna a seguinte
(você pode encontrar ambas as classes de thread na versão do exemplo WebFind
que acompanha o código-fonte deste livro):
tipo
TFindWebThreadAnon = classe(TThread)
protegido
procedimento Executar; sobrepor;
procedimento GrabHtml;
procedimento HtmlToList;
procedimento HttpWork(const Sender: TObject; AContentLength: Int64;
AReadCount: Int64; var AAbort: Booleano);
público
FStrUrl: string;
FStrRead: string;
fim;
O uso de métodos anônimos simplifica o código necessário para a sincronização de threads, pois
você pode evitar campos temporários.
note Os métodos anônimos têm muitos relacionamentos com threading, porque um thread é usado para executar algum código
e o método anônimo representa o código. É por isso que há suporte na classe TThread para utilizá-los, mas também
na Biblioteca de Programação Paralela (em TParallel.For e para definir um TTask). Dado que o exame do multithreading
vai muito além deste capítulo, não adicionarei mais exemplos nessa direção. Ainda assim, usarei outro thread no
próximo exemplo, porque isso geralmente é um requisito ao fazer uma chamada HTTP.
note A sigla AJAX significa Asynchronous JavaScript XML, pois este era originalmente o formato usado nas chamadas de
serviços web pelo navegador. À medida que esta tecnologia se tornou mais popular e difundida, e os serviços web
migraram para a arquitetura REST e o formato JSON, o termo AJAX desapareceu em
favor de REST. Decidi manter esse nome antigo para a demonstração de qualquer maneira, já que ele explica
o propósito do aplicativo e a história por trás dele. Você pode ler mais
em: https://en.wikipedia.org/wiki/Ajax_(programming).
A função global AjaxCall gera um thread, passando para o thread um método anônimo para
executar na conclusão. A função é apenas um wrapper em torno do thread
construtor:
tipo
TAjaxCallback = referência ao procedimento(
ResponseContent: TStringStream);
Todo o código está na classe TAjaxThread , uma classe de thread com um componente cliente
HTTP interno (da biblioteca Cliente HTTP) usado para acessar uma URL de forma assíncrona:
tipo
TAjaxThread = classe(TThread)
privado
FHttp: THTTPCliente;
FUrl: string;
FAjaxCallback: TAjaxCallback;
protegido
procedimento Executar; sobrepor;
público
construtor Create(const StrUrl: string;
AjaxCallback:TAjaxCallback);
destruidor Destruir; sobrepor;
fim;
O construtor faz alguma inicialização, copiando seus parâmetros para os campos locais
correspondentes da classe thread e criando o objeto FHttp . A verdadeira essência da classe está
em seu método Execute , que faz a solicitação HTTP, salvando o resultado em um fluxo que é
posteriormente redefinido e passado para a função de retorno de chamada – o método anônimo:
procedimento TAjaxThread.Execute;
era
AResponseContent: TStringStream;
começar
AResponseContent := TStringStream.Create;
tentar
FHttp.Get(FUrl, AResponseContent);
AResponseContent.Position := 0;
FAjaxCallback(AResponseContent);
finalmente
AResponseContent.Free;
fim;
fim;
Como exemplo de uso, o exemplo AnonAjax possui um botão para copiar o conteúdo de uma página
web para um controle Memo (após adicionar a URL solicitada):
Após a conclusão da solicitação HTTP, você poderá realizar qualquer processamento nela. Um
exemplo seria extrair links do HTML (de uma forma semelhante à aplicação WebFind abordada
anteriormente). Novamente, para tornar esta função flexível, ela toma como parâmetro o método
anônimo a ser executado para cada link:
tipo
TLinkCallback = referência ao procedimento(const StrLink: string);
Se você aplicar esta função ao resultado de uma chamada AJAX e fornecer um método adicional para
processamento, você acaba com duas chamadas de método anônimas aninhadas, como no
segundo botão do exemplo AnonAjax :
fim);
fim;
Neste caso o controle Memo receberá uma coleção de links, ao invés do HTML da página
retornada. Uma variação da rotina de extração de link acima seria uma rotina de extração
de imagem. A função ExtractImages captura a fonte (src) das tags img do arquivo HTML
retornado e chama outro método anônimo compatível com TLinkCallback . Agora você
pode imaginar a abertura de uma página HTML (com a função AjaxCall ), extrair os links da
imagem e usar o AjaxCall novamente para obter as imagens reais. Isso significa usar um
fechamento triplo aninhado, em uma estrutura de codificação que alguns programadores
de Object Pascal podem achar muito incomum, mas que é certamente muito poderosa e expressiva:
note Este trecho de código foi tema de um post meu no blog, “Anonymous, Anonymous, Anonymous” de setembro de 2008, que atraiu alguns
comentários, como você pode ver em:
https://blog.marcocantu.com/blog/anonymous_3.html.
Além do gráfico só funcionar no caso de você estar carregando um arquivo com o mesmo formato
daquele que já está no componente Imagem, o código e seu resultado são impressionantes.
Observe em particular a sequência de numeração, baseada na captura da variável local NHit . O
que acontece se você pressionar o botão duas vezes, em uma sequência rápida?
Cada um dos métodos anônimos obterá uma cópia diferente do contador NHit e poderá
potencialmente ser exibido fora de sequência na lista, com o segundo thread começando a
produzir sua saída antes do primeiro.
Apesar deste problema potencial, o resultado do uso deste último botão é exibir uma imagem
como você pode ver na Figura 15.1.
uma web
página
16: reflexão e
atributos
A primeira versão do Object Pascal rompeu com essa tradição, ao fornecer informações de tempo de execução
para propriedades e outros membros da classe marcados com uma diretiva específica do compilador,
publicada. Este recurso foi habilitado para classes compiladas com uma configuração específica {$M+} e é a
base do mecanismo de streaming por trás dos arquivos DFM da VCL (e dos arquivos FMX da biblioteca
FireMonkey) e da forma como você trabalha com o formulário
e outros designers visuais.
Quando foi disponibilizado pela primeira vez no Delphi 1, esse recurso era uma ideia completamente nova,
que mais tarde outras ferramentas de desenvolvimento adotaram, ampliaram e desenvolveram.
Primeiro, havia extensões para o sistema de tipos (disponível apenas em Object Pascal) para dar conta
da descoberta de métodos e da invocação dinâmica em COM. Isso ainda é suportado no Object Pascal por ID
de despacho, aplicação de métodos a variantes e outros recursos relacionados ao COM. Eventualmente, o
suporte COM no Object Pascal foi estendido com seu próprio tipo de informação de tipo de tempo de execução,
mas este é um tópico muito além do escopo de um livro de linguagem.
O advento de ambientes gerenciados, como Java e .NET, trouxe uma forma muito extensa de informações
sobre o tipo de tempo de execução, com RTTI detalhado vinculado ao compilador
aos módulos executáveis e disponíveis para descoberta por programas que usam esses módulos. Isto
tem a desvantagem de desvendar algumas das partes internas do programa e de aumentar o
tamanho dos módulos, mas traz consigo novos modelos de programação que combinam um pouco da
flexibilidade das linguagens dinâmicas com a estrutura sólida e a velocidade das linguagens fortemente
tipadas.
Quer você goste ou não (e isso de fato foi objeto de intenso debate na época em que esse recurso foi
introduzido), o Object Pascal está se movendo lentamente na mesma direção,
e a adopção de uma forma extensiva de RTTI marca um passo muito significativo nessa direcção. Como
veremos, você pode cancelar o RTTI, mas se não o fizer, poderá aproveitar algum poder extra em seus
aplicativos.
· Primeiro nos concentraremos no novo RTTI estendido que está embutido no compilador e no
novas classes da unidade Rtti que você pode usar para explorá-la.
· Terceiro, apresentarei atributos personalizados, um recurso paralelo ao seu equivalente .NET e que permite
estender as informações RTTI geradas pelo compilador.
Apenas na última parte do capítulo tentarei voltar às razões por detrás do RTTI alargado e examinar
exemplos práticos da sua utilização.
RTTI estendido
O compilador Object Pascal gera por padrão muitas informações estendidas de RTTI.
Essas informações de tempo de execução incluem todos os tipos, incluindo classes e todos os
outros tipos definidos pelo usuário, bem como os principais tipos de dados predefinidos pelo compilador e
abrangem campos publicados, bem como os públicos, até mesmo elementos protegidos e privados. Isso é necessário
ser capaz de mergulhar na estrutura interna de qualquer objeto.
Um primeiro exemplo
Antes de examinarmos as informações geradas pelo compilador e as diversas técnicas para acessá-las,
deixe-me chegar à conclusão e mostrar o que pode ser feito usando o RTTI. O exemplo específico é mínimo
e poderia ter sido escrito com o RTTI antigo, mas deve dar uma ideia do que estou falando (considerando
também que nem todos os desenvolvedores de Object Pascal usaram explicitamente o RTTI tradicional).
Suponha que você tenha um formulário com um botão, como no exemplo RttiIntro . Você pode escrever o
seguinte código para ler o valor da propriedade Text do controle :
usa
Rtti;
O código usa o registro TRttiContext para se referir a informações sobre o tipo TButton , desde essas informações
de tipo até os dados RTTI sobre uma propriedade, e esses dados de propriedade são usados para se referir ao
valor real da propriedade, que é convertido em uma string.
Se você está se perguntando como isso funciona, continue lendo. O que quero dizer aqui é que essa abordagem
agora pode ser usada não apenas para acessar uma propriedade dinamicamente, mas também para ler os
valores dos campos, incluindo campos privados.
Também podemos alterar o valor de uma propriedade, conforme o segundo botão do RttiIntro
exemplo mostra:
Este código substitui o Texto por um número aleatório de *s. A diferença do código
acima é que possui uma variável local temporária referente às informações do RTTI da propriedade. Agora que
você tem uma ideia do que estamos fazendo, vamos começar do início verificando as informações estendidas do
RTTI geradas pelo compilador.
Você pode se surpreender com o fato de obter informações de RTTI para campos privados, mas isso é
necessário para operações dinâmicas, como serialização de objetos binários e rastreamento de objetos
no heap.
Você pode controlar a geração estendida de RTTI de acordo com uma matriz de configurações: Em um
eixo você tem a visibilidade e no outro o tipo de membro. A tabela a seguir descreve o padrão do sistema:
Tecnicamente, as quatro configurações de visibilidade são indicadas usando o seguinte tipo de conjunto,
declarado na unidade do sistema :
tipo
TVisibilityClasses = conjunto de (vcPrivate,
vcProtegido, vcPublic, vcpublicado);
Existem alguns valores constantes prontos para uso para este conjunto, indicando as configurações
padrão de visibilidade RTTI aplicadas ao TObject e herdadas por todas as outras classes por padrão:
const
DefaultMethodRttiVisibility = [vcPublic, vcPublished];
DefaultFieldRttiVisibility = [vcPrivate..vcPublished];
DefaultPropertyRttiVisibility = [vcPublic, vcPublished];
A informação produzida pelo compilador é controlada por uma nova diretiva, $RTTI, que possui um
status que indica se a configuração é para o tipo determinado ou também para seus descendentes
(EXPLICIT ou INHERITED) seguido de três especificadores para definir a visibilidade para métodos,
campos e propriedades.
{$RTTI HERANÇA
Para desabilitar completamente a geração de RTTI estendido para todos os membros de suas classes você
pode usar a seguinte diretiva:
note Você não pode colocar a diretiva RTTI antes da declaração da unidade, como acontece com outros compiladores
diretivas, porque depende das configurações definidas na unidade do sistema . Se fizer isso, você receberá uma mensagem de erro interna, que
não é particularmente intuitiva. Em qualquer caso, basta mantê-lo após a declaração da unidade.
Ao usar esta configuração, considere que ela será aplicada apenas ao seu código e que uma
remoção completa não é possível, pois as informações do RTTI para o RTL e outras classes da
biblioteca já estão compiladas nos DCUs e pacotes correspondentes.
Tenha também em mente que a diretiva $RTTI não causa nenhuma alteração no RTTI tradicional gerado para
tipos publicados: este ainda é produzido independentemente da diretiva $RTTI .
note As classes de processamento RTTI, disponíveis na unidade System.Rtti e abordadas na próxima seção,
conecte-se ao RTTI tradicional e sua estrutura PTypeInfo .
O que você pode fazer com esta diretiva é impedir que o RTTI estendido seja gerado para suas
próprias classes. No extremo oposto da escala, você também pode aumentar a quantidade de RTTI
gerada, incluindo métodos e propriedades privadas e protegidas, se desejar (embora isso não faça
muito sentido).
O efeito óbvio de adicionar informações estendidas de RTTI a um arquivo executável é que o
o arquivo ficará maior (o que tem a principal desvantagem de um arquivo maior para distribuir, já
que o tempo extra de carregamento e o consumo de memória não são tão relevantes). Dessa forma,
se você decidir não usar RTTI em seu código, poderá remover o RTTI das unidades do seu
programa e isso poderá ter um efeito positivo. Entretanto, o RTTI é uma técnica poderosa, como
você verá neste capítulo, e na maioria dos casos vale a pena o tamanho extra do executável.
Existem duas diretivas de compilador que você pode usar para controlar as informações vinculadas
ao executável. A primeira, que está totalmente documentada, é a direção $WeakLinkRTTI.
positivo. Ao ativá-lo, para tipos não utilizados no programa, tanto o tipo em si quanto suas informações
de RTTI serão removidos do executável final.
Alternativamente, você pode forçar a inclusão de todos os tipos e seu RTTI estendido usando o comando
Diretiva $StrongLinkTypes . O efeito em muitos programas é dramático, com um aumento de quase
duas vezes no tamanho do programa.
A Unidade RTTI
Se a geração de RTTI estendido para todos os tipos é o primeiro pilar para a reflexão que será abordada
em breve no Object Pascal, o segundo pilar é a capacidade de navegar por essas informações
de maneira fácil e de alto nível, graças ao Unidade System.Rtti . O terceiro pilar, como veremos mais
adiante, é o suporte a atributos customizados. Mas deixe-me prosseguir um passo de cada vez.
Tradicionalmente, os aplicativos Object Pascal poderiam (e ainda podem) usar as funções da unidade
System.TypInfo para acessar as informações de tipo de tempo de execução publicadas . Esta
unidade define diversas estruturas e funções de dados de baixo nível (todas baseadas em ponteiros
e registros) com algumas rotinas de nível superior para tornar as coisas um pouco mais fáceis.
A unidade Rtti , por outro lado, facilita muito o trabalho com o RTTI estendido, fornecendo um conjunto
de classes com métodos e propriedades adequadas. Para acessar os diversos objetos, o ponto de
entrada é a estrutura de registros TRttiContext , que possui quatro métodos para procurar os tipos
disponíveis:
Como você pode ver você pode passar uma classe, um ponteiro PTypeInfo obtido de um tipo, um nome
qualificado (o nome do tipo decorado com o nome da unidade, como em “System.
TObject”), ou recuperar a lista completa de tipos, definida como uma matriz de tipos RTTI, ou mais
precisamente como TArray<TRttiType>.
Esta última chamada é a que usei na listagem a seguir, uma versão simplificada do código no exemplo
TypesList :
se AType.IsInstance então
Show(AType.QualifiedName);
fim;
O método GetTypes retorna a lista completa de tipos de dados, mas o programa filtra apenas os
tipos que representam classes. Existem cerca de uma dúzia de outras classes que representam tipos
na unidade.
note A unidade Rtti refere-se aos tipos de classe como “instâncias” e “tipos de instância” (como em TRttiInstanceType).
Isso é um pouco confuso, pois geralmente usamos os termos instância para nos referirmos a um objeto real.
Os objetos individuais na lista de tipos são de classes que herdam da classe base TRttiType .
Especificamente, podemos procurar o tipo de classe TRttiInstanceType , reescrevendo o código acima
como no seguinte trecho modificado:
O código real da demonstração é um pouco mais complexo, pois preenche primeiro uma lista de
strings, classifica os elementos e, em seguida, preenche um controle ListView, usando BeginUdpate e
EndUdpate para otimização, adiando uma repintura do controle até o final da operação (usando um bloco try-
finally para garantir que a operação final seja sempre executada):
era
AContext: TRttiContext; OsTipos:
TArray<TRttiType>; SList: TStringList;
AType: TRttiType;
STypeName: string;
comece ListView1.ClearItems;
SList :=
TStringList.Create; tente
TheTypes := AContext.GetTypes; para
se AType.IsInstance então
SList.Add(AType.QualifiedName); SList.Sort;
ListView1.BeginUpdate; tente
STypeName em SList do
(ListView1.Items.Add).Text := STypeName; finalmente
ListView1.EndUpdate; fim;
finalmente
SList.Free; fim;
fim;
Este código produz uma lista bastante longa com centenas de tipos de dados, com o
número real dependendo da plataforma e da versão do compilador, como você pode ver na Figura
16.1. Observe que a imagem lista os tipos da unidade RTTI, abordados na próxima seção.
Figura 16.1:
A saída do
exemplo TypesList
TRttiObject // Abstrato
TRttiNamedObject
Tipo TRtti
TRttiStructuredType // Abstrato
TRttiRecordType
TRttiInstanceType
TRttiInterfaceType
TRttiOrdinalType
TRttiEnumerationType
TRttiInt64Type
TRttiInvokableType
TRttiMethodType
TRttiProcedureType
TRttiClassRefType
TRttiEnumerationType
TRttiSetType
TRttiStringType
TRttiAnsiStringType
TRttiFloatType
TRttiArrayType
TRttiDynamicArrayType
TRttiPointerType
TRttiMembro
TRttiField
Propriedade TRtti
Propriedade TRttiInstance
Propriedade TRttiIndexed
Método TRtti
Parâmetro TRtti
Pacote TRtti
TRttiManagedField
Cada uma dessas classes fornece informações específicas sobre o tipo nomeado. Por exemplo, apenas
um TRttiInterfaceType oferece uma maneira de acessar o GUID da interface.
note Na primeira implementação da unidade Rtti não havia objeto RTTI para acessar propriedades indexadas
(como Strings[] de um TStringList). Isso foi adicionado posteriormente e agora está disponível, tornando as informações do
tipo de tempo de execução realmente completas.
Se você observar o código-fonte do método BtnTypesListClick listado anteriormente, há algo que parece
muito errado. A chamada GetTypes retorna uma matriz de tipos, mas o código não libera esses objetos
internos.
A razão é que a estrutura de registro TRttiContext se torna a proprietária efetiva de todos os objetos
RTTI que estão sendo criados. Quando o registro é descartado (isto é, quando sai do escopo), uma
interface interna é limpa invocando seu próprio destruidor que libera todos os objetos RTTI que foram
criados através dele.
O registro TRttiContext , na verdade, tem uma função dupla. Por um lado, controla a vida útil de
os objetos RTTI (como acabei de explicar), por outro lado, armazena em cache informações RTTI que
são bastante caras para recriar com uma pesquisa. É por isso que você pode querer manter as referências
ao registro TRttiContext ativas por um longo período, permitindo continuar acessando os objetos RTTI
que ele possui sem ter que recriá-los, que é a parte demorada que você deseja evitar.
Internamente o registro TRttiContext utiliza um pool global do tipo TRttiPool, que utiliza uma seção crítica
para sincronizar a execução do código entre diferentes threads, tornando sua thread de acesso
segura.
note Há exceções à segurança de thread do mecanismo de pooling RTTI, descritas com alguns detalhes nos comentários disponíveis
na própria unidade Rtti .
Portanto, para ser mais preciso, o pool RTTI é compartilhado entre os registros TRttiContext , de modo
que os objetos RTTI agrupados são mantidos enquanto pelo menos um registro TRttiContext estiver na
memória. Para citar o comentário na unidade:
{... a trabalhando com objetos RTTI sem erros. Mantendo-se em pelo pelo menos um contexto estar vivo mantém o
um menos um contexto vivo deve Piscina
variável é válida}
Em outras palavras, você deve evitar armazenar em cache e manter objetos RTTI depois de liberar
o contexto RTTI. Este é um exemplo que leva a uma violação de acesso à memória (novamente parte do
exemplo TypesList ):
Resumindo, os objetos RTTI são gerenciados pelo contexto e você não deve mantê-los por perto. O
contexto por sua vez é um registro, portanto é descartado automaticamente. Você poderá ver o código
que usa o TRttiContext da seguinte maneira:
AContext := TRttiContext.Create;
tentar
// Usar o contexto
finalmente
AContext.Free;
fim;
Figura 16.2: As
informações detalhadas
do tipo exibidas pelo exemplo
TypesList para a
classe TRttiMethod
A unidade desta forma secundária, que provavelmente pode ser adaptada e expandida para ser
usada como um navegador de tipo genérico em outras aplicações, possui um método chamado
ShowTypeIn-formation que percorre cada propriedade, método e campo de um determinado
tipo, adicionando-os a três caixas de listagem separadas com a indicação de sua visibilidade (pri
para privado, pro para protegido, pub para público e pbl para publicado, conforme retornado por uma
instrução case simples na função VisibilityToken ):
LabelType.Text := AType.QualifiedName;
para AProperty em Atype.GetProperties faça
FormTypeInfo.LVProperties.Items.Add.Text := AProperty.Name +
' '
':' + AProperty.PropertyType.Name + +
VisibilityToken(AProperty.Visibility);
para AMethod em Atype.GetMethods faça ' '
LVMethods.Items.Add.Text := AMethod.Name + +
VisibilityToken(AMethod.Visibility);
para AField em AType.GetFields faça
LVFields.Items.Add.Text := AField.Name + ':' +
' '
AField.FieldType.Name + +
VisibilityToken(AField.Visibility);
fim;
Você pode extrair mais informações dos tipos dessas propriedades, obter listas de parâmetros dos métodos
e verificar o tipo de retorno e muito mais. Aqui não quero construir um navegador RTTI completo, mas
apenas dar uma ideia do que pode ser alcançado.
Se você usar pacotes de tempo de execução, também poderá recuperar a lista de tipos de cada um dos
pacotes (e o próprio arquivo executável), usando código como:
era
AContext: TRttiContext;
Pacote A: Pacote TRtti;
AType: TRttiType;
começar
para APackage em AContext.GetPackages faça
começar
'PACOTE '
ListBox1.Items.Add(+ APackage.Nome);
para AType em APackage.GetTypes faça
se AType.IsInstance então
começar
// mostra o nome do tipo com espaços para recuo
' - '
ListBox1.Items.Add(end; + AType.QualifiedName);
fim;
note Pacotes em Object Pascal podem ser usados para adicionar componentes ao ambiente de desenvolvimento, como vimos no
Capítulo 11. Entretanto, pacotes também podem ser usados em tempo de execução, implantando um executável principal
com alguns pacotes de tempo de execução, em vez de um único, arquivo executável maior. Se você está familiarizado com
o desenvolvimento do Windows, os pacotes têm uma função semelhante às DLLs (e tecnicamente são DLLs), ou ainda
mais precisamente como assemblies .NET. Embora os pacotes desempenhem um papel muito importante no
Windows, eles não são atualmente suportados em plataformas móveis (também devido a limitações de implantação de
aplicativos do sistema operacional, como no iOS).
A Estrutura TValue
O novo RTTI estendido não apenas permite navegar pela estrutura interna de um programa, mas
também fornece informações específicas, incluindo valores de propriedades e campos. Enquanto a unidade
TypInfo fornece a função GetPropValue para acessar uma propriedade genérica e recuperar um tipo
de variante com seu valor, a nova unidade Rtti usa uma estrutura diferente para armazenar um elemento
não digitado, o registro TValue .
O registro TValue pode armazenar quase qualquer tipo de dados Object Pascal possível e faz isso por
manter o controle da representação de dados original, mantendo os dados e seu tipo de dados. O que
ele pode fazer é ler e gravar dados no formato determinado. Se você escrever um número inteiro em
TValue, só poderá ler um número inteiro dele. Se você escrever uma string, poderá lê-la.
O que não pode fazer é converter de um formato para outro. Portanto, mesmo que um TValue tenha
um método AsString e AsInteger , você poderá usar o primeiro apenas se o dado que representa for de
fato uma string, o segundo apenas se você originalmente atribuiu um número inteiro a ele. Para
Por exemplo, neste caso você pode usar o método AsInteger e, se chamar o método IsOrdinal
método, ele retornará True:
era
V1: ValorTV;
começar
V1:= 100;
se V1.IsOrdinal então
Log(IntToStr(V1.AsInteger));
No entanto, você não pode usar o método AsString , o que geraria uma conversão de tipo inválida
exceção:
era
V1: ValorTV;
começar
V1:= 100;
Log(V1.AsString);
Porém, se você precisar de uma representação de string, poderá usar o método ToString , que possui uma
instrução case grande tentando acomodar a maioria dos tipos de dados:
era
V1: ValorTV;
começar
V1:= 100;
Log(V1.ToString);
Você provavelmente poderá entender melhor lendo as palavras de Barry Kelly, ex-membro da equipe de
P&D da Embarcadero que trabalhou no RTTI:
TValue é o tipo usado para empacotar valores de e para chamadas baseadas em RTTI para
métodos e leituras e gravações de campos e propriedades.
É um pouco parecido com o Variant, mas muito mais sintonizado com o sistema do tipo Object
Pascal; por exemplo, instâncias podem ser armazenadas diretamente, bem como conjuntos,
referências de classe, etc. Também é digitado de forma mais estrita e não faz (por exemplo)
conversões silenciosas de string para número.
Agora que você entende melhor sua função, vamos dar uma olhada nas capacidades reais do registro
TValue . Possui um conjunto de métodos de nível superior para atribuir e extrair os valores reais, além de
um conjunto de métodos baseados em ponteiros de baixo nível. Vou me concentrar no primeiro grupo.
Para atribuir valores, TValue define vários operadores implícitos , permitindo
execute uma atribuição direta como nos trechos de código acima:
O que todos esses operadores fazem é chamar o método de classe genérica From :
Ao chamar essas funções de classe você precisa especificar o tipo de dados e também passar um valor
desse tipo, como o código a seguir substituindo a atribuição do valor 100 dos trechos de código anteriores:
V1 := TValue.From<Integer>(100);
Esta é uma espécie de técnica universal para mover qualquer tipo de dados para um TValue. Depois que os
dados forem atribuídos, você poderá usar vários métodos para testar seu tipo:
Observe que o IsType genérico pode ser usado para praticamente qualquer tipo de dados.
Existem métodos correspondentes para extrair os dados, mas novamente você pode usar apenas o método
compatível com os dados reais armazenados no TValue, já que nenhuma conversão está ocorrendo:
Alguns desses métodos duplicam com uma versão Try que retorna False, em vez de gerar uma exceção, no
caso de um tipo de dados incompatível. Existem também alguns métodos de conversão limitados, sendo os
mais relevantes o Cast genérico e a função ToString que já usei no código:
era
Contexto: TRttiContext;
AType: TRttiType;
Propriedade A: TRttiProperty;
ValorA: ValorTV;
Campo AF: TRttiField;
começar
AType := Context.GetType(TButton);
AProperty := AType.GetProperty(); 'Texto'
AValue := AProperty.GetValue(Remetente);
Mostrar(AValue.AsString);
Invocando Métodos
O novo RTTI estendido não apenas permite acessar valores e campos, mas também fornece uma
maneira simplificada de chamar métodos. Neste caso você tem que definir um TValue
elemento para cada parâmetro do método. Existe uma função Invoke global que você
pode chamar para executar um método:
Como alternativa melhor, existe um método sobrecarregado Invoke simplificado na classe TRt-
tiMethod :
Dois exemplos de invocação de métodos usando esta segunda forma simplificada (um retornando um valor
e o segundo exigindo um parâmetro) fazem parte do exemplo RttiAccess e estão listados abaixo:
era
Contexto: TRttiContext;
AType: TRttiType;
Método AM: Método TRtti;
TheValues: matriz de TValue;
ValorA: ValorTV;
começar
AType := context.GetType(TButton);
AMethod := 'Para sequenciar');
AType.GetMethod(TheValues:= [];
AValue := AMethod.Invoke(Sender, TheValues);
Mostrar(AValue.AsString);
AType := Context.GetType(TForm1);
AMethod := AType.GetMethod(); 'Mostrar'
SetLength(OsValores, 1);
OsValores[0] := AValor;
AMethod.Invoke(Self, TheValues);
fim;
Usando atributos
A primeira parte deste capítulo deu a você uma boa compreensão do RTTI estendido gerado pelo compilador Object
Pascal e dos recursos de acesso RTTI introduzidos pela nova unidade Rtti . Na segunda parte do capítulo podemos
finalmente nos concentrar em um dos principais motivos pelos quais toda essa arquitetura foi introduzida: a
possibilidade de definir atributos customizados e estender o RTTI gerado pelo compilador de maneiras específicas.
Veremos essa tecnologia de uma perspectiva bastante abstrata e depois nos concentraremos nas razões pelas quais
este é um importante passo à frente para o Object Pascal; e faremos isso examinando exemplos práticos.
O que é um atributo?
Um atributo em Object Pascal e C#, ou uma anotação, no jargão Java, é um comentário ou indicação que você pode
adicionar ao seu código-fonte aplicando-o a um tipo, campo, método ou propriedade que o compilador irá
incorporar. o programa. Isso geralmente é indicado entre colchetes, como em:
tipo
[MeuAtributo]
TMyClass = turma
...
Ao ler essas informações em tempo de design, em uma ferramenta de desenvolvimento ou em tempo de execução, no
aplicação final, um programa pode mudar seu comportamento dependendo dos valores que encontrar.
Geralmente os atributos não são usados para alterar os recursos principais reais de uma classe de objetos, mas
sim para permitir que essas classes especifiquem outros mecanismos dos quais podem participar. Declarar uma
classe como serializável não afeta seu código de forma alguma, mas permite que o serial -ização do código saiba que
ele pode operar nessa classe e como (caso você forneça mais
informações junto com o atributo ou outros atributos que marcam os campos ou propriedades da classe).
Foi exatamente assim que o RTTI original e limitado foi usado dentro do Object Pascal. Propriedades marcadas
como publicadas podem aparecer no inspetor de objetos, ser transmitidas para um arquivo DFM e acessadas
em tempo de execução. Os atributos permitem que esse mecanismo se torne muito mais flexível e poderoso. Eles
também são muito mais complexos de usar e fáceis de usar indevidamente, assim como quaisquer recursos de
linguagem poderosos. Além disso, só porque esse recurso existe
não significa que você deva jogar fora todas as coisas boas que sabe sobre programação orientada a
objetos para adotar esse novo modelo, mas sim complementar uma com a outra.
Por exemplo, uma classe de funcionário ainda será representada numa hierarquia como uma classe
derivada de uma classe de pessoa; um objeto funcionário ainda terá um ID para seu crachá; mas você
pode “marcar” ou “anotar” a classe do funcionário como uma classe que pode ser mapeada para uma
tabela de banco de dados ou exibida por um formulário de tempo de execução específico. Portanto,
temos herança (é-a), propriedade (tem-a) e anotações (marcadas como) como três mecanismos separados
que você pode usar ao projetar um aplicativo para um design mais flexível e desacoplado.
Depois de ver os recursos do compilador que suportam atributos personalizados no Object Pascal e
observar alguns exemplos práticos, a ideia abstrata que acabei de mencionar deve se tornar mais
compreensível, ou pelo menos essa é a minha esperança!
Como você define uma nova classe de atributos (ou categoria de atributos)? Você deve herdar da nova
classe TCustomAttribute disponível na unidade System :
tipo
SimpleAttribute = classe(TCustomAttribute)
fim;
O nome da classe que você der à classe de atributo se tornará o símbolo a ser usado no código-
fonte, com a exclusão opcional do sufixo Attribute . Então, se você nomear sua classe como
SimpleAttribute, você poderá usar, no código, um atributo chamado Simple ou SimpleAttribute. Por esta
razão o clássico T inicial para classes Object Pascal geralmente não é usado no caso de atributos, sendo
a principal exceção System.TCustomAttribute.
tipo
[Simples]
TMyClass = classe(TObject)
público
[Simples]
procedimento Um;
tipo
ValueAttribute = classe(TCustomAttribute)
privado
FValor: Inteiro;
público
construtor Criar (N: Inteiro);
tipo
[Valor(22)]
TMyClass = classe(TObject)
público
[Valor(0)]
procedimento dois;
Os valores dos atributos, passados ao seu construtor, devem ser expressões constantes, pois são
resolvidos em tempo de compilação. É por isso que você está limitado a apenas alguns tipos de
dados: valores ordinais, strings, conjuntos e referências de classe. Do lado positivo, você
pode ter vários construtores sobrecarregados com parâmetros diferentes. Observe que você pode
aplicar vários atributos ao mesmo símbolo, como fiz no exemplo RttiAttrib , que resume os
trechos de código desta seção:
tipo
[Simples][Valor(22)]
TMyClass = classe(TObject)
público
[Simples]
procedimento Um;
[Valor(0)]
procedimento dois;
fim;
E se você tentar usar um atributo que não está definido (talvez por causa de uma instrução de uso
ausente)? Infelizmente, você recebe uma mensagem de aviso muito enganosa:
O fato de ser um aviso implica que o atributo será ignorado, então você deve tomar cuidado
para esses avisos, como você deveria fazer em qualquer caso de aviso, ou melhor ainda, tratar o
aviso de “recurso de idioma não suportado” como um erro (algo que você pode fazer no
Página Dicas e Avisos da caixa de diálogo Opções do Projeto):
note A refatoração de atributos foi mencionada pela primeira vez por Malcolm Groves em
seu blog em http://www.malcolmgroves.com/blog/?p=554
Atributos de navegação
Agora, esse código pareceria totalmente inútil se não houvesse uma maneira de descobrir quais
atributos estão definidos e possivelmente injetar um comportamento diferente em um objeto por causa
desses atributos. Deixe-me começar a focar na primeira parte. As classes da unidade Rtti permitem
descobrir quais símbolos possuem atributos associados.
Este é o código, extraído do exemplo RttiAttrib , mostrando a lista de atributos da classe atual:
procedimento TMyClass.One;
era
Contexto: TRttiContext; Atributos:
TArray<TCustomAttribute>; Atributo: TCustomAttribute;
Atributos iniciais :=
Form39.Log(Atributo.ClassName);
Atributo Simples
Atributo de valor
Você pode estendê-lo adicionando o seguinte código ao loop for-in para extrair o valor específico do tipo de
atributo fornecido:
Que tal buscar os métodos com um determinado atributo, ou com qualquer atributo? Você não pode
filtrar os métodos antecipadamente, mas tem que passar por cada um deles, verificar seus atributos e
ver se é relevante para você. Para ajudar nesse processo, escrevi uma função que verifica se um
método suporta um determinado atributo:
tipo
TCustomAttributeClass = classe de TCustomAttribute;
A função HasAttribute é chamada pelo programa RttiAttrib para verificar o atributo Simple específico e
ao mesmo tempo listar quaisquer outros atributos que encontrar:
era
Contexto: TRttiContext;
AType: TRttiType;
Método AM: Método TRtti;
começar
AType := Context.GetType(TMyClass);
O efeito é listar os métodos marcados com o atributo fornecido ou com qualquer atributo:
Em vez de simplesmente descrever atributos, o que geralmente se faz é adicionar algum comportamento
independente determinado pelos atributos de uma classe, em vez de seu código real. Como exemplo,
posso injetar um comportamento específico no código anterior: O objetivo poderia ser chamar todos os
métodos de uma classe marcada com um determinado atributo, considerando-os como métodos sem
parâmetros:
AMethod.Invoke(ATarget, ZeroParams);
finalmente
ATarget.Grátis;
fim;
fim;
O que esse trecho de código faz é criar um objeto, capturar seu tipo, verificar um determinado
atributo e invocar cada método que possui o atributo Simple . Em vez de herdar
a partir de uma classe base, implementando uma interface ou escrevendo um código específico para
realizar a solicitação, tudo o que precisamos fazer para obter o novo comportamento é marcar um ou mais
métodos com um determinado atributo. Não que este exemplo torne o uso de atributos extremamente óbvio.
Para alguns padrões comuns no uso de atributos e alguns estudos de caso reais, você pode consultar a
parte final deste capítulo.
Por que você quer fazer isso? Em uma aplicação Object Pascal padrão, você provavelmente não usaria
esse recurso. Se você precisar de um objeto com comportamento diferente, basta alterá-lo ou criar uma
subclasse. As coisas são diferentes para as bibliotecas, pois as bibliotecas devem ser escritas de uma
forma muito genérica, sabendo pouco sobre os objetos que poderão manipular, e
impondo o mínimo de carga possível sobre os próprios objetos. Este é o tipo de cenário que os
interceptores de métodos virtuais foram adicionados para manipular no Object Pascal.
note Uma postagem de blog muito detalhada de Barry Kelly sobre Virtual Method Interceptors (ao qual devo muito) está em
disponível em http://blog.barrkel.com/2010/09/virtual-method-interception.html.
Antes de nos concentrarmos nos cenários possíveis, deixe-me discutir a tecnologia em si. Suponha que
você tenha uma classe existente com pelo menos um método virtual, como o seguinte:
tipo
TPpessoa = turma
...
público
nome da propriedade: string lida FName escreve SetName;
propriedade Data de nascimento: TDate leitura FBirthdate gravação SetBirthdate;
Suporte, você tem um objeto FPerson1 desta classe. Agora o que você pode fazer é criar um
objeto TVirtualMethodInterceptor (uma nova classe definida na unidade RTTI) vinculado à classe do
objeto que deseja subclassificar (TPerson), alterando a propriedade estática do objeto FPerson1 .
classe para a dinâmica:
era
FVmi: TVirtualMethodInterceptor;
começar
FVmi := TVirtualMethodInterceptor.Create(TPerson);
FVmi.Proxify(FPerson1);
Depois de ter o objeto FVmi , você pode instalar manipuladores especiais para seus eventos (OnBefore,
OnAfter e OnException) usando métodos anônimos. Eles serão acionados antes de qualquer
chamada de método virtual, após qualquer chamada de método virtual e no caso de uma
exceção em um método virtual. Estas são as assinaturas para os três tipos de métodos anônimos:
No exemplo InterceptBaseClass , que usa a classe TPerson acima, interceptei os métodos virtuais
da classe com este código de registro:
FVmi := TVirtualMethodInterceptor.Create(TPerson);
FVmi.OnBefore := procedimento(Instância: TObject; Método: TRttiMethod;
const Args: TArray<TValue>; fora DoInvoke: Boolean;
resultado: TValue)
começar
'
'Antes de ligar + Método.Nome);
Mostrar(fim;
FVmi.OnAfter := procedimento(Instância: TObject; Método: TRttiMethod;
const Args: TArray<TValue>; var Resultado: TValue)
começar
'
'Depois de ligar + Método.Nome);
Mostrar(fim;
FVmi.Proxify(FPerson1);
fim;
Observe que o objeto FVmi precisa ser mantido pelo menos até que o objeto FPerson1 esteja em uso,
ou você usará uma classe dinâmica que não está mais disponível e chamará métodos anônimos
que já foram lançados. Na demonstração salvei-o como um campo de formulário (FVmi), assim como
o objeto ao qual se refere (FPerson1).
O programa usa o objeto chamando seus métodos e verificando o nome da classe base:
'
Mostrar( 'Idade: + IntToStr(FPerson1.Idade));
'
Mostrar( 'Pessoa:
'Aula: ' +FPerson1.ToString);
Mostrar( +FPerson1.ClassName);
'
Mostrar( 'Classe base: + FPerson1.ClassParent.ClassName);
Idade: 26
Pessoa: Mark tem 26 anos
Classe: TP Pessoa
Classe Base: TObject
Observe que a classe tem o mesmo nome da classe base, mas na verdade é uma classe
diferente, a classe dinâmica criada pelo interceptador de método virtual.
Embora não exista uma maneira oficial de restaurar a classe do objeto de destino para o original,
a classe em si está disponível no objeto FVmi e também como classe base do objeto.
Ainda assim, você pode usar força bruta para atribuir aos dados de classe do objeto (seus
quatro bytes iniciais) a referência de classe correta:
PPointer(FPerson1)^ := FVmi.OriginalClass;
Como exemplo adicional, modifiquei o código OnBefore para que, caso você esteja ligando
Age, ele retorna um determinado valor e ignora a execução do método real:
A saída muda para o seguinte (observe que as chamadas Age e o relativo OnAfter
eventos são ignorados):
Agora que vimos os detalhes técnicos por trás dos interceptadores de métodos virtuais, podemos
volte para descobrir em quais cenários você gostaria de usar esse recurso.
Novamente, basicamente não há razão para usar isso em um aplicativo padrão. O foco, por outro
lado, é principalmente para quem desenvolve bibliotecas avançadas e precisa implementar um
comportamento customizado para testar ou processar objetos.
Por exemplo, isso poderia ajudar a construir uma biblioteca de testes unitários, embora fosse limitada
apenas a métodos virtuais. Você também possivelmente usaria isso junto com atributos personalizados
para implementar um estilo de codificação semelhante à programação orientada a aspectos.
ções. Em vez disso, o que farei é guiá-lo no desenvolvimento passo a passo de dois exemplos simples, mas
significativos.
O primeiro programa de demonstração mostrará o uso de atributos para identificar informações específicas dentro de
uma classe. Em particular, queremos ser capazes de inspecionar um objeto de uma classe que declara fazer parte
de uma arquitetura e possuir uma descrição e um ID único referente ao próprio objeto. Isso pode ser útil em diversas
situações, como na descrição de objetos armazenados em uma coleção (seja genérica ou tradicional).
A segunda demonstração será um exemplo de streaming, especificamente streaming de uma classe para um arquivo
XML. Começarei pela abordagem clássica de usar o RTTI publicado, para depois passar para o novo RTTI estendido
e, finalmente, mostrarei como você pode usar atributos para personalizar o código e torná-lo mais flexível.
Uma técnica padrão para superar esta situação é usar uma interface em vez de uma classe base comum.
Múltiplas classes implementando a interface (mas sem nenhuma classe ancestral comum) podem fornecer uma
implementação dos métodos da interface, que agem muito
da mesma forma que os métodos virtuais.
Um estilo totalmente diferente (com vantagens e desvantagens) é o uso de atributos para marcar classes
participantes e determinados métodos (ou propriedades). Isto abre mais flexibilidade, não envolve interfaces, mas é
baseado em um sistema comparativamente lento e
processo de pesquisa de informações em tempo de execução propenso a erros, em vez de uma resolução em
tempo de compilação. Isso significa que não estou defendendo esse estilo de codificação em vez de
interfaces como uma abordagem melhor, apenas como algo que pode valer a pena avaliar e ser interessante de
usar em algumas circunstâncias.
tipo
TDescriptionAttrKind = (dakClass, dakDescription, dakId);
DescriçãoAttribute = class(TCustomAttribute)
privado
FDak: TDescriptionAttrKind; construtor
público
Create(ADak: TDescriptionAttrKind = dakClass); propriedade Tipo: TDescriptionAttrKind lê
FDak; fim;
Observe o uso do construtor com um valor padrão para seu único parâmetro para permitir que você use
o atributo sem parâmetros.
As aulas de amostra
A seguir escrevi duas classes de exemplo que usam o atributo. Cada classe é marcada com o atributo
e possui dois métodos marcados com o mesmo atributo customizados com diferentes tipos.
O primeiro (TPerson) tem a descrição mapeada para a função GetName e usa seu método
TObject.GetHashCode para fornecer um ID temporário, declarando novamente o método para aplicar o
atributo a ele (o código do método é simplesmente uma chamada para a versão herdada ) :
tipo
[Descrição]
TPpessoa = aula
particular
Data de nascimento: TDate;
NomeF: string;
FCaís: string; procedimento
SetBirthdate(const Valor: TDate); procedimento SetCountry(const
Valor: string); procedimento SetName(const Valor: string); público
A segunda classe (TCompany) é ainda mais simples, pois possui valores próprios para o ID e a descrição:
tipo
[Descrição]
TCompany = classe
privada
NomeF: string;
FCaís: string;
FID: sequência;
procedimento SetName(const Valor: string); procedimento
SetID (valor const: string);
público
[Descrição(dakDescrição)]
função GetNome: string;
[Descrição(dakID)]
função GetID: string;
Publicados
nome da propriedade: string read GetName write SetName;
propriedade País: string lida FCountry escreve FCountry;
ID da propriedade: string lida FID gravação SetID;
fim;
Mesmo que existam semelhanças entre as duas classes, elas não estão totalmente relacionadas em
termos de hierarquia, interface comum ou algo parecido. O que eles compartilham é o uso do mesmo
atributo.
FObjectsList: TObjectList<TObject>;
// Adicione um pessoa
APerson := TPerson.Create;
APessoa.Nome := 'Wiley' ;
APerson.Country := 'Deserto' ;
APerson.BirthDate := Data - 1000;
FObjectsList.Add(APerson);
// Adicione um empresa
AEmpresa := TEmpresa.Create;
AEmpresa.Nome := ; 'ACME Inc.'
ACompany.ID := IntToStr(GetTickCount);
AEmpresa.País := 'Mundialmente' ;
FObjectsList.Add(AEmpresa);
era
Atributo: TCustomAttribute; comece
para
Attrib em AType.GetAttributes comece se Attrib for
note Neste caso você precisa verificar o nome completo da classe, DescriptionAttribute, e não apenas
“Descrição”, que é o símbolo que você pode usar ao aplicar o atributo, pois o compilador
reconhece o nome abreviado apenas quando usado entre colchetes.
Se for esse o caso, o programa prossegue obtendo cada atributo de cada método, com um loop
aninhado, e verificando se este é o atributo que procuramos:
No centro do loop, os métodos marcados com atributos são invocados para ler os resultados em
duas strings temporárias (posteriormente adicionadas à interface do usuário):
O que o programa não consegue fazer é verificar se um atributo está duplicado (ou seja, se existem
vários métodos marcados com o mesmo atributo, situação em que você pode querer gerar uma
exceção). Resumindo todos os trechos da página anterior, este é o código completo do método
UpdateList :
procedimento TFormDescrAttr.UpdateList;
era
UmObjeto: TObject;
Contexto: TRttiContext;
AType: TRttiType;
Atributo: TCustomAttribute;
Método AM: Método TRtti;
StrDescr,StrID: string; comece para
AType := Context.GetType(AnObject.ClassInfo);
se TypeHasDescription(AType) então
começar
para AMethod em AType.GetMethods faça
para Atributo em AMethod.GetAttributes faça
se Attrib for DescriptionAttribute então
caso DescriçãoAtributo(Atributo).Tipo de
dakClass: ; // Ignorar
dakDescrição:
// Deve verificar se o atributo está duplicado
StrDescr := aMethod.Invoke(
AnObject, []).ToString;
dataId:
StrID := AMethod.Invoke(
AnObject, []).ToString;
fim;
// Parei de procurar atributos
// Deve verificar se encontrounós
alguma coisa
com ListView1.Items.Add faça
começar
Texto := STypeName;
Detalhe := StrDescr;
fim;
fim;
fim;
// Caso contrário, ignore o objeto, poderia levantar um exceção
fim;
Se este programa produz resultados bastante desinteressantes, a forma como isso é feito é relevante, pois
marquei algumas classes e dois métodos dessas classes com um atributo e consegui processar essas classes
com um algoritmo externo.
Em outras palavras, as próprias classes não precisam de nenhuma classe base específica, nem de
implementação de interface, nem de qualquer código interno para fazer parte da arquitetura, mas apenas precisam declarar
eles querem participar usando atributos. A responsabilidade total pelo gerenciamento das classes está em
algum código externo.
Transmissão XML
Um caso interessante e muito útil para usar RTTI é criar uma representação “externa” portátil de um objeto,
para salvar seu status em um arquivo ou enviá-lo para outra aplicação. Tradicionalmente, a abordagem Object
Pascal para este problema tem sido
para transmitir as propriedades publicadas de um objeto, a mesma abordagem usada ao criar arquivos DFM.
Uma abordagem opcional tem sido escrever ou usar estruturas de streaming personalizadas para serializar
e desserializar objetos de e para streams.
Agora o RTTI permite salvar os dados reais do objeto, seus campos, em vez da interface externa. Isto é
mais poderoso, embora possa levar a uma complexidade extra, por
exemplo na gestão dos dados de objetos internos. Novamente, a demonstração funciona como uma
simples vitrine da técnica e não se aprofunda em todas as suas implicações.
Este exemplo vem em três versões compiladas em um único projeto para simplificar. A primeira é a
abordagem tradicional do Object Pascal baseada em propriedades publicadas, a segunda usa RTTI
estendido e campos, a terceira usa atributos para customizar o mapeamento de dados.
Para ajudar na geração do XML, baseei o exemplo XmlPersist em uma versão estendida de uma
classe TTrivialXmlWriter que escrevi originalmente em meu Delphi 2009 Handbook para demonstrar o
uso da classe TTextWriter , que não quero abordar
novamente aqui. Basta dizer que a classe pode acompanhar os nós XML que abre, graças a uma pilha
de strings, e fechar os nós XML em uma ordem LIFO (último a entrar, primeiro a sair).
note O código fonte da classe TTrivialXmlWriter , do Delphi 2009 Handbook, pode ser encontrado em http://github.com/
MarcoDelphiBooks/Delphi2009Handbook/tree/master/07/ReaderWriter
À classe original adicionei algum código de formatação limitada e três métodos para salvar um objeto,
com base nas três abordagens diferentes que explorarei nesta seção. Esta é a declaração completa da
classe:
tipo
TTrivialXmlWriter = classe
privado
FWriter: TTextWriter;
FNodes: TStack<string>;
FOwnsTextWriter: Booleano;
público
construtor Criar (AWriter: TTextWriter); sobrecarga;
construtor Criar (AStream: TStream); sobrecarga;
destruidor Destruir; sobrepor;
procedimento WriteStartElement(const SName: string);
procedimento WriteEndElement(Recuo: Boolean = False);
procedimento WriteString(const SValue: string);
procedimento WriteObjectPublished(AnObj: TObject);
procedimento WriteObjectRtti(AnObj: TObject);
procedimento WriteObjectAttrib(AnObj: TObject);
função Recuo: string;
fim;
Para se ter uma ideia do código, este é o método WriteStartElement , que utiliza o
Função de recuo para deixar o dobro de espaços do número atual de nós na pilha interna:
fim;
NProps := GetTypeData(AnObj.ClassInfo)^.PropCount;
GetMem(PropList, NProps * SizeOf(Pointer));
GetPropInfos(AnObj.ClassInfo, PropList);
para I := 0 para NProps - 1 faça
...
O que isso faz é solicitar o número de propriedades, alocar uma estrutura de dados de
tamanho adequado e preencher a estrutura de dados com informações sobre as propriedades publicadas.
Caso você esteja se perguntando se poderia escrever esse código de baixo nível? Bem, você
acabou de encontrar uma boa razão para a introdução do novo RTTI: para simplificar e ocultar a
complexidade do antigo RTTI.
Para cada propriedade, o programa extrai o valor das propriedades dos tipos numérico e string,
enquanto extrai qualquer subobjeto e atua recursivamente sobre ele:
StrPropName := UTF8ToString(PropList[i].Nome);
case PropList[i].PropType^.Tipo de
tkInteger, tkEnumeration, tkString, tkUString, ...:
começar
WriteStartElement(StrPropNome);
WriteString(GetPropValue(AnObj, StrPropName));
WriteEndElement;
fim;
Classe tk:
começar
InternalObject := GetObjectProp(AnObj, StrPropName);
// Recurso na subclasse
WriteStartElement(StrPropNome);
WriteObjectPublished(InternalObject como TPersistent);
WriteEndElement(Verdadeiro);
fim;
fim;
Existe alguma complexidade extra, mas, por exemplo, e para lhe dar uma ideia da abordagem
tradicional, isso deve ser suficiente.
Para demonstrar o efeito do programa escrevi duas classes (TCompany e TPer-son) adaptadas do exemplo
anterior. Desta vez, porém, a empresa pode ter uma pessoa designada para uma propriedade extra,
chamada Boss. No mundo real isto seria mais complexo, mas, para este exemplo, é uma suposição razoável.
Estas são as propriedades publicadas das duas classes:
tipo
TPpessoa = classe(TPersistente)
...
propriedade
publicada Nome: string lida FName escreve FName; propriedade
País: string lida FCountry escreve FCountry; fim;
TEmpresa = classe(TPersistente)
...
propriedade
publicada Nome: string lida FName escreve FName; propriedade
País: string lida FCountry escreve FCountry; ID da propriedade: string lida FID
gravação FID; propriedade Chefe: TPerson lê FPerson
escreve FPerson; fim;
A forma principal do programa possui um botão utilizado para criar e conectar dois objetos dessas duas
classes e salvá-los em um fluxo XML, que posteriormente será exibido. A seção de streaming possui o
seguinte código:
SS := TStringStream.Create; XmlWri :=
TTrivialXmlWriter.Create(SS);
'Empresa' );
XmlWri.WriteStartElement(XmlWri.WriteObjectPublished(ACompany);
XmlWri.WriteEndElement;
<Empresa>
<Nome>ACME Inc.</Nome>
<País>Em todo o mundo</País>
<ID>29088851</ID>
<Chefe>
<Nome>Wiley</Nome>
<País>Deserto</País>
</Chefe>
</Empresa>
com código de nível muito superior. O código completo do método WriteObjectRtti é o seguinte:
WriteObjectRtti(AField.GetValue(AnObj).AsObject);
WriteEndElement(Verdadeiro); fim senão início WriteStartElement(AField.Name);
WriteString(AField.GetValue(AnObj).ToString);
WriteEndElement; fim; fim; fim;
O XML resultante é um pouco semelhante, mas de alguma forma menos limpo, pois os nomes dos campos são
geralmente menos legíveis do que os nomes das propriedades:
<Empresa>
<FName>ACME Inc.</FName>
<FCountry>Em todo o mundo</FCountry>
<FID>29470148</FID>
<FPerson>
<FName>Wiley</FName>
<FCountry>Deserto</FCountry>
</FPerson>
</Empresa>
Outra grande diferença, porém, é que, para que isso funcionasse, as classes não precisavam herdar da classe
TPersistent nem ser compiladas com nenhuma opção especial.
note O streaming das propriedades do Object Pascal pode ser controlado usando a diretiva armazenada , que pode ser lida
usando a unidade TypInfo . Ainda assim, esta solução está longe de ser simples e limpa, mesmo que o mecanismo
de streaming DFM a utilize de forma eficaz.
Esses são problemas que podemos resolver usando atributos, embora a desvantagem seja ter que usá-los
bastante na declaração de nossas classes, um estilo de codificação que não gosto muito. Para a nova
versão do código, defini um construtor de atributos com um parâmetro opcional:
tipo
XmlAttribute = classe(TCustomAttribute) privado
FTag: string;
construtor ''
público Create(StrTag: string = propriedade );
TagName: string lida FTag; end;
O código de streaming baseado em atributos é uma variação da última versão baseada no RTTI
estendido. A única diferença é que agora o programa chama a função auxiliar CheckXmlAttr para
verificar se o campo possui o atributo Xml e a decoração opcional do nome da tag:
WriteStartElement(StrTagName);
WriteObjectAttrib(AField.GetValue(AnObj).AsObject);
WriteEndElement(Verdadeiro); fim senão início WriteStartElement(StrTagName);
WriteString(AField.GetValue(AnObj).ToString);
WriteEndElement; fim; fim; fim; fim;
Os campos sem o atributo XML são ignorados e a tag usada na saída XML é personalizável. Para demonstrar
isso, o programa possui as seguintes classes (desta vez omiti as propriedades publicadas da listagem, pois não
são relevantes):
tipo
TAttrPerson = turma
privado
[xml()] 'Nome'
NomeF: string;
[xml]
FCaís: string;
...
TAttrEmpresa = classe
privado
[xml( 'Nome da empresa')]
NomeF: string;
[xml()] 'País'
FCaís: string;
FID: sequência; // Omitido
[xml()] 'O chefe'
FPerson: TAttrPerson;
...
Com essas declarações, a saída XML será semelhante à seguinte (observe o nome da tag, o fato de o ID ser
omitido e o nome padrão (de aparência feia) para o FCountry
campo):
<Empresa>
<NomedaEmpresa>ACME Inc.</NomeEmpresa>
<País>Em todo o mundo</País>
<O Chefe>
<Nome>Wiley</Nome>
<FCountry>Deserto</FCountry>
</TheBoss>
</Empresa>
A diferença aqui é que podemos ser muito flexíveis sobre quais campos incluir e como nomeá-los no XML, algo
que as versões anteriores não permitiam.
Mesmo que esta seja apenas uma implementação muito esquelética, acho que dar a você a oportunidade de ver a
versão final sendo criada passo a passo, começando com o RTTI clássico, deu a você uma boa noção das
diferenças entre as várias técnicas.
O que é importante ter em mente, na verdade, é que não é certo que usar atributos sempre será a melhor solução!
Por outro lado, deve ficar claro que o RTTI e os atributos agregam muito poder e flexibilidade em qualquer cenário
em que seja necessário inspecionar
a estrutura de um objeto desconhecido em tempo de execução.
Mesmo que eu não queira me aprofundar muito neste tópico, que na verdade é uma biblioteca específica e não faz
parte da linguagem ou do sistema central, acho que uma pequena listagem pode lhe dar uma ideia disso:
era
BindExpr: TBindingExpression;
Pessoa: TPpessoa;
começar
Pessoa := TPpessoa.Create;
tentar
NomePessoal := 'João';
Cidade Pessoal := 'São Francisco';
BindExpr := TBindingExpressionDefault.Create;
tentar
"
BindExpr.Source := 'pessoa.nome + mora em " + pessoa.cidade' );
BindExpr.Compile([
TBindingAssociation.Create(Pers, 'pessoa' )]);
Show(BindExpr.Evaluate.GetValue.ToString);
finalmente
BindExpr.Free;
fim;
finalmente
Pessoal Grátis;
fim;
fim;
Observe que a vantagem aqui vem do fato de você poder alterar a expressão em tempo de execução
(embora, no trecho específico acima, seja uma string constante). A expressão
pode vir de uma edição ou pode ser escolhido dinamicamente em várias expressões possíveis.
É primeiro atribuído ao objeto TBindingExpression e depois analisado e compilado
(que é transformado em uma forma simbólica, não em código assembly) em tempo de execução com a
chamada Com-pile . Ele então usará RTTI quando executado para acessar o objeto TPerson .
17: objeto e o
Unidade de sistema
No coração de qualquer aplicação Object Pascal existe uma hierarquia de classes. Cada classe no
sistema é, em última análise, uma subclasse da classe TObject , portanto, toda a hierarquia tem
uma única raiz. Isso permite usar o tipo de dados TObject como um substituto para o tipo de dados
de qualquer tipo de classe no sistema.
A classe TObject é definida em uma unidade RTL central chamada System, que tem um
papel tão importante que é automaticamente incluída em cada compilação. Embora eu não
aborde todas as classes de unidades do sistema e outras funções de unidades do sistema, há
algumas que vale a pena aprofundar e TObject é certamente a mais importante.
nota Poderia ser debatido longamente se uma classe central do sistema como TObject faz parte da linguagem ou se faz parte de
a Biblioteca de Tempo de Execução (RTL). O mesmo vale para outras funcionalidades da unidade do Sistema , unidade
tão crítica que é automaticamente incluída na compilação de qualquer outra unidade. (Na verdade, é ilegal adicioná-lo a um
usa caluse.) Tal debate seria bastante fútil, então deixarei isso para outro momento.
A classe TObject
Como acabei de mencionar, a classe TObject é muito especial, pois todas as outras classes herdam dela.
Quando você está declarando uma nova classe, na verdade, se você não estiver indicando uma classe base,
a classe herdará automaticamente de TObject. Em termos de linguagem de programação, esse tipo de cenário é
chamado de hierarquia de classes de raiz única, um recurso que o Object Pascal compartilha com C#, Java e
algumas outras linguagens de programação modernas.
A exceção notável é C++, que não tem conceito de uma única classe base e permite definir múltiplas
hierarquias de classes totalmente separadas.
A classe base TObject não é uma classe que você usaria diretamente criando instâncias dela. No entanto, é
uma classe que você acabará usando sempre que precisar de uma variável que possa conter um objeto de
qualquer tipo e declará-la como uma classe. Um bom exemplo desse uso está em bibliotecas de componentes
e seus manipuladores de eventos que muitas vezes usam TObject como tipo do primeiro parâmetro, geralmente
chamado de Sender. Isso significa que qualquer objeto de qualquer classe pode ser o Remetente. Muitas
coleções genéricas também são coleções de objetos e há vários cenários nos quais o tipo TObject é usado
diretamente.
Construção e Destruição
Embora não faça muito sentido criar um TObject diretamente, o construtor e o destruidor desta classe são
importantes, pois são herdados automaticamente por todas as outras classes. Se você definir uma classe sem
construtor, ainda poderá chamar Create nela, invocando o construtor TObject , que é um método vazio (já
que não há nada para inicializar nesta classe base). Este construtor Create não é virtual e você o substitui
totalmente em suas classes, a menos que esse construtor do-nothing seja bom o suficiente. Chamar o
construtor da classe base é uma boa prática para qualquer subclasse, mesmo que uma chamada direta para
TObject.Create não seja particularmente útil.
note que sublinhei que este é um construtor não virtual porque existe outra classe de biblioteca principal, TCompo-
nent, que define um construtor virtual. O construtor virtual da classe TComponent desempenha uma chave
papel no sistema de streaming, abordado no próximo capítulo.
Para destruir um objeto, a classe TObject possui um método Free (que eventualmente chama o destruidor
Destroy ). Abordei isso detalhadamente no Capítulo 13, juntamente com muitas sugestões sobre o
gerenciamento correto da memória, portanto não há necessidade de reiterá-las aqui.
mais uma vez.
Conhecendo um objeto
Um grupo interessante de métodos da classe TObject são aqueles que retornam informações
sobre o tipo. Os mais comumente usados são os métodos ClassType e ClassName . O método
ClassName retorna uma string com o nome da classe. Por ser um método de classe (como um
grande número de métodos da classe TObject ), você pode aplicá-lo tanto a um objeto quanto a uma
classe. Suponha que você tenha definido uma classe TButton e um Button1
objeto dessa classe. Então as seguintes afirmações têm o mesmo efeito:
Texto := Button1.ClassName;
Texto := TButton.ClassName;
Claro, você também pode aplicá-los a um TObject genérico e não obterá o TObject
informações, mas informações sobre a classe específica do objeto atualmente atribuído a
a variável. Por exemplo, no manipulador de eventos OnClick de um botão, chamando:
Texto := Remetente.ClassName;
provavelmente retornaria o mesmo das linhas acima, que é a string 'TButton'. Isso ocorre porque
o nome da classe é determinado em tempo de execução (pelo próprio objeto específico) e não pelo
compilador (que apenas pensará que se trata de um objeto TObject ). É claro que, se a referência
não for atribuída, ou seja, a variável for nula, a tentativa de chamar qualquer um desses métodos
de classe gerará uma exceção.
Embora obter o nome da classe possa ser útil para depuração, registro e exibição de informações
da classe em geral, geralmente é mais importante acessar a referência de classe da classe. Por
exemplo, é melhor comparar duas referências de classe, que são endereços numéricos
simples, do que as strings com os nomes das classes, que são estruturas de dados e, portanto,
levam mais ciclos de CPU para serem avaliadas. Podemos obter referências de classe com o
método ClassType , enquanto o método ClassParent retorna uma referência de classe para a base
classe da atual, permitindo navegar até a lista de classes-pai, ou seja, classes-base; com o método
retornando nulo para TObject, pois é a classe raiz e, como tal, não possui classe pai. Depois de ter
uma referência de classe, você poderá usá-la para chamar qualquer método de classe, incluindo o
método ClassName .
Outro método muito interessante que retorna informações sobre uma classe é InstanceSize, que
retorna o tamanho de tempo de execução de um objeto, ou seja, a quantidade de memória necessária
para seus campos (e aqueles herdados das classes base). Este é um recurso utilizado internamente
quando o sistema precisa alocar uma nova instância da classe.
note Embora você possa pensar que a função global SizeOf também fornece essas informações, essa função
na verdade, retorna o tamanho de uma referência de objeto – um ponteiro, que invariavelmente tem quatro
ou oito bytes, dependendo da plataforma de destino – em vez do tamanho do próprio objeto. Por
outro lado, InstanceSize retorna o tamanho dos campos, mas essa não é a quantidade real de memória usada pelo
objeto, pois os campos podem ser referências a strings e outros objetos na memória, que requerem
espaço adicional.
· ClassName retorna uma string com o nome da classe, usada principalmente para fins de exibição
poses.
· ClassParent retorna uma referência de classe para a classe pai da classe atual ou
classe do objeto. Você pode navegar de ClassParent para ClassParent, até chegar à própria classe TObject ,
ponto em que este método retorna nulo.
· ClassInfo retorna um ponteiro para informações internas de tipo de tempo de execução de baixo nível
(RTTI) da turma. Isso foi usado nos primeiros dias da unidade TypInfo , mas agora foi substituído pelos recursos
da unidade RTTI , conforme abordado no Capítulo 16. Internamente, ainda é assim que o RTTI é buscado
para uma classe.
· ClassType retorna uma referência à classe do objeto (este não é um método de classe e
não pode ser aplicado diretamente a uma classe). Como se trata de uma referência à classe do objeto, duas
classes com o mesmo nome, mas declaradas em unidades diferentes, não corresponderão, o que é correto,
pois Delphi é uma linguagem fortemente tipada.
· InstanceSize retorna o tamanho dos dados do objeto em bytes. Esta é uma soma
campos, além de alguns bytes reservados especiais extras (incluindo, por exemplo, a referência de classe,
mas não incluindo a memória real usada por strings e objetos internos).
Observe, mais uma coisa, este é o tamanho da instância, enquanto a referência a uma instância é
apenas enquanto um ponteiro (4 ou 8 bytes, dependendo da plataforma).
· UnitName retorna o nome da unidade na qual a classe está definida, o que pode ser útil para descrever uma
classe. O nome da classe, na verdade, não é único no sistema. Como vimos no capítulo anterior, apenas o
nome da classe qualificada (formado pelo nome da unidade e
o nome da classe, separado por um ponto) é exclusivo em um aplicativo.
Esses métodos de TObject estão disponíveis para objetos de todas as classes, já que TObject é a classe ancestral
comum de todas as classes Delphi.
Aqui está como podemos usar esses métodos para acessar informações de classe:
'Tamanho da instância:
'
Memo1.Lines.Add( + IntToStr(Sender.InstanceSize));
Você também pode verificar se o parâmetro Sender corresponde a um determinado objeto, com isso
teste:
Em vez de verificar uma classe ou objeto específico, geralmente você precisará testar a
compatibilidade de tipo de um objeto com uma determinada classe; isto é, você precisará
verificar se a classe do objeto é uma determinada classe ou uma de suas subclasses. Isso
permite saber se você pode operar no objeto com os métodos definidos para a classe. Este
teste pode ser realizado usando o método InheritsFrom , que também é chamado quando você usa o m
operador, com a única diferença de que o operador is lida com nulo. Os dois testes a seguir são equivalentes:
ListParent.Items.Clear;
enquanto MyClass.ClassParent <> nada faz
começar
MinhaClasse := MinhaClasse.ClassParent;
ListParent.Items.Add(MinhaClasse.ClassName);
fim;
Você notará que usamos uma referência de classe no centro do loop while , que testa a ausência de uma
classe pai (nesse caso, a classe atual é TObject). Alternativamente, poderíamos ter escrito a instrução while
de uma das seguintes maneiras:
note Se você usou o .NET framework, reconhecerá imediatamente que esses métodos fazem parte do Sys-
classe tem.Object da biblioteca de classes base C#. Métodos semelhantes são usados para as classes base
disponíveis em Java e são comumente usados em JavaScript e em outras linguagens. A origem de alguns deles, como
a de toString, pode ser rastreada até Smalltalk, que é considerada a primeira linguagem OOP
medidor.
O método ToString
A função virtual ToString é um placeholder para retornar a representação textual (uma descrição
ou mesmo uma serialização) de um determinado objeto. A implementação padrão do método
na classe TObject retorna o nome da classe:
Claro, isso está longe de ser útil. Em teoria, cada classe deveria fornecer uma maneira de se descrever
para um usuário, por exemplo, quando um objeto é adicionado a uma lista visual. Algumas das classes
na biblioteca de tempo de execução substituem a função virtual ToString , como TStringBuilder,
TStringWriter e a classe Exception , para retornar as mensagens em uma lista de exceções (conforme
abordado na seção “O mecanismo de exceção interno” do Capítulo 9).
Ter uma forma padrão de retornar a representação de string de qualquer objeto é uma ideia
bastante interessante, e recomendo que você aproveite esse recurso central da classe
TObject , tratando-o como um recurso de linguagem.
note Observe que o método ToString “sobrecarrega semanticamente” a “string de token de análise” ou o símbolo
toString definido na unidade Classes . Por esta razão esse símbolo é referenciado como Classes.toString.
O método igual
A função virtual Equals é um espaço reservado para verificar se dois objetos têm o mesmo valor
lógico, uma operação diferente de verificar se duas variáveis se referem ao mesmo objeto na
memória, algo que você pode conseguir com o operador =. Contudo, e isto é
realmente confuso, a implementação padrão faz exatamente isso, por falta de uma maneira melhor:
Um exemplo do uso deste método (com um override adequado) está na classe TStrings , na qual o
método Equals compara o número de strings na lista e, se estas forem
iguais, o conteúdo das strings reais é comparado um por um até que um par não corresponda ou
o fim seja alcançado – se o fim for alcançado, as listas serão iguais.
Uma seção da biblioteca, na qual esta técnica é significativamente utilizada, é para suporte a
genéricos, em particular nas unidades Generics.Default e Generics.Collections . É importante para
uma biblioteca ou estrutura definir o conceito de “equivalência de valor” do objeto separadamente da
identidade do objeto. Ter um mecanismo padrão para comparar objetos “por valor” é uma grande
vantagem.
O método GetHashCode
A função virtual GetHashCode é outro espaço reservado emprestado da estrutura .NET para
permitir que cada classe calcule o código hash para seus objetos. O código padrão retorna um
valor aparentemente aleatório, o endereço do próprio objeto:
observação Com o endereço dos objetos sendo criados geralmente retirados de um conjunto limitado de áreas de heap, a
distribuição desses números não é uniforme e isso pode afetar adversamente um algoritmo de hash. É altamente
recomendável customizar este método criando um hash baseado em valores lógicos com uma boa distribuição de
hash. Isso levará a um melhor desempenho de dicionários e estruturas de dados que usam valores hash.
A função virtual GetHashCode é usada por algumas classes de coleção que suportam tabelas hash
e como forma de otimizar algum código, como TDictionary<T>.
tipo
TAnyObject = classe
privado
FValor: Inteiro;
NomeF: string;
público
construtor Create(AName: string; AValue: Integer);
função Equals(Obj: TObject): Boolean; sobrepor;
função ToString: string; sobrepor;
fim;
Na implementação dos três métodos eu simplesmente tive que alterar uma chamada para GetType
com isso para ClassType:
Observe que os objetos são considerados iguais se forem exatamente da mesma classe e seus valores
corresponderem, enquanto sua representação de string inclui apenas o campo FName .
Observe que duas referências (Ao2 e Ao3) apontam para o mesmo objeto na memória, e que o último
objeto (Ao4) possui o mesmo valor numérico.
O programa possui uma interface de usuário que permite ao usuário selecionar quaisquer dois itens e
comparar os objetos selecionados, ambos usando Equals e por comparação direta de referência. Aqui
estão alguns dos resultados:
O programa possui outro botão usado para testar alguns destes métodos para o próprio botão:
era
Btn2: Botão T;
começar
Btn2 := BtnTest;
'
'É igual a: +
Log(BoolToStr(BtnTest.Equals(Btn2), True));
'Referência = ' +
Log(BoolToStr(BtnTest = Btn2, True));
'
'ObterHashCode: +
Log(IntToStr(BtnTest.GetHashCode));
'
Log(+ BtnTest.ToString);
'Para sequenciar:
fim;
Igual: Verdadeiro
Referência = Verdadeiro
ObterHashCode: 28253904
ToString: TButton
tipo
TObject = classe
público
construtor Criar;
procedimento
Gratuito; procedimento
DisposeOf; função de classe InitInstance (Instância: Ponteiro): TObject;
procedimento CleanupInstance;
fim;
Desde que o suporte Unicode foi introduzido na linguagem, os nomes das classes em Object Pas-cal
usam internamente a representação ShortString (uma matriz de caracteres de um byte), mas
com uma codificação UTF-8 em vez da codificação ANSI tradicional do ShortString
tipo. Isto acontece tanto no nível TObject quanto no nível RTTI.
Por exemplo, o método ClassName é implementado com algum código de nível realmente baixo, como
segue:
Da mesma forma, na unidade TypInfo , todas as funções que acessam nomes de classes convertem as
representações internas UTF-8 ShortString em UnicodeString. Algo semelhante acontece com nomes de
propriedades.
A Unidade do Sistema
Embora a classe TObject tenha claramente um papel fundamental para a linguagem, tornando muito difícil
dizer se faz parte da linguagem ou da biblioteca de tempo de execução, existem outras classes de baixo
nível na unidade System que constituem uma parte fundamental e integrada do suporte do compilador.
A maior parte do conteúdo desta unidade, entretanto, é composta de estruturas de dados de baixo
nível, estruturas de registros simples, funções e procedimentos e algumas classes.
Aqui vou me concentrar principalmente nas classes, mas é inegável que muitos outros recursos da
unidade System são fundamentais para a linguagem. Por exemplo, a unidade do sistema define as
chamadas funções “intrínsecas” que não possuem código real, mas são resolvidas diretamente pelo
compilador. Um exemplo é SizeOf, uma chamada que o compilador substitui diretamente pelo
tamanho real da estrutura de dados que foi passada como parâmetro.
Você pode ter uma ideia da função especial da unidade do sistema lendo o comentário adicionado ao
seu início (principalmente para explicar por que navegar pelos símbolos do sistema leva a esta
unidade... mas não ao símbolo que você estava procurando):
Ler o código-fonte desta unidade pode ser um tanto tedioso, também porque aqui você pode encontrar
alguns dos códigos de nível inferior de toda a biblioteca de tempo de execução. Então decidi descrever
apenas uma seleção muito limitada de seu conteúdo.
· TVisibilityClasses é uma enumeração usada para configurações de visibilidade RTTI (veja o Capítulo 16
para mais detalhes)
· TGUID é um registro usado para representar um GUID no Windows, mas também em todos os outros
sistemas operacionais suportados
· TMethod é um registro central que representa a estrutura usada para o manipulador de eventos, com um
ponteiro para um endereço de método e outro para um objeto atual (mencionado brevemente no Capítulo
10)
· TDateTime é um alias fortemente digitado do tipo Double que é usado para armazenar datas
informação (na parte integral do valor) e informação de tempo (na parte decimal). Outros aliases incluem
os tipos TDate e TTime. Esses tipos foram abordados no Capítulo 2.
· THandle é um alias de tipos numéricos, usado para representar uma referência a um objeto do sistema
operacional, geralmente chamado de “handle” (pelo menos no jargão da API do Windows).
· THeapStatus é um registro com informações sobre o status da memória heap, brevemente mencionado
no Capítulo 13.
· TTextLineBreakStyle é uma enumeração que indica o estilo de quebra de linha para arquivos de texto em um
determinado sistema operacional. A variável global DefaultTextLineBreakStyle desse tipo contém as
informações atuais, usadas por muitas bibliotecas do sistema. Da mesma forma o
IInterface é o tipo de interface básico do qual todas as outras interfaces herdam e tem o mesmo
papel fundamental que o TObject tem para as classes.
· As operações de suporte e comparação do enumerador são definidas pelas seguintes interfaces: IEnumerator,
IEnumerable, IEnumerator<T>, IEnumerable<T>, IComparable, IComparable<T> e IEquatable<T>.
Existem também algumas classes principais que oferecem uma implementação básica de interfaces. Muitas
vezes você herda dessas classes base ao implementar uma interface, conforme abordado no Capítulo 11:
· TInterfacedObject, uma classe que possui uma implementação básica de contagem de referências e
verificação de ID de interface
· TAggregatedObject e TContainedObject, duas classes que oferecem implementação especial para objetos
agregados e implementa a sintaxe
· Mover é a operação principal de cópia de memória no sistema, apenas copiando um determinado número de
bytes de um local de memória para outro (muito poderoso, muito rápido, mas um pouco perigoso)
· As funções ParamCount e ParamStr podem ser usadas para processar parâmetros de linha de comando de
uma aplicação (e também funcionam em sistemas GUI como Windows e Mac).
· Random e Randomize são duas funções clássicas (provavelmente derivadas do BASIC) que fornecem
valores aleatórios (pseudo-aleatórios, mas apenas se você se lembrar de chamar
Randomize, caso contrário você obterá a mesma sequência toda vez que executar seu programa)
note Algumas dessas funções têm uma definição indireta. Em outras palavras, a função é na verdade um ponteiro para a função
real, de modo que o comportamento original do sistema possa ser substituído dinamicamente no código em tempo
de execução. (Se você sabe o que está fazendo, é claro, pois essa pode ser uma boa maneira de jogar fora a
memória do seu aplicativo).
O último grupo de tipos de dados, que quero mencionar neste capítulo, está relacionado aos
atributos – as informações extras de RTTI que você pode anexar a qualquer símbolo da linguagem.
Este tópico foi abordado no Capítulo 16, mas não mencionei os atributos predefinidos no sistema.
· TCustomAttribute é a classe base para todos os atributos personalizados. Esta é a classe base
você precisa herdar atributos de (e é a única maneira de uma classe ser identificada pelo compilador
como sendo um atributo, pois não há sintaxe de declaração especial).
· WeakAttribute é usado para indicar referências fracas para referências de interface (veja
Capítulo 13)
· UnsafeAttribute é usado para desabilitar a contagem de referências para referências de interface (também
abordado no Capítulo 13)
· VolatileAttribute indica variáveis voláteis que podem ser modificadas externamente e não devem ser
otimizadas pelo compilador
· HFAAttribute pode ser usado para ajustar o mecanismo de passagem de parâmetros de CPU ARM de 64
bits, controlando os agregados homogêneos de ponto flutuante (HFA)
Há mais na unidade Sistema , mas isso é para desenvolvedores especialistas. Prefiro passar para o
último capítulo, onde abordo a unidade Classes e alguns dos recursos da biblioteca em tempo de execução.
Se a classe TObject e a unidade System podem ser consideradas como sendo uma parte estrutural da
linguagem, algo necessário ao próprio compilador para construir qualquer aplicação, todo o resto na
biblioteca de tempo de execução (RTL) pode ser considerado como opcional. extensões para o sistema
central.
O RTL possui uma coleção muito grande de funções de sistema, abrangendo as operações padrão mais
comuns, que remontam parcialmente à época do Turbo Pascal e são anteriores à linguagem Object Pascal.
Muitas unidades do RTL são coleções de funções e rotinas, incluindo utilitários básicos (SysUtils), funções
matemáticas (Math), operações de string (StringUtils), processamento de data e hora (DateUtils) e
muitos outros.
Neste livro eu realmente não quero me aprofundar nessa parte mais tradicional do RTL, mas sim focar nas
classes principais, que são a base das bibliotecas de componentes visuais usadas no Object Pascal
(VCL e FireMonkey) e também de outros subsistemas. A classe TCom-ponent , por exemplo, define o
conceito de arquitetura “baseada em componentes”. Também é fundamental para gerenciamento de memória
e outros recursos básicos. O TPersistente
class é a chave para streaming de representações de componentes.
Existem muitas outras classes que poderíamos observar, já que o RTL é extremamente grande e
abrange o sistema de arquivos, o suporte de threading central, a biblioteca de programação paralela,
construção de strings, muitos tipos diferentes de coleções e classes de contêineres, classes principais
estruturas geométricas (como pontos e retângulos), estruturas matemáticas básicas (como vetores e
matrizes) e muito, muito mais.
Dado que o foco do livro é realmente a linguagem Object Pascal, e não um guia para as bibliotecas, aqui
vou me concentrar apenas em algumas classes selecionadas, escolhidas por seu papel principal ou
porque foram introduzidas nos últimos anos e são amplamente ignorados
pelos desenvolvedores.
A Unidade de Aulas
A unidade na base da biblioteca de classes Object Pascal RTL (e também das bibliotecas visuais) é
apropriadamente chamada de System.Classes. Esta unidade contém uma grande coleção de aulas em
sua maioria variadas, sem um foco específico. Vale a pena dar uma breve olhada no
importantes e acompanhar com uma análise aprofundada dos mais importantes.
· TList é uma lista básica de ponteiros que geralmente é adaptada como uma lista não digitada. Em geral
é recomendado usar TList<T> , conforme abordado no Capítulo 14.
· TBits é uma classe muito simples para manipular bits individuais em um número ou algum outro valor.
É um nível muito mais alto do que fazer manipulação de bits com turnos e operadores binários
ou e e .
· TPersistent é uma classe fundamental (a classe base de TComponent), abordada em detalhes na próxima
seção.
· TCollectionItem e TCollection são classes utilizadas para definir propriedades de coleção, ou seja,
propriedades com um array de valores. Estas são classes importantes para desenvolvedores de
componentes (e indiretamente ao usar componentes), e não tanto para desenvolvedores de
aplicações genéricas.
· TStrings é uma lista abstrata de strings, enquanto TStringList é uma implementação real da classe
base TStrings , fornecendo armazenamento para as strings reais. Cada item também possui um
objeto anexado, e essa é a maneira padrão de usar listas de strings para pares de strings nome/
valor. Há mais informações sobre esta classe na seção “Usando listas de strings” no final deste
capítulo.
· TStream é uma classe abstrata que representa qualquer sequência de bytes com
acesso, que pode abranger muitas opções de armazenamento diferentes (memória, arquivos, strings,
soquetes, campos BLOB e muitos outros). A unidade Classes define muitas das classes de fluxo
específicas, incluindo THandleStream, TFileStream, TCustomMemoryStream, TMemoryStream,
TBytesStream, TStringStream e TResourceStream. Outros fluxos específicos são declarados em
diferentes unidades RTL. Você pode ler uma introdução aos streams na seção “Apresentando
Streams” deste capítulo.
· Classes para streaming de componentes de baixo nível, como TFiler, TReader, TWriter e
TParser, usado principalmente por autores de componentes, e mesmo assim não com tanta frequência por eles.
· Classes para a implementação do padrão observador (usado, por exemplo, em ligações visuais ao vivo),
incluindo TObservers, TLinkObservers e TObserverMapping.
· Suporte a classes para ações e listas de ações (ações são abstrações de “comandos” emitidos por elementos da UI ou
internamente), incluindo TBasicAction e TBasicAction-Link.
· Interfaces de nível superior para operações de arquivos e fluxos, incluindo TTextReader e TTextWriter,
TBinaryReader e TBinaryWriter, TStringReader e TString-Writer, TStreamReader e TStreamWriter.
Essas classes também são abordadas neste capítulo.
A classe TPersistent
A classe TObject possui uma subclasse muito importante, uma das bases de toda a biblioteca, chamada
TPersistent. Se você observar os métodos da classe, sua importância pode ser surpreendente, pois a classe
faz muito pouco. Um dos elementos-chave da classe TPer-sistent é que ela é definida com a opção especial do
compilador {M+}, cuja função é
habilite a palavra-chave publicada , abordada no Capítulo 10.
O streaming dos componentes das bibliotecas FMX permanece baseado no papel da palavra-chave
publicada e na opção do compilador {$M+} .
note Usando o compilador atual, se você adicionar a palavra-chave publicada a uma classe que não herda de TPer-sistent
e não possui o sinalizador do compilador {$M+} , o sistema adicionará o suporte adequado de qualquer
maneira, indicando isso com um aviso .
A classe TComponent
A classe TComponent é a base das bibliotecas de componentes que são mais frequentemente
usado em conjunto com compiladores Object Pascal. O conceito de um componente é basicamente o
de uma classe que possui algum comportamento extra em tempo de design, recursos específicos
de streaming (para que a configuração em tempo de design possa ser salva e restaurada em um
aplicativo em execução) e o PME ( modelo de propriedade-método-evento) que discutimos no
Capítulo 10.
Esta classe define um número significativo de comportamentos e recursos padrão, introduz seu próprio
modelo de memória baseado em um conceito de propriedade de objetos, notificações de
componentes cruzados e muito mais. Embora não seja feita uma análise completa de todas as
propriedades e métodos, certamente vale a pena focar em alguns dos principais recursos da
classe TComponent por seu papel central no RTL.
Outro recurso crítico da classe TComponent é o fato de ela introduzir um construtor virtual Cre-ate ,
crítico para a capacidade de criar um objeto a partir de uma referência de classe enquanto ainda
invoca o código construtor específico da classe. Nós tocamos nisso no Capítulo 12, mas esta é
uma característica peculiar da linguagem Object Pascal, que vale a pena entender.
Por padrão, quando você descarta um componente em um designer (um formulário, um quadro ou um
módulo de dados), este é considerado o proprietário do componente. Ao criar um componente em
código, cabe a você especificar um proprietário ou passar nulo; nesse caso, você será
responsável por liberar você mesmo o componente da memória.
Você pode usar as propriedades Components e ComponentCount para listar os componentes pertencentes
a um componente (AComp neste caso), com código como:
era
Eu: Inteiro;
começar
para I := 0 para AComp.ComponentCount – 1 faça
AComp.Componentes[I]. Faça alguma coisa;
era
ChildComp: TComponent;
começar
para ChildComp em AComp do
ChildComp. Faça alguma coisa;
Quando um componente é destruído, ele é removido da lista de proprietários (se houver) e destrói todos
os componentes que possui. Esse mecanismo é crucial para o gerenciamento de memória em Object Pascal:
como não há coleta de lixo, a propriedade pode resolver a maioria dos problemas de gerenciamento de
memória, como vimos parcialmente no Capítulo 13.
Propriedades do Componente
Além do mecanismo principal de propriedade (que também inclui notificações e outros recursos não abordados
aqui), qualquer componente possui duas propriedades publicadas:
· Nome é uma string com o nome do componente. Isto é usado para encontrar um componente dinamicamente
(chamando o método FindComponent do proprietário) e para conectar o componente com o
campo do formulário referente a ele. Todos os componentes pertencentes ao
o mesmo proprietário deve ter nomes exclusivos que não diferenciam maiúsculas de minúsculas, mas seus nomes
também podem estar vazios. Duas regras curtas aqui: defina nomes adequados de componentes para melhorar
a legibilidade do código e nunca altere o nome de um componente em tempo de execução (a menos que você
esteja realmente ciente dos possíveis efeitos colaterais desagradáveis).
· Tag é um valor NativeInt (antigamente era um Integer) não utilizado pela biblioteca, mas disponível para você conectar
informações extras ao componente. O tipo é compatível com o tamanho de ponteiros e referências de objetos,
que geralmente são armazenados no Tag de um componente.
Streaming de componentes
O mecanismo de streaming, usado tanto pelo FireMonkey quanto pelo VCL para criar arquivos FMX ou DFM, é baseado
na classe TComponent . O mecanismo de streaming do Delphi salva o
propriedades e eventos publicados de um componente e seus subcomponentes. Essa é a representação que você
obtém em um arquivo DFM ou FMX, e também o que você obtém se copiar e colar um componente do designer em
um editor de texto.
Existem métodos para obter as mesmas informações em tempo de execução, incluindo os métodos WriteComponent e
ReadComponent da classe TStream , mas também os métodos ReadComponentRes e
WriteComponentRes da mesma classe, e os métodos ReadRootComponent e WriteRootComponent das
classes TReader e TWriter – classes especiais que ajudam a lidar com com streaming de componentes. Essas
operações geralmente usam a representação binária de fluxos de formulário: Você pode usar o procedimento global
ObjectResourceToText para converter a representação binária do formulário em textual e
ObjectTextToResource para a conversão reversa.
Um elemento chave é que o streaming não é um conjunto completo das propriedades publicadas de um componente. A
transmissão inclui:
· As propriedades publicadas de um componente com valor diferente do valor padrão (ou seja, os valores padrão não
são salvos para reduzir o tamanho).
· Somente imóveis publicados marcados como armazenados (que é o padrão). Uma propriedade com
armazenado definido como False (ou uma função retornando False), não será salvo.
· Entradas adicionais que não correspondam às propriedades do componente podem ser adicionadas em execução
tempo substituindo o método DefineProperties .
· O método virtual Loaded é chamado para finalizar o carregamento (e os componentes podem fazer
processamento personalizado extra, desta vez com os valores das propriedades já carregados do
stream)
Tomando emprestado de seu ancestral da linguagem Pascal, o Object Pascal ainda possui palavras-chave
e mecanismos básicos de linguagem para processar arquivos. Eles estavam basicamente obsoletos
quando o Object Pascal foi introduzido e não vou abordá-los neste livro. O que abordarei nesta seção,
em vez disso, são algumas técnicas modernas para processamento de arquivos, apresentando a
unidade IOUtils , as classes de fluxo e as classes de leitor e gravador.
Extraindo Subpastas
O programa pode preencher um list box com as subpastas encontradas em uma pasta definida pelo
usuário, utilizando o método GetDirectories do registro TDirectory , passando como parâmetro o
valor TSearchOption.soAllDirectories. O resultado é um array de strings que você pode
enumerar:
começar
ListBox1.Items.Clear;
PathList: = TDirectory.GetDirectories (EdBaseFolder.Text,
TSearchOption.soAllDirectories, nil);
para StrPath em PathList faça
ListBox1.Items.Add(StrPath);
fim;
fim;
Pesquisando arquivos
Um segundo botão do programa permite listar todos os arquivos PAS nessas pastas, examinando cada
pasta com uma chamada GetFiles baseada em uma determinada máscara. Você pode ter uma
filtragem mais complexa passando um método anônimo do tipo TFilterPredicate para uma versão
sobrecarregada de GetFiles.
Este exemplo usa a filtragem mais simples baseada em máscara e preenche uma lista de strings interna.
Os elementos desta lista de strings são então copiados para a interface do usuário após a remoção do
caminho completo, mantendo apenas o nome do arquivo.
Ao chamar o método GetDirectories você obtém apenas as subpastas, mas não a atual. É por isso que o programa
pesquisa primeiro na pasta atual e depois examina cada subpasta:
era
PathList, FilesList: TStringDynArray;
StrPath, StrFile: string;
começar
se TDirectory.Exists(EdBaseFolder.Text) então
começar
// Limpar
ListBox1.Items.Clear;
SFilesList.Add(StrFile);
Agora
// apenas copie dos arquivos (sem caminho)
os nomes para a caixa de listagem
para StrFile em SFilesList faça
ListBox1.Items.Add(TPath.GetFileName(StrFile));
fim;
fim;
Nas linhas finais, a função GetFileName do TPath é usada para extrair o nome do arquivo do caminho completo
do arquivo. O registro TPath possui alguns outros métodos interessantes, incluindo GetTempFileName,
GetRandomFileName , um método para mesclar caminhos, alguns
para verificar se são válidos ou contêm caracteres ilegais e muito mais.
Apresentando fluxos
Se a unidade IOUtils for para localizar e manipular arquivos, quando você quiser ler ou gravar um arquivo (ou
qualquer outra estrutura de dados semelhante acessada sequencialmente), você poderá usar a classe TStream e
suas muitas classes descendentes. A classe abstrata TStream possui apenas algumas propriedades (tamanho e
posição), juntamente com a interface básica que todas as classes de fluxo compartilham com os principais
métodos de leitura e gravação . O conceito expresso por esta classe é o acesso sequencial. Cada vez que você
lê e escreve um número de bytes, a posição atual é avançada por esse número. Para a maioria dos fluxos, você
pode mover a posição para trás, mas também pode haver fluxos unidirecionais.
· TBufferedFileStream é um fluxo de arquivos de disco otimizado que usa um buffer de memória para desempenho
extra. Esta classe de stream foi introduzida no Delphi 10.1.
· TMemoryStream é um fluxo de dados na memória que você pode acessar com um ponteiro.
· TBytesStream representa um fluxo de bytes na memória, que você também pode acessar
como uma matriz de bytes.
Usando fluxos
Criar e usar um fluxo pode ser tão simples quanto criar uma variável de um tipo específico e chamar os
métodos de um componente para carregar o conteúdo do arquivo. Por exemplo, dado um fluxo e um
componente de memorando, você pode escrever:
Como você pode ver neste código, o método Create para fluxos de arquivos possui dois parâmetros: o
nome do arquivo e algum sinalizador indicando o modo de acesso solicitado. Como mencionei, os fluxos
suportam operações de leitura e gravação, mas são de nível bastante baixo, então recomendo usar as
classes de leitor e gravador discutidas na próxima seção. Em vez disso, o que o uso direto de stream
fornece é um conjunto de operações abrangentes, como carregar um stream inteiro no trecho de código
acima ou copiar um em outro, como neste exemplo:
Cada escritor de texto possui dois conjuntos de operações sobrecarregadas sem (Write) e com
(WriteLine) um separador de fim de linha. Aqui está o primeiro conjunto:
Para ler o TStreamReader, você pode trabalhar novamente em um stream ou arquivo (nesse caso ele
pode detectar a codificação do marcador UTF BOM):
era
RS: TStreamReader;
começar
SR := TStreamReader.Create(tente 'teste.txt' , Verdadeiro);
Memo1.Lines.Add(SR.ReadLine);
finalmente
SR.Livre;
fim;
Observe como você pode verificar o status EndOfStream . Comparadas ao uso direto de fluxos de texto (ou mesmo
strings), essas classes são particularmente úteis de usar e fornecem bom desempenho.
Como exemplo (bastante simples), escrevi o programa BinaryFiles . Na primeira parte, este programa grava alguns
elementos binários em um arquivo (o valor de uma propriedade e a hora atual) e os lê de volta, atribuindo o valor da
propriedade:
BW.Write(Esquerda);
BW.Escrever(Agora);
'
'Tamanho do arquivo: + IntToStr(BW.BaseStream.Size));
Registro (finalmente
BW.Livre;
fim;
fim;
Esquerda:= BR.ReadInt32;
'
'Leia à esquerda:
Log(+ IntToStr(Esquerda));
ATime := BR.ReadDouble;
'
'Tempo de leitura:
Log(+ TimeToStr(ATime));
finalmente
BR.Grátis;
fim;
fim;
A regra principal no uso dessas classes de leitor e gravador é que você deve ler os dados em
na mesma ordem em que você escreveu, caso contrário você bagunçará totalmente os dados. Na verdade, apenas o
os dados binários de campos individuais são salvos, sem nenhuma informação sobre o campo em si.
Porém, nada impede você de interpor dados e metadados no arquivo, como salvar o tamanho do próximo
tipo de dados antes de gravar os dados para esse tipo.
A classe TStringBuilder
Já mencionei no Capítulo 6 que, diferentemente de outras linguagens, Object Pascal tem suporte completo
para concatenação direta de strings, que na verdade é uma operação bastante rápida.
A linguagem RTL, entretanto, também inclui uma classe específica para montar uma string a partir de
fragmentos de diferentes tipos de dados, chamada TStringBuilder.
Como exemplo simples do uso da classe TStringBuilder , considere o seguinte trecho de código:
era
SBuilder: TStringBuilder;
Str1: sequência;
começar
SBuilder := TStringBuilder.Create;
tentar
SBuilder.Append(12);
SBuilder.Append(); 'Olá'
Str1 := SBuilder.ToString;
finalmente
SBuilder.Grátis;
fim;
fim;
Observe que temos que criar e destruir este objeto TStringBuilder . Outro elemento
que você pode notar acima é que existem muitos tipos de dados diferentes que você pode passar como
parâmetros para a função Append .
Outros métodos interessantes da classe TStringBuilder incluem um AppendFormat (com uma chamada
interna para Format) e um AppendLine que adiciona o valor sLineBreak . Junto com Append, há uma
série correspondente de métodos Insert sobrecarregados , bem como métodos Remove e alguns métodos
Replace .
note A classe TStringBuilder possui uma interface agradável e oferece boa usabilidade. Em termos de desempenho,
porém, o uso de funções padrão de concatenação e formatação de strings pode fornecer melhores resultados,
ao contrário de outras linguagens de programação que definem strings imutáveis e têm desempenho muito ruim
no caso de concatenação pura de strings.
Este idioma de codificação abre a possibilidade de encadeamento de métodos, ou seja, chamar um método no objeto
retornado pelo anterior. Em vez de escrever:
SBuilder.Append(12);
SBuilder.AppendLine;
SBuilder.Append( 'Olá' );
SBuilder.Append(12).AppendLine.Append( 'Olá' );
Construtor.
Anexar (12).
AnexarLinha.
Acrescentar( 'Olá' );
Costumo gostar mais dessa sintaxe do que da original, mas sei que é apenas um açúcar sintático e algumas pessoas
preferem a versão original com o objeto escrito em cada linha. De qualquer forma, lembre-se de que as várias
chamadas para Append não retornam novos objetos (portanto, não há possíveis vazamentos de memória), mas
exatamente o mesmo objeto que você está invocando
os métodos ativados.
· TStrings é uma classe abstrata para representar todas as formas de listas de strings, independentemente de suas
implementações de armazenamento. Esta classe define uma lista abstrata de strings. Por esse motivo, os objetos
TStrings são usados apenas como propriedades de componentes capazes de armazenar as próprias strings.
· TStringList, uma subclasse de TStrings, define uma lista de strings com armazenamento próprio.
Você pode usar esta classe para definir uma lista de strings em um programa.
As duas classes de listas de strings também possuem métodos prontos para uso para armazenar ou
carregar seu conteúdo de ou para um arquivo de texto, SaveToFile e LoadFromFile (que são totalmente
habilitados para Unicode). Para percorrer uma lista, você pode usar uma instrução for simples baseada em
seu índice, como se a lista fosse um array ou um enumerador for-in .
Se considerarmos apenas a parte principal da biblioteca, que é o namespace System , ela inclui as
seguintes unidades (das quais removi algumas unidades raramente usadas):
· System.Actions inclui o suporte principal para a arquitetura de ações, que fornece uma maneira de
representar comandos do usuário conectados, mas abstraídos, da camada da interface do usuário.
· System.Contnrs inclui as classes de contêiner antigas e não genéricas, como lista de objetos, dicionário,
fila e pilha. Recomendo usar a versão genérica das mesmas classes, quando possível.
· System.Devices faz interface com dispositivos do sistema (como GPS, acelerômetro e outros
sobre).
· System.Diagnostics define uma estrutura de registro para medição precisa do tempo decorrido em testes
de código, que usei ocasionalmente no livro.
· System.Generics possui na verdade duas unidades separadas, uma para coleções genéricas e outra
para tipos genéricos. Essas unidades são abordadas no Capítulo 14.
· System.IniFiles define uma interface para processar arquivos de configuração INI, frequentemente
encontrados no Windows.
· System.IOUtils define registros para acesso ao sistema de arquivos (arquivos, pastas, caminhos), que
foram abordados anteriormente neste capítulo.
· System.JSON inclui as mesmas classes principais para processamento de dados na notação de objeto
JavaScript comumente usada, JSON.
· System.Math define funções para operações matemáticas, incluindo funções trigonométricas e financeiras.
Ele também possui outras unidades em seu namespace para vetores e matrizes.
· System.Rtti possui todo o conjunto de classes RTTI, conforme explicado no Capítulo 16.
· System.SysUtils possui a coleção básica de utilitários de sistema, com alguns dos mais tradicionais que
datam dos primórdios do Delphi.
· System.Types possui alguns tipos de dados adicionais básicos, como registros TPoint, TRectangle e
TSize , a classe TBitConverter e muitos outros tipos de dados básicos usados pelo RTL.
· System.TypInfo define a interface RTTI mais antiga, também apresentada no Capítulo 16,
basicamente substituído por aqueles da unidade System.RTTI .
· System.Variants e System.VarUtils possuem funções para trabalhar com variantes (um recurso de
linguagem abordado no Capítulo 5).
Existem também várias outras partes da RTL que são subseções do Sistema
espaço de nome, com cada seção abrangendo várias unidades (ocasionalmente um grande número, como
o namespace System.Win ), incluindo clientes HTTP (System.Net) e suporte à Internet das Coisas
(IoT) (System.Beacon, System.Bluetooth , System.Sensors e System.Tether). Existem também, é
claro, APIs traduzidas e arquivos de cabeçalhos para interface com todos os sistemas operacionais
suportados.
Há uma grande variedade de funções, tipos, registros, interfaces e classes RTL prontas para usar,
que estão disponíveis para você explorar, para aproveitar o poder do Object Pascal. Não tenha
pressa navegando na documentação do sistema para saber mais.
para encerrar
O capítulo 18 marca o fim do livro, exceto pelos três apêndices seguintes. Este foi originalmente meu
primeiro livro focado exclusivamente na linguagem Object Pascal, e estou fazendo o meu melhor para
continuar atualizando-o e mantendo o texto do livro e o código-fonte ao longo do tempo. Fiz uma atualização
somente em PDF para o Delphi 10.1 Berlin, uma nova versão disponível impressa para 10.4 Sydney, e
esta atualização somente em PDF que você está lendo para o Delphi 11 Alexandria.
Consulte novamente a Introdução para obter o código-fonte do livro mais recente do GitHub (baseado
no mesmo repositório da edição 10.4) e visite o site do livro ou meu blog para obter informações e
atualizações futuras.
Espero que você tenha gostado de ler o livro tanto quanto eu gostei de escrevê-lo e de escrever sobre
Delphi nos últimos 25 anos. Boa codificação com Delphi!
fim. - 519
fim.
Esta seção final do livro tem alguns apêndices que enfocam questões secundárias específicas
que valem a pena considerar, mas que não se enquadram no fluxo do livro. Há também uma breve
história das linguagens Pascal e Object Pascal, bem como um glossário.
Resumo do Apêndice
Apêndice A: A Evolução do Object Pascal
Apêndice B: Glossário de Termos
Apêndice C: Índice
a: a evolução de
objeto pascal
Object Pascal é uma linguagem desenvolvida para a crescente variedade de dispositivos de computação atuais.
de smartphones e tablets a desktops e servidores. Não apareceu simplesmente do nada. Ele foi
cuidadosamente projetado em uma base sólida para ser a ferramenta preferida dos
programadores modernos. Ele fornece um equilíbrio quase ideal entre a velocidade da
programação e a velocidade dos programas resultantes, clareza de sintaxe e poder de
expressão.
A base sobre a qual o Object Pascal foi construído é a família de linguagens de programação
Pascal. Da mesma forma que o Go do Google, ou o Objective-C da Apple, são linguagens
enraizadas em C, o Object Pascal está enraizado em Pascal. Sem dúvida você teria adivinhado isso
do nome.
Este pequeno apêndice inclui um breve histórico da família de linguagens e ferramentas reais em
torno de Pascal, Turbo Pascal, Delphi's Pascal e Object Pascal. Embora não seja realmente
necessário ler isto para aprender a língua, certamente vale a pena compreender a evolução da
língua e onde ela se encontra hoje.
A linguagem de programação Object Pascal que usamos hoje nas ferramentas de desenvolvimento
da Embarcadero foi inventada em 1995, quando a Borland introduziu o Delphi, que na época era
seu novo ambiente de desenvolvimento visual. A primeira linguagem Object Pascal foi estendida
da linguagem já em uso nos produtos Turbo Pascal, onde a linguagem foi
geralmente referido como Turbo Pascal. A Borland não inventou o Pascal, apenas ajudou a fazer
é muito popular e amplia seus fundamentos para superar algumas de suas limitações em comparação com a
linguagem C.
As seções a seguir cobrem a história da linguagem, desde o Pascal de Wirth até o mais recente compilador
Object Pascal do Delphi baseado em LLVM para chips ARM e dispositivos móveis.
Pascal de Wirth
A linguagem Pascal foi originalmente desenvolvida em 1971 por Niklaus Wirth, professor da Politécnica de
Zurique, na Suíça. A biografia mais completa de Wirth está disponível em http://www.cs.inf.ethz.ch/~wirth.
Pascal foi projetado como uma versão simplificada da linguagem Algol para fins educacionais. O próprio Algol
foi criado em 1960. Quando Pascal foi inventado, existiam muitas linguagens de programação, mas apenas
algumas eram amplamente utilizadas: FORTRAN, Assembler, COBOL e BASIC. A ideia chave da nova
linguagem era a ordem, gerenciada por meio de um forte conceito de tipos de dados, declaração de variáveis e
controles estruturados de programas.
A linguagem também foi pensada para ser uma ferramenta de ensino, ou seja, para ensinar programação
utilizando as melhores práticas.
Escusado será dizer que os princípios fundamentais do Pascal de Wirth tiveram uma enorme influência na história
de todas as linguagens de programação, muito além daquelas ainda baseadas na sintaxe Pascal. Quanto ao
ensino de línguas, muitas vezes as escolas e universidades seguiram outros critérios (como pedidos de
emprego ou doações de fornecedores de ferramentas) em vez de procurar qual língua ajuda melhor a
aprender os conceitos-chave da programação. Mas isso é outra história.
Turbo Pascal
O mundialmente famoso compilador Pascal da Borland, chamado Turbo Pascal, foi lançado em 1983,
implementando o compilador de acordo com a especificação definida por "Pascal: User Manual and Report"
de Jensen e Wirth. O compilador Turbo Pascal foi uma das séries de compiladores mais vendidas de todos os
tempos e tornou a linguagem particularmente popular na plataforma PC, graças ao seu equilíbrio entre simplicidade,
potência e preço. O autor original foi Anders Hejlsberg, mais tarde pai das populares linguagens de programação
C# e TypeScript publicadas pela Microsoft.
Turbo Pascal introduziu o Ambiente de Desenvolvimento Integrado (IDE) onde você poderia editar
o código (em um editor compatível com WordStar), executar o compilador, ver os erros e voltar para
as linhas que contêm esses erros. Parece trivial agora, mas antes você tinha que sair do editor e
voltar ao DOS; execute o compilador de linha de comando, anote as linhas de erro, abra o editor e
localize as linhas de erro.
Além disso, a Borland vendeu o Turbo Pascal por 49 dólares, enquanto o compilador Pascal da
Microsoft foi vendido por algumas centenas. Os muitos anos de sucesso do Turbo Pascal
contribuíram para que a Microsoft abandonasse seu produto compilador Pascal.
Na verdade, você pode baixar uma cópia da versão original do Turbo Pascal da Borland na seção
Museu da Embarcadero Developer Network:
http://edn.embarcadero.com/museum
história Após a linguagem Pascal original, Niklaus Wirth projetou a linguagem Modula-2, uma extensão do
Sintaxe Pascal agora quase esquecida, que introduziu um conceito de modularização muito semelhante ao
conceito de unidades no antigo Turbo Pascal e no atual Object Pascal.
Uma extensão adicional do Modula-2 foi o Modula-3, que tinha recursos orientados a objetos semelhantes
ao Object Pascal. O Modula-3 foi ainda menos usado que o Modula-2, com a maior parte do desenvolvimento
comercial da linguagem Pascal migrando para os compiladores Borland e Apple, até que a Apple abandonou o
Object Pascal pelo Objective-C, deixando a Borland com quase o monopólio da linguagem.
observação Você pode ler mais sobre o lançamento do produto Delphi em https://delphi.embarcadero.com/ e em https://
www.marcocantu.com/delphibirth/, e nas postagens do blog que geralmente escrevo todos os anos por volta
de 14 de fevereiro, para comemorar o aniversário do lançamento do produto.
história O ano de 1995 foi realmente um ano especial para linguagens de programação, pois viu a estreia do Delphi
Objeto Pascal, Java, JavaScript e PHP. Estas são algumas das linguagens de programação mais populares
ainda em uso hoje. Na verdade, a maioria das outras linguagens populares (C, C++, Objective-C e COBOL)
são muito mais antigas, enquanto a única linguagem popular mais recente é C#. Para uma história das
linguagens de programação você pode ver http://en.wikipedia.org/wiki/History_of_programming_languages.
Com o Delphi 2, a Borland trouxe o compilador Pascal para o mundo de 32 bits, na verdade reprojetando-
o para fornecer um gerador de código comum ao compilador C++. Isso trouxe muitas otimizações
anteriormente encontradas apenas em compiladores C/C++ para a linguagem Pascal.
Com o lançamento da versão 7 do Delphi, a Borland começou formalmente a chamar a linguagem Object
Pas-cal de linguagem Delphi, mas nada realmente mudou na linguagem naquela época. Naquela época,
a Borland também criou o Kylix, uma versão Delphi para Linux, e mais tarde criou um compilador Delphi
para o Microsoft .NET framework incluído no Delphi 8. Ambos
projetos foram posteriormente abandonados, mas o Delphi 8, lançado no final de 2003, marcou um
conjunto muito extenso de mudanças na linguagem, necessidade de suporte ao .NET, mudanças que foram
posteriormente adotadas no compilador Win32 Delphi e todos os outros compiladores seguintes.
Ao mesmo tempo, junto com o compilador Win32, a empresa introduziu um compilador Win64 como
parte do Delphi XE2, e um compilador macOS, voltando a uma estratégia multiplataforma após a
tentativa fracassada de Borland e Kylix, sua versão do Delphi para Linux. Desta vez, porém, a ideia era ter
um único ambiente de desenvolvimento Windows e fazer compilação cruzada para outras plataformas.
O suporte para Mac foi apenas o começo da estratégia multidispositivo da empresa, abrangendo
plataformas desktop e móveis, como iOS e Android. Essa estratégia foi possível graças à adoção de um
novo framework GUI, denominado FireMonkey.
Tornando-se móvel
Onde todas as versões anteriores do Delphi tinham como alvo CPUs Intel x86, a mudança para dispositivos
móveis introduziu o primeiro compilador Object Pascal para chips ARM. Essa mudança levou a uma
rearquitetura geral do compilador e das ferramentas relacionadas, o “conjunto de ferramentas do compilador”, baseado em
a arquitetura aberta do compilador LLVM.
note LLVM é o nome abreviado de LLVM Compiler Infrastructure ou “uma coleção de compiladores modulares e
reutilizáveis e tecnologias de conjunto de ferramentas”, sobre as quais você pode ler em https://llvm.org/
O compilador ARM para iOS, lançado com Delphi XE4, foi o primeiro compilador Object Pascal baseado em
LLVM, mas também o primeiro a introduzir alguns novos recursos como Automatic Reference Counting
(ARC), posteriormente removido da linguagem.
Posteriormente, no mesmo ano (2013), o Delphi XE5 adicionou suporte à plataforma Android, com um
segundo compilador ARM baseado em LLVM. Para resumir, o Delphi XE5 vem com 6 compiladores para
a linguagem Object Pascal (para Win32, Win64, macOS, iOS Simulator no Mac, iOS ARM e Android ARM).
Todos esses compiladores fazem parte do Delphi atual e suportam uma definição de linguagem amplamente
comum, com algumas diferenças significativas que abordei detalhadamente ao longo do livro.
Nos primeiros meses de 2014, a Embarcadero lançou uma nova ferramenta de desenvolvimento baseada
nas mesmas tecnologias móveis básicas e chamada AppMethod. Em abril de 2014, a empresa também
lançou a versão XE6 do Delphi, enquanto setembro de 2014 viu o terceiro lançamento do AppMethod e
Delphi XE7, seguido, na primavera de 2015, pelo Delphi XE8, que incluiu o primeiro compilador ARM de 64
bits, visando iOS.
Ao longo da série 10.x, a Embarcadero continuou a evoluir a linguagem Object Pascal com a introdução de
recursos como declarações de variáveis inline e registros gerenciados personalizados, além de muitas outras
melhorias menores, todas abordadas neste livro.
A versão do Delphi 11
Com o lançamento do Windows 11 pela Microsoft e Apple se afastando de uma longa série de versões 10.x do
sistema operacional macOS, a Embarcadero decidiu sair da série 10.x para adotar uma numeração sequencial
de versões principais começando com 11 Alexandria – correspondendo assim Número da versão do Windows 11
neste momento. Isso foi feito para sublinhar a estreita ligação do produto com o sistema operacional Windows,
mesmo depois de ter adotado o desenvolvimento para vários dispositivos como um princípio central do Delphi
moderno. Notável no Delphi 11 é o suporte para a plataforma macOS ARM de 64 bits, com geração de código
nativo para a CPU Apple M1, e a estreia do IDE habilitado para High-DPI.
526 - b: glossário
b: glossário
A
Classe abstrata Uma classe que não está totalmente definida e fornece apenas a interface
do método que as subclasses devem implementar.
Chamada ambígua Esta é a mensagem de erro que você recebe caso o compilador
tenha duas ou mais opções para resolver uma chamada de função
e não tenha como determinar automaticamente qual delas você
está tentando chamar.
Android O nome do sistema operacional do Google para dispositivos
móveis adotado por centenas de fornecedores de hardware
(além do Google) devido à sua natureza aberta. O Android é
atualmente o sistema operacional mais utilizado no mundo,
tendo ultrapassado o Microsoft Windows.
Método anônimo Um método anônimo, ou função anônima, é uma função que não
está associada a um nome de função e pode ser atribuída a uma
variável ou passada como argumento para
outra função, que posteriormente pode executar seu código.
Os métodos anônimos são um pouco mágicos em comparação com
as funções regulares. A verdadeira mágica é que eles podem
acessar variáveis do bloco em que são declaradas, mesmo que
finalmente sejam executadas em um bloco diferente.
b: glossário - 527
B
Expressão booleana Nomeada em homenagem ao famoso matemático George Boole,
uma expressão booleana é uma expressão avaliada como
Verdadeiro ou Falso. Um exemplo simples é 1 = 2, que é falso. A
expressão booleana não precisa ser uma expressão matemática
tradicional, pode ser simplesmente uma variável booleana ou
mesmo uma chamada para
uma função que retorna um valor booleano.
C
Cardeal Um número cardinal é um dos números naturais. Simplesmente
dito, isso significa um número que pode ser usado para contar
coisas, como tal é sempre maior ou igual a zero.
528 - b: glossário
Aula Uma classe é uma definição das propriedades, métodos e campos de dados
que um objeto (daquela classe) terá quando for criado.
Diretiva do compilador Uma diretiva de compilador é uma instrução especial para o compilador
que altera seu comportamento padrão. As diretivas do compilador
são atribuídas com palavras especiais prefixadas pelo sinal $ ou podem
ser definidas nas Opções do Projeto.
Nota: Algumas diretivas de compilador fornecem, por razões históricas,
versões de um único caractere de seus nomes mais longos.
b: glossário - 529
D
Tipo de dados Um tipo de dados indica o requisito de armazenamento e as
operações que você pode executar em uma variável desse tipo. Em
Object Pascal, como em qualquer linguagem de programação
fortemente tipada, cada variável possui um tipo de dados específico.
530 - b: glossário
F
FireMonkey FireMonkey (FMX) é uma biblioteca de componentes visuais e não
visuais fornecida com Delphi (além da biblioteca VCL). Os
componentes são multiplataforma, portanto funcionarão igualmente
bem em Windows, macOS, iOS,
Android e até Linux (por meio da biblioteca complementar
FMXLinux).
Forma O termo usado para uma janela nas bibliotecas VCL e FireMonkey.
G
Memória global A memória global é uma área de memória estática para variáveis
globais de suas aplicações. Essa memória é usada durante todo o
tempo de vida de uma aplicação e não pode crescer, diferentemente
do heap que fornece uma área de memória alocada dinâmica. A
memória global é usada com moderação em aplicativos Object
Pascal.
GUI Uma interface gráfica do usuário (GUI) permite que os usuários interajam
com computadores, tablets e telefones através de gráficos
b: glossário - 531
Pilha (memória) O heap é uma área de memória para blocos de memória alocados
dinamicamente. Como o nome indica, não há estrutura ou
sequência na alocação de memória heap. Sempre que um
é necessário um bloco, ele é retirado de uma área livre. A vida útil
dos blocos individuais é diferente e a ordem de alocação e
desalocação não está relacionada. A memória heap é usada para
dados de objetos, strings, arrays dinâmicos e outros tipos de
referência (veja Referências), mas também para blocos alocados
manualmente (veja Ponteiros). O heap é grande, mas não infinito
e, se você não liberar objetos não utilizados da memória, seu
aplicativo acabará ficando sem recursos.
memória.
EU
532 - b: glossário
M
Mac OS O nome do sistema operacional dos computadores Apple Mac,
anteriormente conhecido como OS X.
Método Um método é uma função ou procedimento vinculado a um objeto.
Os métodos têm acesso a todos os dados armazenados no objeto.
b: glossário - 533
Tipo ordinal Um tipo ordinal é um tipo de dados feito de elementos que podem ser
contados e têm uma sequência. Você pode pensar em números inteiros,
mas os caracteres também têm uma sequência, assim como os tipos
enumerados personalizados.
P
Ponteiro Um ponteiro é uma variável que contém um endereço de memória. Um
ponteiro pode se referir à localização de alguns dados ou de uma função
na memória. Ponteiros não são comumente usados, enquanto referências
(veja Referência) são ponteiros opacos e gerenciados que são
extremamente comuns, mas também significativamente mais fáceis
de usar.
Propriedade Uma propriedade define o estado de um objeto. Dado que uma propriedade
pode ser mapeada para dados ou usar métodos para ler e escrever o valor,
ela pode ser usada para abstrair da implementação real.
534 - b: glossário
R
RAD O Desenvolvimento Rápido de Aplicativos (RAD) é uma característica
de um ambiente de desenvolvimento que torna fácil e rápido
para construir aplicativos. As ferramentas RAD são geralmente baseadas
em designers visuais, embora esta seja uma definição bastante antiga e
raramente usada hoje em dia.
Object Pascal também inclui registros avançados que podem ter métodos
semelhantes a um objeto.
Referência Uma referência é uma variável que se refere a alguns dados em outro
local da memória, em vez de armazená-los diretamente. Em Object
Pascal, estas são variáveis para tipos como
classes e strings, mas também interfaces e arrays dinâmicos são
referências. Diferentemente dos ponteiros (veja Ponteiros), as
referências são geralmente gerenciadas pelo compilador e por uma
biblioteca de tempo de execução e requerem pouco conhecimento de
baixo nível e pouco gerenciamento direto de memória, se houver, por
parte do desenvolvedor.
Biblioteca de tempo de execução (RTL) Esta é uma coleção de rotinas pré-escritas que o compilador inclui
automaticamente com o código do aplicativo para suportar o aplicativo
executável. Inclui suporte
b: glossário - 535
Pilha (memória) A pilha é uma área de memória alocada de forma dinâmica e ordenada.
Cada vez que você chama um método, procedimento ou função,
ele reserva sua própria área de memória (para variáveis locais,
inclusive temporárias, e parâmetros).
À medida que o método retorna, a memória é limpa, de forma muito
ordenada. O único cenário real para ficar sem
memória de pilha é quando um método entra em uma chamada
recursiva infinita (consulte Recursão).
EM
536 - b: glossário
EM
EM
Janela Uma janela é uma área da tela que contém elementos GUI com os
quais um usuário pode interagir. Um aplicativo GUI pode exibir várias
janelas. No VCL e no FireMonkey, as janelas são definidas usando
um objeto Form.
janelas O nome do sistema operacional onipresente da Microsoft, que
foi pioneiro (junto com outros sistemas operacionais da época, como
System X para Apple Macintosh) no conceito
de janelas gráficas (ver Janela).
c: índice - 537
c: índice
538 - c: índice
C
C...28, 31, 61, 63, 65, 78, 81, 86f., 89, 91, 94, 100, 102, 108,
120, 126, 144 C/C+
+......... .................................................. ....24 C#. 25, 28, 49, 52,
70, 78, 90, 101, 142, 202, 204, 207, 217, 228, 234, 239, 242,
247, 249, 256, 276f., 284, 287, 291ss., 310, 329, 383,
400, 431 C++...28, 42, 49, 63, 101, 109, 126, 147, 202, 211,
217, 223, 239, 256, 310, 383, 426, 523 Funções de retorno de
chamada ....................................333 Convenção
de Chamada...... ...................................356 Convenções de
Chamada............ ............................119 Carcaça de
camelo......... ....................................29 Variável
capturada........... ................................434
Cardeal................. ....................................60, 527
Careta......... .................................................. ......161 Cary
Jensen......................................... ................6
Claro................................................. ...............376ClienteDataSet......................... ...
Caso................................. ...........................34, 88f.
Exemplo................................................. ....264
RTTI.......................................... ...............452
Insensibilidade a maiúsculas e minúsculas.................................28
$ALINHAR................................. ......................141
Pegar................................................. ..............256
$DEFINIR.......................... ....................45, 371
Cdecl.................................. ..............................119
$OUTRO......................... ...................................45
Caráter.................. ...................................60, 65s., 173
$HIGHCHARUNICODE.......... .............176f.
c: índice - 539
540 - c: índice
c: índice - 541
542 - c: índice
Aulas de amigos.................................................. ..211 IDE......20, 24, 112, 145, 250, 279, 288, 299, 522, 531
De................................................ ..................462
Função.............................. ..............35, 101, 530 Ponteiro de Identificadores......................... ...................................27f.
Função........................... ................120 IDispatch................................................. ........499 Comparador
de IEqualidade........................................ .419
G Se ................................................ ...................34, 87f.
Se então................................................. .....172, 189
Bando dos Quatro............................................... ...529 Coleta IInterface..............................311ss., 321, 325, 499
de lixo.........................208, 354 Restrições IInvokable.. .................................................. ...499
genéricas...... ...................................398 Contêineres
Implementação.........................................34, 38
Genéricos............. ............................406 Dicionário
Implementos................................................322, 499
Genérico............ .......................412 Métodos Implícito ....................................149, 152, 425, 462
Genéricos........................ ....................390 Declaração de Tipo Em...... .................................................. .........35, 75
Genérico.......................... .....389 Inc......................................... .................63, 174, 279
ObterDiretórios........................................... ......507 Incluir........................... ............................42, 76
GetEnumerator.........................................94, 301f . Recuo................. ....................................30
Obter arquivos.................................................. ..........508 Indexadores.......... ................................................284
ObterHashCode.........................186, 475, 493 ÍndiceDe .................................................. ..........186
ObterMem.........................................162, 357, 372
IndexOfAny.................................... ...............186 Ocultação de
GetMemoryManagerState..............................368 informações................................. .........211
GetMemoryMap................. ............................368 Herança....................................230 , 237, 243
ObterAlinhamento de Bloco Mínimo................... .....370 Herdado...................................35, 222, 243, 253 Herdado
GetNumericValue......................................... .174 Obter de.. ..............................340, 378, 490f.
Pacotes............................................... .....460 Inicialização........................................34, 39, 337 Em
GetPropValue......................................... ......290 linha... .................................................. .....35, 116 Variáveis
GetType.......................................... .................454 Inline........................................ ...54, 90
GetTypeKind......................... ....................394 InnerException.................................270, 273
GetUnicodeCategory.......................... ...........174 Inserção ................................................133, 186, 513 Tamanho
ObterNomeUsuário.................................... .............122
da Instância.................................340, 399, 489f.
ObterWindowText................................. ...........372 Memória Inter................................................. .....................84
Global.................................... ...........530 Variáveis Int64.......................... ....................................60
Globais.........................53, 58, 306, 355
Inteiro.......... ................................................34, 60 Auxiliar de
Google.. .................................................. .........526 Ir tipo inteiro..................................61 Pagamento de
para........................................ ...........................98
juros... ...........................................69
Grafemas..................... ...................................167 Interface.... ................................................34f. , 38 Restrições
GUI............... ....................................527f., 530 de interface....................................400 Delegação de
GUID.... ...................................................312, 325, 417
interface..... ...................................321 ID da
interface............ ....................................417 Propriedades da
H Interface....... ...................................320
Interfaces.............. .................310f., 314, 327, 532 Internet das
Parar.................................................. ..................98
Haskell.............................. ................................105 Coisas...................... ............516 Classe
HasWeakRef................. ...................................394 Interposer.......................... ............328, 344 Auxiliares de
Registro Intrínseco......................... ...61 Auxiliares de Tipo
Pilha.............. ...........................................178, 531 HFA
Atributo... ................................................500 Intrínseco..........................350f.
IntToStr................................................. ...........84 Typecast
Alto. ...................................63, 127, 130, 174, 184
inválido.......................... ..........462
HPPGENAtributo..... ....................................500
EU
c: índice - 543
EstáVazio............ ....................................186
IsInArray............ .............................................175
ÉCarta... .................................................. .........66
M
ÉLetraOuDígito......................................... .........174ÉInferior........................................ .....................175 IsManagedType........................... ...................394 IsNullOrWhiteSp
544 - c: índice
N Divisão.................................................. .............78f.
Mod................................................. .............78
Nome................................................. ......304, 505 Construtores Não................................... ............................78
Nomeados......................................220 Tipos Ou............. .............................................. 78
nomeados.................................................. ....70 Espaços para Shl... .................................................. ...........78
nome......................................... ....38, 43 Shr......................................... ...........................78
NativeInt......................................... .................61
NativeUInt......................... ........................162 Exceções
aninhadas........................ ...................270 Tipos
aninhados.......................... .......................227
NeverSleepOnMMThreadContention............370
Novo........... .................................................. .....162 Nova
Instância........................................... ........374
Herança........................................ ...............532Nick
Hodges................................ ...................309 Niklaus
Wirth.......................... ..............85, 521f.
Nada................................................36, 159 , 209, 361
Notificação............................................. .........411
Agora........................................ ...........................80f.
AgoraUTC.................................................. ...........82
NULO.................................... ...........................159 Declaração
nula.................. ..............................86
CaixaNúmera.................. ....................................89 Tipos Xou..................... ...........................................78:=..... .................................................. .
Numéricos.......... ........................................59
P
O
Pacote................................................. ............34
Objeto................................................. .......35, 532 Inspetor PadEsquerdo.......................... ........................186 Biblioteca de
de Objetos....................293, 296, 298, 300 Modelo de Programação Paralela................444, 516
Referência de Objetos....... ...................207, 357 ParamCount.. .................................................. 499
Programação Orientada a Objetos................200, 202 Parâmetros................................................ ......106 Tipo
Objective-C... ................................25, 78, 147, 157 Paramétrico......................................... .....385
Objetos.......... ...........................................202, 206 Objetos ParamStr......................................... ..............499
como parâmetros.. ...................................358 Pai.................................... ....................218, 342
Estranho............ .................................................. ....63 ParentClass.......................... ...........................340
De.......................................... ...........................34 Do Analisar..................... ....................................62, 186
Objeto.................. ............................292, 296 Pascal......... ...........................................119, 520f.
Ointeres................. ...........................................141 Pedro
Ligado.... .................................................. ..............256
OnChange.......................... .....................342
OnClick........................... ......292f., 296, 304, 440
OnCreate................................... ...............171, 334
OnException......................... ............260, 266
OnMouseDown................................. .............342
POO.................................... ..............................532
Parâmetros de matriz aberta.................. ...................133
Princípio aberto-fechado.......................... ..230, 343
Operador.......................................... ....................
E................................................. ...........78f.
Como................................................. ................78 Wood.................................................. ..........5PHP............................................
c: índice - 545
546 - c: índice
c: índice - 547
548 - c: índice
System.Actions........................................515 EM
System.AnsiStrings ....................................515
Sistema.Caráter.......... ..............66f., 174, 515 Var.................................................. ......34, 50, 108
Sistema.Classes........................... 502, 510, 515 Variáveis...................................... .....................50
System.Contnrs.................................406, 515 Variante........................... ...................................135
System.ConvUtils.. ....................................515 Registros de variantes............. .................................140
System.DateUtils......... ........................80, 515 Variantes............... ....................................157, 159
Sistema.Dispositivos.................... .......................515 VCL......... .............21, 39, 503, 506, 522, 528, 536
Diagnóstico.doSistema......................... ......117, Virtuais........................ ....................35, 239ss., 244
515 Sistema.Hash...................................... .........515 Métodos de aula virtual.................... .................332
System.ImageList.................................... ..515 Interceptores de Método Virtual.........................470
System.IniFiles.........................................516 Método Virtual Tabela...................................244
System.IOUtils..................74, 507, 509, 516 Métodos Virtuais........ ....................................536
System.JSON.................. ............................516 Visibilidade.......... ...........................................42, 58
Sistema.Matemática................. .........69, 111, 116, Visual Básico. .................................................. ..142
516 Sistema.Mensagens.............................. .......516 Biblioteca de Componentes Visuais....................21, 39
System.NetEncoding.........................516 Herança de Formulários Visuais............ ............250
System.RegularExpressions. ......................516 ligações visuais ao vivo.......................... .........485
Sistema.Rtti......................... .................454, 516 VmtInstanceSize......................... ...........379
System.StrUtils........................... 173, 189, 516 VmtSelfPtr.................................. ..................379 Atributo
Sistema.SyncObjs........................................516 Volátil.............................. ...............500
Sistema .SysUtils....80, 82, 173, 189, 195, 258, VTipo................................. ..............................159
338, 351, 438, 516
Sistema.Threading...................... ................516 EM
Tipos de sistema......................... ...............516
Referências fracas..................................317, 366
System.TypInfo.................289, 395, 454, 516
WeakAttribute....... ....................................500
System.Variantes... ...................................516
Enquanto....... .................................................. 35, 94f.
Sistema.Zip........ ...........................................516
Espaço em branco.................................................. .......29
Winapi.Windows.... ....................................122
WideChar......................................... ...........65, 67
Nome da Unidade........... .............................................
WideString................................. ...................199
38 Nomes de escopo de unidade ....................................39
Janelas.......................... ....47, 526, 529f., 536 API do
Nome da Unidade.... ..................................................
Windows...................................122 , 245, 333
.490 Tipos Sem Nome......................................... ..70
Com............................................. ............35, 142f.
Inseguro................................................. ...............367
WM_USER................................................. ....244
Referências inseguras......................... ...........317
Palavra............................................ ............60
Atributo Inseguro.................................. .........500
Escrever........................... .............36, 276, 509, 511f.
Até........................................ ...........................35
Escritoras................................................. ............510
UpCase..................... ........................................175
Maiúsculas...... ....................................................28
Maiúsculas.................................................. ......186 X
Tipos de dados definidos pelo usuário....................70 Documento XML.................................................. ...........26
Usos.. .................................................. ...34, 41, 44 Fluxo de XML...................................... ...........478
UTF-16......................................... ..............169, 195
UTF-32.............................. ..............................169 #
UTF-8................ ............................169f., 195, 197
UTF32Char.............. ........................................198 #................................................ ......................176
UTF8String........ ...........................................198f.
€
€................................................ ..............167, 176
c: índice - 549