Delphi

Generics in Delphi: dizionari e filtri di array riutilizzabili

calendar_today personTeam EGSOFT schedule4 min di lettura
info

Disclaimer: le informazioni pubblicate in questa sezione hanno finalità di pura divulgazione tecnica. EG Software S.r.l. non si assume alcuna responsabilità per un utilizzo improprio dei contenuti, né per eventuali danni diretti o indiretti derivanti dalla loro applicazione, e non garantisce l'aggiornamento, l'accuratezza o la completezza delle informazioni riportate. Prima di utilizzare in produzione codice o procedure qui descritte, verificane sempre l'adeguatezza al proprio contesto.

Le collection generiche di System.Generics.Collections sono uno degli strumenti più sottoutilizzati di Delphi: permettono di scrivere codice riutilizzabile senza rinunciare al type-checking a compile-time. Vediamo due pattern pratici — un dizionario chiave-valore incapsulato in una classe di servizio, e un filtro generico per array — che tornano utili in tanti progetti reali.

TDictionary<K,V>: incapsulare o ereditare?

Il modo più semplice per lavorare con un TDictionary è nasconderlo dietro una classe che ne gestisce il ciclo di vita ed espone solo i metodi che servono davvero:

unit ServizioAnagrafica;

interface

uses
  System.Generics.Collections, System.SysUtils, System.Classes;

type
  TAnagraficaCache = class
  private
    FDati: TDictionary<string, string>;
  public
    constructor Create;
    destructor Destroy; override;

    procedure Imposta(const Chiave, Valore: string);
    function Ottieni(const Chiave: string; out Valore: string): Boolean;
    procedure Rimuovi(const Chiave: string);
    function TutteLeVoci: TStringList;
  end;

implementation

constructor TAnagraficaCache.Create;
begin
  FDati := TDictionary<string, string>.Create;
end;

destructor TAnagraficaCache.Destroy;
begin
  FDati.Free;
  inherited;
end;

procedure TAnagraficaCache.Imposta(const Chiave, Valore: string);
begin
  // AddOrSetValue evita l'eccezione se la chiave esiste già
  FDati.AddOrSetValue(Chiave, Valore);
end;

function TAnagraficaCache.Ottieni(const Chiave: string; out Valore: string): Boolean;
begin
  Result := FDati.TryGetValue(Chiave, Valore);
end;

procedure TAnagraficaCache.Rimuovi(const Chiave: string);
begin
  FDati.Remove(Chiave);
end;

function TAnagraficaCache.TutteLeVoci: TStringList;
var
  Chiave: string;
begin
  Result := TStringList.Create;
  for Chiave in FDati.Keys do
    Result.Add(Format('%s: %s', [Chiave, FDati[Chiave]]));
end;

end.

Questo approccio "a incapsulamento" ha un vantaggio: la classe che usa TAnagraficaCache non sa nemmeno che sotto c'è un TDictionary, quindi in futuro potresti sostituirlo con un'altra struttura dati senza toccare il resto del codice.

L'alternativa è ereditare direttamente da TDictionary<K,V> per aggiungere metodi specifici, quando ti interessa comunque esporre l'intera interfaccia della collezione:

type
  TDizionarioContatori = class(TDictionary<Integer, string>)
  public
    procedure ImpostaOAggiorna(Chiave: Integer; const Valore: string);
    function RiepilogoComeTesto: string;
  end;

procedure TDizionarioContatori.ImpostaOAggiorna(Chiave: Integer; const Valore: string);
begin
  AddOrSetValue(Chiave, Valore);
end;

function TDizionarioContatori.RiepilogoComeTesto: string;
var
  Chiave: Integer;
begin
  Result := '';
  for Chiave in Keys do
    Result := Result + Format('%d: %s'#13#10, [Chiave, Items[Chiave]]);
end;

In generale conviene incapsulare quando la collezione è un dettaglio implementativo, ed ereditare solo quando vuoi davvero che il chiamante tratti l'oggetto come "un dizionario con qualcosa in più".

Filtrare un array con una classe generica

Lo stesso principio si applica bene anche a TArray<T>. Una classe generica di filtro puoi scriverla una volta e riutilizzarla per qualsiasi tipo, incluso un record:

type
  TFiltroArray<T> = class
  private
    FSorgente: TArray<T>;
  public
    constructor Create(const ASorgente: TArray<T>);
    function Filtra(const Predicato: TFunc<T, Boolean>): TArray<T>;
  end;

constructor TFiltroArray<T>.Create(const ASorgente: TArray<T>);
begin
  FSorgente := ASorgente;
end;

function TFiltroArray<T>.Filtra(const Predicato: TFunc<T, Boolean>): TArray<T>;
var
  Elemento: T;
begin
  SetLength(Result, 0);
  for Elemento in FSorgente do
    if Predicato(Elemento) then
    begin
      SetLength(Result, Length(Result) + 1);
      Result[High(Result)] := Elemento;
    end;
end;

Passare un TFunc<T, Boolean> come predicato, invece di un valore fisso da cercare come nell'esempio originale da cui questa classe è ispirata, rende il filtro molto più flessibile: puoi confrontare un campo Nome, un intervallo di date o qualunque condizione, senza scrivere una nuova classe ogni volta.

var
  Prodotti, Disponibili: TArray<TProdottoRecord>;
  Filtro: TFiltroArray<TProdottoRecord>;
begin
  Filtro := TFiltroArray<TProdottoRecord>.Create(Prodotti);
  try
    Disponibili := Filtro.Filtra(
      function(const P: TProdottoRecord): Boolean
      begin
        Result := P.Disponibile;
      end);
  finally
    Filtro.Free;
  end;
end;

Con poche righe ottieni una piccola libreria interna di utility generiche che puoi condividere fra i progetti, senza duplicare la stessa logica di ricerca in ogni form.

Altri articoli su Delphi grid_viewTutte le tematiche
Scrivici su WhatsApp