mandag den 31. oktober 2011

Implementering af "for in operatorer" vha. TEnumerator og  TEnumerable

Jeg har i et tidligere indlæg vist, hvordan man laver en typestærk liste ved hjælp af Generics. Det kostede ikke meget sved på panden i modsætning til i Delphi 2007, hvor man ikke har generics og således skal kode det hele selv.

Som jeg skrev i slutningen af mit indlæg, får man med brugen af TObjectList<T> eller TList<T> kun en standard enumerator - altså man kan "kun" benytte sig af den data type som den typestærke klasse indeholder. Hvis man vil noget andet, skal man stadig selv implementere sin enumerator.

Jeg vil i dette indlæg beskrive, hvordan man laver sine egne enumerators på en generisk klasse. Jeg har brugt en klasse, der kan vise helligdage i forskellige lande (Tyskland, Danmark, Norge, Sverige, USA, Grønland og Frankrig) på henholdsvis engelsk og det respektive sprog. Jeg vil i det følgende kun vise uddrag af koden, det komplette eksempel kan hentes her:  Helligdage demo.

Lad mig starte med lidt klasse-definitioner, her bragt i uddrag :

type
  THoliDay = class
  strict private
    ....
  public
    constructor Create(const Native, English: string; aDate: TDateTime);
    property NativeName: string read FNativeName write SetNativeName;
    property EnglishName: string read FEnglishName write SetEnglishName;
    property Date: TDateTime read FDate write SetDate;
    property DateAsStr: string read GetDateAsStr;
    property FriendlyDate: Boolean read FFriendlyDate write SetFriendlyDate;
  end;


  THolidayList = class(TObjectList<THoliDay>)
    ...
  public
    ...
    property NativeNames: TNativeNameCollection read GetNativeNames;
    property EnglishNames: TEnglishNameCollection read GetEnglishNames;
  end;



Så har jeg en klasse, der binder det hele sammen og regner på påsken, mm.

type
  TStdDato = class(THolidayList)
    ....
  public
    constructor Create;
    function GetEasterDay(Year: Word = 0): TDateTime;
    procedure MakeHoliDays(Year: Word = 0);
    function GetHoliDaysInMonth(Month: Integer = 0; Year: Integer = 0): THolidayList;
    function GetHoliDayName(aDate: TDateTime): string;
    property HoliDays[Index: Integer]: THoliDay read GeTHoliDay;
  published
    property Country: TCountryLocale read FCountry write SetCountry;
    property Year: Integer read FYear write SetYear;
    property MarkSunday: Boolean read FMarkSunday write SetMarkSunday;
    property MarkHolidays: Boolean read FMarkHolidays write SetMarkHolidays;
    property Language: TLanguage read FLanguage write SetLanguage;
    property SundayStr: string read FSundayStr;
  end;



Så langt så godt. Indtil videre er der ikke noget mystisk ved det.

Som det er nu, kan jeg gøre det følgende, når jeg skal bruge klassen TStdDato:

var
  STDDato: TStdDato;
begin
  STDDato := TStdDato.Create;
  for HolyDay in STDDato do
  begin
    ...
  end;
   
  FreeAndNil(STDDato);
end;

Men det jeg ønsker at opnå er:


var
  STDDato: TStdDato;
  aName: string;
begin
  STDDato := TStdDato.Create;

  ListBox1.Clear;
  for aName in STDDato.EnglishNames do
    ListBox1.Items.Add(aName);

  FreeAndNil(STDDato);
end;


Bemærk at jeg benytter mig af en string som enumerator. Det kræver man implementerer det selv.

Det første der skal gøres er, at implementere en nedarvning af TEnumerator<string>. Den har jeg valgt at lægge som en public type på THolidayList, da det er der den hører hjemme. Selve klassen minder meget om den vi så ovre i mit Delphi 2007 eksempel:

type
  THolidayList = class(TObjectList<THoliDay>)
  public type
    TEnglishNameEnumerator = class(TEnumerator<string>)
    private
      FList: THolidayList;
      FIndex: Integer;
      function GetCurrent: string; virtual;
    protected
      function DoGetCurrent: string; override;
      function DoMoveNext: Boolean; override;
    public
      constructor Create(aList: THolidayList);
      property Current: string read GetCurrent;
      function MoveNext: Boolean;
    end;
   ...


Implementeringen af klassen er triviel og kan ses i det komplette eksempel.

Nu hvor vi skal tilføje en enumerator til vores klasse, kræver det endnu en klasse som i dette tilfælde implementerer en nedarvning af TEnumerable<string>:

type
  TEnglishNameCollection = class(TEnumerable<string>)
  private
    FList: THolidayList;
    function GetCount: Integer;
  protected
    function DoGetEnumerator: TEnumerator<string>; override;
  public
    constructor Create(ADictionary: THolidayList);
    function GetEnumerator: TEnglishNameEnumerator; reintroduce;
    function ToArray: TArray<string>; override; final;
    property Count: Integer read GetCount;
  end;




Implementeringen af den er også ganske ligetil, så den vil jeg udelade her.

Til sidst skal vi bare have det hele koblet på klassen, det gøres således:

type
  THolidayList = class(TObjectList<THoliDay>)
   ....
  private
    FEnglishNames: TEnglishNameCollection;
     ...
    function GetEnglishNames: TEnglishNameCollection;
  public
     ...
     property EnglishNames: TEnglishNameCollection read GetEnglishNames;
  end;

...

function THolidayList.GetEnglishNames: TEnglishNameCollection;
begin
  if FEnglishNames = nil then
    FEnglishNames := TEnglishNameCollection.Create(Self);
  Result := FEnglishNames;
end;



Hermed har vi opnået vores mål. Den nye enumerator kan nu bruges som vi ønskede:

var
  STDDato: TStdDato;
  aName: string;
begin
  STDDato := TStdDato.Create;

  ListBox1.Clear;
  for aName in STDDato.EnglishNames do
    ListBox1.Items.Add(aName);

  FreeAndNil(STDDato);
end;


Den opmærksomme læser bemærker straks, at jeg i det ovenstående "kun" har tilføjet en enumerator for de engelske navne. Ville det være smart, hvis man også kunne gøre det samme med lokale navne? Ja, det kunne det, og det kan gøres nemt og enkelt ved hjælp af arv:

type
  THolidayList = class(TObjectList<THoliDay>)
  public type
    ...

    TNativeNameEnumerator = class(THolidayList.TEnglishNameEnumerator)
    private
      function GetCurrent: string; override;
    end;
    
    ...

    TNativeNameCollection = class(THolidayList.TEnglishNameCollection)
    public
      function GetEnumerator: TNativeNameEnumerator; reintroduce;
    end;
  private
    ...
    FNativeNames: TNativeNameCollection;

    ...
    function GetNativeNames: TNativeNameCollection;
  public
    ...

    property NativeNames: TNativeNameCollection read GetNativeNames;
  end;

...

{ THolidayList.TNativeNameEnumerator }

function THolidayList.TNativeNameEnumerator.GetCurrent: string;
begin
  Result := FList[FIndex].NativeName;
end;

{ THolidayList.TNativeNameCollection }

function THolidayList.TNativeNameCollection.GetEnumerator: TNativeNameEnumerator;
begin
  Result := TNativeNameEnumerator.Create(FList);
end;
Som sagt var der ikke mange ben i det, men jeg synes det skulle med alligevel for at gøre dette indlæg komplet.

Her til slut vil jeg lige endnu en gang nævne at hele eksemplet kan hentes her:  Helligdage demo og i den forbindelse vil jeg da lige vise er skærmbillede derfra :


Jens Borrisholt

Ingen kommentarer:

Send en kommentar