Elementy języka Delphi

Adam Boduch

Firma Borland zrobiła wiele aby dotychczasowe projekty napisane pod system Win32 działały również w Delphi 8 więc przy odrobinie szczęścia uda się przenieść swój program na platformę .NET z minimalnym oporem. Jeżeli będziesz miał naprawdę dużo szczęścia to od razu możesz skompilować aplikację w Delphi 8 i od tej pory Twój program będzie działał pod platformę .NET. Wadą takiego rozwiązania jest to, że mimo, iż program działa pod .NET, to rozmiar aplikacji wykonywalnej będzie o wiele większy niż w przypadku gdyby taki sam program kompilować w Delphi 7.

1 Przestrzenie nazw
2 Obiekty w Delphi
     2.1 Transformacja modułu
3 Kompilacja warunkowa
4 Brakujące elementy
     4.2 Niebezpieczny kod
          4.2.1 Komunikaty niebezpiecznego kodu
     4.3 Wskaźniki
     4.4 Asembler
     4.5 Pliki typowane
     4.6 Pliki tekstowe
          4.6.2 Przykład: migracja aplikacji do .NET ? operacje na plikach
     4.7 Dyrektywa absolute
     4.8 Słowo kluczowe object
     4.9 Dynamiczna alokacja danych
     4.10 Dyrektywa exports
5 Łańcuchy w Delphi
     5.11 Unikod w łańcuchach
     5.12 ShortString
     5.13 AnsiString
     5.14 WideString
     5.15 Łańcuch z zerowym ogranicznikiem
     5.16 Klasa System.String
     5.17 Klasa StringBuilder
6 Typy liczbowe
7 Destruktory
     7.18 Finalize
     7.19 Dispose
8 Komunikaty

W tej sekcji skupię się raczej na elementach, które zostały zmienione lub usunięte, w nowej wersji.

Przestrzenie nazw

Podstawową zmianą, rzucającą się w oczy, to nowa organizacja modułów w Delphi. Już Delphi 7 pozwalało na używanie w nazwach modułów, znaków kropki, lecz teraz specyficzny zapis przestrzeni nazw jest charakterystyczną cechą.

W poprzednich wersjach Delphi wyglądało to tak:

uses
    Windows, Messages, SysUtils, Classes, Graphics, Controls,         
    Forms, Dialogs, StdCtrls;

Wraz z pojawieniem się .NET, zmieniła się organizacja zapisu nazw modułów i nosi nazwę przestrzeni nazw (ang. namespaces). Teraz np. moduł Windows nazywa się Borland.VCL.Windows i jest zapisany pod postacią pliku Borland.VCL.Windows.pas, a nie Windows.pas. Dzięki takiemu zapisowi można zachować pewną hierarchiczną organizację modułów. I tak w .NET głównym podzespołem jest System (jest najwyżej w hierarchii), dalej są System.Security, później System.Security.Cryptography itp. itd.

Wprawdzie nazwy modułów z Delphi 7 uległy zmianie, lecz nie musisz dokonywać poprawek na tym gruncie, w projekcie Delphi 8. Kompilator dodaje bowiem, w sposób automatyczny prefiks Borland.VCL. Opcję tę możesz zmienić w oknie opcji projektu (Project | Project Options), na zakładce Directories/Conditionals.

Należy zrozumieć, iż podzespół .NET może posiadać wiele przestrzeni nazw, w których znajdują się klasy.

Przykładowo: utwórz nowy projekt aplikacji konsolowej, a następnie dodaj do niego dwa moduły (menu File | New | Unit); nazwij je Samochod oraz Samochod.Maluch. Niech oba moduły będą zawierały stałą o tej samej nazwie:

unit Samochod.Maluch;

interface

const
  Name = 'Samochod.Maluch';

implementation

end.

{ moduł drugi }

unit Samochod;

interface

const
  Name = 'Samochod';

implementation

end.

Jeżeli do programu dołączymy teraz dwa moduły, w których znajduje się taka sama stała i zechcemy ją wykorzystać to kompilator odczyta wartość z drugiego w kolejności modułu:

program Project3;

{$APPTYPE CONSOLE}

uses
  Samochod.Maluch, Samochod;


begin
  Writeln(Name);
  Readln;
end.

Jeżeli chcemy odczytać wartość stałej z konkretnego modułu to należy wcześniej podać jego nazwę:

  Writeln(Samochod.Maluch.Name);

Co prawda nie jest to żadna innowacja w Delphi, ale samo namespace pozwala zachować pewną przejrzystość w nazewnictwach modułów i ich powiązaniach. Daje to, więc obraz pewnej hierarchiczności obiektów. W taki sam sposób będziesz się odwoływał do klas znajdujących się w danych przestrzeniach.

Obiekty w Delphi

Należy zaznaczyć, że VCL został przystosowany do .NET w taki sposób, aby łatwa była migracja aplikacji do nowego środowiska. Przykładem może być klasa TObject, która od tej pory wskazuje na klasę System.Object.

Oprócz tego, że klasa TObject przejmuje metody i właściwości po klasie System.Object, to dodatkowo rozszerza funkcjonalność o metody i właściwości obecne w klasie TObject w poprzednich wersjach Delphi. Wszystko odbywa się przy pomocy nowego w Delphi, mechanizmu Class helpers.

Piszę o tym aby uświadomić, że w .NET jest w pełni obiektowy jednak jego struktura umożliwia w wielu przypadkach wykorzystanie obiektów bez wywoływania konstruktorów. W ten sposób np. wszystkie typy zmiennych, zgodne z CTS są klasami. Można przytoczyć tutaj przykład typu String, który wskazuje w rzeczywistości na klasę System.String. Tak samo jest z typem Integer, który wskazuje na klasę System.Int32. Poniższa tabela zawiera spis typów Delphi oraz ich odpowiedników w CLR.

Typ DelphiTyp CLR
[[Delphi/Integer]]System.Int32
[[Delphi/Cardinal]]System.UInt32
[[Delphi/SmallInt]]System.Int16
[[Delphi/Int64]]System.Int64
[[Delphi/Byte]]System.Byte
[[Delphi/Word]]System.UInt16
[[Delphi/Single]]System.Single
[[Delphi/Double]]System.Double
[[Delphi/Extended]]System.Double
[[Delphi/Real]]System.Double
[[Delphi/Char]]System.Char
[[Delphi/WideChar]]System.Char
[[Delphi/Boolean]]System.Boolean
[[Delphi/String]]System.String
[[Delphi/AnsiChar]]Borland.Delphi.System.AnsiChar
[[Delphi/AnsiString]]Borland.Delphi.System.AnsiString
[[Delphi/WideString]]System.String

Niektóre z typów mają charakter ogólnodostępny, lecz niektóre z typów przedstawionych w tabeli są specyficzne jedynie dla Delphi - przykładowo AnsiString. Wskazuje on na klasę Borland.Delphi.System.AnsiString, która znajduje się w przestrzeni nazw Borland.Delphi.System, a ta z kolei jest automatycznie kompilowana w podzespół powstały przy użyciu kompilatora Delphi.

O typie String, szczegółowo opowiem w dalszej części tego dokumentu.

W języku takim, jak np. C# programowanie odbywa się w sposób obiektowy ? język ten jest ściśle powiązany z programowaniem obiektowym, a każdy program musi posiadać przynajmniej jedną klasę. W Delphi można programować strukturalnie, lecz tylko na pozór. W rzeczywistości kod jest przez kompilator przekształcany na obiekt i w takiej postaci jest zapisywany do kodu IL. Przyjrzyj się poniższemu modułowi:

unit Boduch.Test;

interface

  function GetName : String;

implementation

function GetName : String;
begin
  Result := 'Boduch.Test';
end;

end.

Moduł nosi nazwę Boduch.Test, zawiera jedną funkcję, nie mającą specjalnie żadnego znaczenia. Po użyciu takiego modułu, w swojej aplikacji i skompilowaniu programu kompilator dokona wersji takiego kodu do takiej postaci:

unit Boduch.Test;

interface

type
  &Unit = class
  public
    class function GetName : String; static; 
  end;

implementation

class function &Unit.GetName : String;
begin
  Result := 'Boduch.Test';
end;

end.

Na poparcie mych słów, możesz uruchomić program ildasm.exe i przeanalizować w nim naszą aplikację

dotnet_2.jpg

Transformacja modułu

Jak widzisz, kompilator Delphi przekształca kod modułu na klasę. Wspomniałem jednak tylko o procedurach i funkcjach modułu, które są przekształcane na metody. Tak samo dzieje się ze zmiennymi znajdującymi się w module. Są one przekształcane na pola klasy i umiejscawiane w sekcji Public:

unit Boduch.Test;

interface

var
  Variable : Integer;

Powyższy kod zostanie przekształcony na:

type
  &Unit = class
  public
    class Variable : Integer; 
  end;

To samo można powiedzieć o sekcji Initialization, która jest opcjonalna w modułach Delphi. Podczas kompilacji programu, tworzony jest konstruktor w klasie i kod z sekcji Initialization umieszczany jest w konstruktorze:

initialization
  Variable := 10;

Powyższy kod umiejscawiany jest w:

class constructor &Unit.Create; static;
begin
  Variable := 10;
end;

Odkąd moduły mogą posiadać blok begin..end, kod znajdujący się w tej sekcji umieszczany jest w metodzie o nazwie odpowiadającej nazwie modułu:

unit Boduch.Test;

interface

var
  Variable : Integer;
  function GetName : String;

implementation

function GetName : String;
begin
  Result := 'Boduch.Test';
end;

begin
  Variable := 20;
end.

Moduł przekształcany jest do takiej postaci:

unit Boduch.Test;

interface

type
  &Unit = class
  public
    class Variable : Integer; 
    class function GetName : String; static; 
    class procedure Boduch.Test;
  end;

implementation

class function &Unit.GetName : String;
begin
  Result := 'Boduch.Test';
end;

class procedure &Unit.Boduch.Test;
begin
  Variable := 10;
     end;

end.

Dociekliwy Czytelnik może także spytać, co z sytuacjami, w których zmienne globalne mają nadane wartości już w trakcie pisania programu:

var
  Variable : Integer = 10;

W takim wypadku wartość pola Variable jest nadawana w konstruktorze klasy Unit.

Kompilacja warunkowa

Mimo tego, iż większość kodu kompiluje się bez problemu w różnych wersjach Delphi, czasami istnieją sytuacje, gdzie dany kod nie działa na starszym lub nowszym kompilatorze (szczególnie w przypadku Delphi 8). W takich sytuacjach, gdy piszemy kod przeznaczony dla kilku wersji kompilatorów, istnieje możliwość umieszczania dyrektyw opisujących, dla jakiej wersji kompilatora przeznaczony jest dany fragment.

Dzięki temu w jednym pliku źródłowym mogą znajdować się fragmenty przeznaczone dla Delphi 1 (platforma 16 bitowa) oraz dla wyższych wersji (platforma 32 bitowa). Delphi 7 oraz Delphi 8 rozpoznają także wersję kodu przeznaczoną dla .NET.

Oto przykład kodu przeznaczanego dla kompilacji w Delphi 7:

{$IFDEF VER150}
procedure Grozna;
var
  P : PChar;
begin
  P := 'Łańcuch typu PChar';
  MessageBox(0, P, '', 0);
end;
{$ENDIF}

Normalnie kompilator w takim kodzie wskazałby błąd, gdyż typ PChar nie jest dozwolony w .NET. Ujmując odpowiedni fragment kodu dyrektywami {$IFDEF} powodujesz, iż kompilator ignoruje ową procedurę (tak jak w powyższym przykładzie) i nie jest ona w ogóle wykonywana w programie skompilowanym w Delphi 8.

Duże znaczenie w tej dyrektywie ma fragment VER150, który oznacza fragment kodu przeznaczony dla Delphi 7. W poniższej znajdują się inne możliwe wartości.

Wersja DelphiSymbol
Delphi 1VER80
Delphi 2VER90
Delphi 3VER100
Delphi 4VER120
Delphi 5VER130
Delphi 6VER140
Delphi 7VER150
Win32WIN32
Win16WINDOWS
.NETCLR

Pamiętaj o dyrektywie {$ENDIF} zakańczającej blok warunkowy. Istnieje także dyrektywa {$ELSE} która działa na identycznej zasadzie co instrukcja else w Delphi. Przykładowo możesz pisać program przeznaczony zarówno dla Delphi 8 i Delphi 7:

uses
  {$IFDEF CLR}System.Windows.Forms{$ELSE}Forms{$ENDIF};

Dobrym rozwiązaniem jest na początku programu zadeklarowanie instrukcji takiej jak poniżej, jeżeli np. kod źródłowy przeznaczony jest jedynie dla kompilatorów Win32:

{$IFNDEF MSWINDOWS}
{$MESSAGE FATAL 'Ten kod może być skompilowany jedynie dla środowiska Win32'}
{$ENDIF}

W takim wypadku przy próbie kompilacji w Delphi 8, kompilator wyświetli komunikat: Ten kod może być skompilowany jedynie dla środowiska Win32.

Brakujące elementy

Za sprawą przystosowania Delphi do .NET nie tylko można zaobserwować pojawienie się wielu nowych elementów języka, ale również - niestety ? parę takich elementów uległo likwidacji. Oznacza to, że konieczna będzie w niektórych wypadkach edycja kodów źródłowych, tak, aby kompilator Delphi nie pokazywał błędów.
Oto niektóre elementy, które zostały zlikwidowane w Delphi 8:

Zapewne dla wielu, dużą stratą będzie brak plików typowanych (inaczej zwanych rekordowymi) oraz funkcji BlockRead i BlockWrite. Należy w tym wypadku poszukać rozwiązania zastępczego ? np. strumieni (klasa TStream, w VCL.NET lub Stream w podzespole System.IO) lub baz danych.

Niebezpieczny kod

Środowisko .NET jest uważane za bezpieczne, więc niektóre elementy uważane za niebezpieczne zostały zlikwidowane ? jak np. wskaźniki. Istnieje jednak sposób, aby mimo tego z nich skorzystać. Do tego służy odpowiednia dyrektywa {$UNSAFECODE ON}, która umożliwia korzystanie z niebezpiecznych elementów języka. Powyżej wypisałem elementy języka, które uległy likwidacji jednak przy odrobinie wysiłku z niektórych da się skorzystać, właśnie po opatrzeniu kodu dyrektywą {$UNSAFECODE}. Może to być dobre rozwiązanie na wstępnym procesie migracji aplikacji z Win32 do .NET.

Tu jednak mała uwaga! Niekiedy konieczne stanie się stworzenie osobnej procedury, w której umieścimy cały niebezpieczny kod. Taka konieczność najdzie w przypadku, gdy kompilator wskaże błąd: [Error] XYZ.dpr(11): Unsafe code only allowed in unsafe procedure. Przykładowo, jeżeli chcemy skorzystać ze wskaźników w programie, w taki sposób jak poniżej, kompilator nam na to nie zezwoli.

program XYZ;

{$APPTYPE CONSOLE}

{$UNSAFECODE ON}
var
  P : ^String;
  S : String;
begin
  S := 'Boduch';
  P := @S;
  P^ := 'Adam';
{$UNSAFECODE OFF}
end.

W takim wypadku trzeba taki kod (z wykorzystaniem wskaźników) umieścić w procedurze, którą dodatkowo ? należy opatrzyć klauzulą unsafe (patrz listing 1.).

Listing 1. Przykład użycia wskaźników w Delphi 8

program P6_1;

{$APPTYPE CONSOLE}

{$UNSAFECODE ON}
procedure UnsafeProc; unsafe;
var
  P : ^String;
  S : String;
begin
  S := 'Boduch';
  P := @S;
  P^ := 'Adam';

  Writeln(S); // wyświetli napis 'Adam'
  Readln;
end;
{$UNSAFECODE OFF}

begin
  UnsafeProc;
end.

Nie wolno zapominać o słowie kluczowym unsafe! Zarówno dyrektywa {$UNSAFECODE} jak i słowo unsafe musi stanowić całość ? w razie pominięcia jednego z tych elementów, program nie zostanie skompilowany.

Komunikaty niebezpiecznego kodu

W momencie wykrycia niebezpiecznego kodu, w czasie kompilacji, w oknie komunikatów (Message View), Delphi wyświetli ostrzeżenia. Istnieje możliwość wyłączenie ostrzeżeń stosując odpowiednie dyrektywy w kodzie. Delphi rozróżnia trzy komunikaty błędów: unsafe type (niebezpieczny typ), unsafe code (niebezpieczny kod) oraz unsafe cast (niebezpieczne rzutowanie). Wyłączenie tych komunikatów następuje poprzez zastosowanie poniższych dyrektyw:

<tt>{$WARN UNSAFE_TYPE OFF}
{$WARN UNSAFE_CODE OFF}
{$WANR UNSAFE_CAST OFF}</tt>

Wskaźniki

Jak pokazywałem w poprzednim przykładzie i na listingu 1. ? istnieje możliwość wykorzystania wskaźników (tj. operatora ^ oraz @, a także typu Pointer) dzięki zastosowaniu dyrektywy {$UNSAFECODE}.

Asembler

W poprzednich wersjach Delphi możliwe było umieszczanie w kodzie wstawek Asemblerowych (zresztą nie tylko w Delphi ? taka możliwość jest także w językach Turbo Pascal, C/C++ i innych). Polegało to na umieszczeniu kodu Asemblera pomiędzy znacznikami Asm i End:

asm
{ kod Asemblera }
end;

W Delphi 8, kompilator taki kod potraktuje wyświetleniem komunikatu z błędem: [Error] P6_2.dpr(9): Unsupported language feature: 'ASM'.

Pliki typowane

Pliki typowane (inaczej zwane plikami rekordowymi) to sposób na zapisanie całych określonych porcji danych w pojedynczym pliku. Dla przypomnienia dodam, że zapisanie danych w postaci rekordu, do pliku wiązało się z zadeklarowaniem określonej struktury (rekordu), a następnie typu:

type
  TNowyPlik = file of TRekord;

Począwszy od Delphi 8, konstrukcja typu File of jest ?przestarzała? i zabroniona. Nie zaleca się już używanie tego typu zapisu. Technologią jakiej powinni teraz używać programiści do przechowywania danych tekstowych ? jest XML lub strumienie.

W przypadku próby zastosowania plików typowanych, kompilator wyświetli komunikat błędu: [Error] P_2.dpr(11): Unsupported language feature: 'typed or untyped file'.

Mechanizm plików typowanych może być w dość prosty sposób zastąpiony strumieniami! VCL.NET posiada klasę TStream oraz klasy pochodne (np. TFileStream ? wykorzystywana w czasie pracy z plikami). Możemy także skorzystać z klasy Stream dostarczanej przez .NET (klasa znajduje się w przestrzeni nazw System.IO).

Pliki tekstowe

Pliki tekstowe nadal są dostępne w Delphi (typ TextFile), aczkolwiek do przechowywania informacji zalecane jest korzystanie z baz danych lub XML.

Należy zauważyć brak w Delphi 8 funkcji BlockRead oraz BlockWrite, które często służyły do operacji na plikach. W łatwy sposób można jednak skorzystać ze strumieni (klasa TFileStream) w celu manipulowania zawartością pliku.

Funkcje BlockRead oraz BlockWrite można równie dobrze zastąpić mechanizmem baz danych. Zastanów się czy wykorzystanie plików tekstowych, w Twoim programie jest rzeczywiście koniecznością. Bazy danych mogą się okazać często prostszym i bardziej efektywnym sposobem na przechowywanie danych.

Przykład: migracja aplikacji do .NET ? operacje na plikach

Na listingu 2. przedstawiony został kod źródłowy aplikacji napisanej w Delphi 7. Ten prosty przykład pokazuje możliwości wykorzystania typu File oraz funkcji BlockWrite/BlockRead w celu skopiowania zawartości pliku.

W poniższym przykładzie zaprezentowałem przykład kopiujący pliki i napisałem do tego własną procedurę. W Delphi do kopiowania plików służy funkcja CopyFile, której deklaracja znajduje się w pliku Borland.VCL.Windows.pas, a wygląda tak:

function CopyFile(lpExistingFileName, lpNewFileName: string; bFailIfExists: BOOL): BOOL;

Listing 2. Kod źródłowy programu napisanego w Delphi 7

unit MainFrm;

interface

uses
  Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms,
  Dialogs, StdCtrls, ComCtrls;

type
  TMainForm = class(TForm)
    OpenDialog: TOpenDialog;
    GroupBox1: TGroupBox;
    lblFile: TLabel;
    pbCopy: TProgressBar;
    btnCopy: TButton;
    procedure btnCopyClick(Sender: TObject);
  private
    { Private declarations }
  public
    { Public declarations }
  end;

var
  MainForm: TMainForm;

implementation

{$R *.dfm}

procedure TMainForm.btnCopyClick(Sender: TObject);
var
  SrcFile, DstFile : File; { plik źródłowy i plik przeznaczenia }
  FSize : Integer;       { rozmiar kopiowanego pliku }
  Bytes : Integer;       { ilość odczytanych danych }
  Buffer : array[0..255] of byte;  { bufor przechowujący dane }
  TotalSize : Integer;     { ilość skopiowanych już bajtów }
begin
  if OpenDialog.Execute then
  begin
  { wyświetl na etykiecie ścieżkę kopiowanego pliku }
    lblFile.Caption := 'Plik ' + OpenDialog.FileName;
    AssignFile(SrcFile, OpenDialog.FileName);
    try
      Reset(SrcFile, 1); { otwórz plik }
      FSize := FileSize(SrcFile); { odczytaj rozmiar pliku }
      pbCopy.Max := FSize;  { max. pozycja na pasku postępu }

      AssignFile(DstFile, 'C:\' + ExtractFileName(OpenDialog.FileName) + '~');
      try
      { utwórz plik }
        Rewrite(DstFile, 1);

        repeat
          Application.ProcessMessages;

          { odczytaj dane }
          BlockRead(SrcFile, Buffer, SizeOf(Buffer), Bytes);
          if Bytes > 0 then  { jeżeli liczba odczytanych bajtów jest większa od 0 }
          begin
          { przypisz odczytane dane do pliku }
            BlockWrite(DstFile, Buffer, Bytes);
            TotalSize := TotalSize + Bytes;
          end;
          { pozycja na pasku postępu }
          pbCopy.Position := TotalSize;
          
        until Bytes = 0;

      finally
        CloseFile(DstFile);
      end;

    finally
      CloseFile(SrcFile);
    end;
  end;
end;

end. 

Główną ideą programu jest kopiowanie pojedynczych fragmentów danych z jednego pliku do drugiego. Dzięki temu możemy na komponencie TProgressBar, pokazać postęp w kopiowaniu pliku. Całość realizujemy poprzez funkcje BlockRead oraz BlockWrite.

Program realizujący tę samą czynność przy użyciu strumieni zamieściłem na listingu 3. Delphi jest na tyle elastyczne, że wystarczy otworzyć projekt napisany w Delphi 7, zmodyfikować nieco kod, a następnie ? skompilować. Żadne inne zmiany nie są konieczne, w przypadku naszego przykładowego programu.

Listing 3. Program realizujący kopiowanie przy użyciu strumieni

unit MainFrm;

interface

uses
  Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms,
  Dialogs, StdCtrls, ComCtrls, System.ComponentModel;

type
  TMainForm = class(TForm)
    OpenDialog: TOpenDialog;
    GroupBox1: TGroupBox;
    lblFile: TLabel;
    pbCopy: TProgressBar;
    btnCopy: TButton;
    procedure btnCopyClick(Sender: TObject);
  private
    { Private declarations }
  public
    { Public declarations }
  end;

var
  MainForm: TMainForm;

implementation

{$R *.dfm}

const
  BuffSize = 1023;

procedure TMainForm.btnCopyClick(Sender: TObject);
var
  SrcFile, DstFile : TFileStream; { plik źródłowy i plik przeznaczenia }
  FSize : Integer;       { rozmiar kopiowanego pliku }
  Bytes : Integer;       { ilość odczytanych danych }
  Buffer : array[0..BuffSize] of byte;  { bufor przechowujący dane }
  TotalSize : Integer;     { ilość skopiowanych już bajtów }
begin
  if OpenDialog.Execute then
  begin
  { wyświetl na etykiecie ścieżkę kopiowanego pliku }
    lblFile.Caption := 'Plik ' + OpenDialog.FileName;
    SrcFile := TFileStream.Create(OpenDialog.FileName, fmOpenRead);
    try
      FSize := SrcFile.Size; { rozmiar pliku }
      pbCopy.Max := FSize;  { max. pozycja na pasku postępu }

      DstFile := TFileStream.Create('C:\' + ExtractFileName(OpenDialog.FileName) + '~', fmCreate);
      try
        repeat
          Application.ProcessMessages;

          Bytes := SrcFile.Read(Buffer, TotalSize, SizeOf(Buffer));

          if Bytes > 0 then  { jeżeli liczba odczytanych bajtów jest większa od 0 }
          begin
          { przypisz odczytane dane do pliku }
            DstFile.Write(Buffer, Bytes);
            TotalSize := TotalSize + Bytes;
          end;
          { pozycja na pasku postępu }
          pbCopy.Position := TotalSize;

        until Bytes = 0;

      finally
        DstFile.Free;
      end;

    finally
      SrcFile.Free;
    end;
  end;
end;

end.

Przestrzegam, iż to, czy dany projekt Win32 zostanie prawidłowo skompilowany w Delphi 8 zależy od jego złożoności oraz mechanizmów w nim wykorzystywanych. Ja miałem szczęście ? mój program był na tyle prosty, a komponenty obecne zarówno w Delphi 7 oraz w Delphi 8, iż przystosowanie aplikacji do .NET polegało na użyciu strumieni.

Celem powyższego programu było zaprezentowanie możliwości operowania na strumieniach, wraz z pokazaniem postępu kopiowania pliku. To samo zadanie można bowiem zrealizować przy pomocy funkcji CopyFrom klasy TFileStream:

DstFile.CopyFrom(SrcFile, SrcFile.Size);

Jak więc widzisz, to co wykonaliśmy wcześniej to jedynie wyważanie otwartych drzwi dla celów edukacyjnych. Celem tego ćwiczenia było pokazanie, że czasami chcąc importować aplikację do .NET należy pozbyć się pewnych nieaktualnych już elementów języka i wprowadzić pewne rozwiązanie zastępcze (w tym wypadku strumienie).

Dyrektywa absolute

Dyrektywa Absolute jest dość archaiczna ? istniała bowiem już w Turbo Pascalu, gdzie umożliwiała deklarowanie danej zmiennej pod określonym adresem w pamięci. Później, w Delphi 6 możliwe było jedynie zadeklarowanie danej zmiennej pod takim samym adresem, co inna zmienna:

var
   Zmienna: string[32];
   Dlugosc: Byte absolute Zmienna;

Borland nigdy nie zalecał używania tej dyrektywy w Delphi aż w końcu ? w Delphi 8 została ona zlikwidowana całkowicie. Należy więc zmienić kody źródłowe, tak, aby nie zawierały słowa kluczowego absolute.

Słowo kluczowe object

W poprzednich wersjach Delphi, słowem kluczowym, alternatywnym do Class, było Object. Pozwalało ono na deklarowanie obiektów, podobnie jak z użyciem słowa class:

type
  TFoo = object(TObject)
  { metody }
end;

W Win32, słowo kluczowe object zachowane zostało jedynie ze względów kompatybilności, a w .NET taka konstrukcja znikła całkowicie i nie jest dopuszczana przez kompilator.

Dynamiczna alokacja danych

Do tej pory zapewne nierzadko korzystałeś ze wskaźników oraz z dynamicznego przydziału pamięci w trakcie działania programu. Chodzi mi tutaj o funkcje: GetMem, FreeMem, AllocMem. Teraz niestety te funkcje nie mogą zostać użyte w programie.

Istnieje natomiast możliwość wykorzystania funkcji New, lecz jedynie w kontekście tablic dynamicznych:

type
  TSomeType = array of Byte;

var
  SomeType : TSomeType;

begin
  SomeType := New(TSomeType, 10);
  SomeType[0] := 0;
  SomeType[1] := 1;
end.

Jeżeli spróbujemy użyć funkcji New w innym kontekście, otrzymamy komunikat: NEW standard function expects dynamic array type identifier.

Dyrektywa exports

Osoby zajmujące się wcześniej programowaniem pod Win32, a w szczególności ? pisaniem bibliotek DLL, wiedzą zapewne, iż dyrektywa Exports zawarta w bibliotece DLL umożliwia eksportowanie funkcji i procedur. Delphi 8 umożliwia tworzenie bibliotek, lecz sama dyrektywa exports traktowana jest przez Delphi jako kod niebezpieczny. Dlatego też wystarczy umieścić w kodzie biblioteki klauzulę:

{$UNSAFECODE ON}

Od tego momentu możemy pisać w Delphi 8 biblioteki DLL, które mogą być normalnie wykorzystywane przez aplikacje działające w Win32.

Łańcuchy w Delphi

Jedną z głównych zmian w Delphi 8 jest brak typu PChar. Jako, że typ PChar, jak i PAnsiChar i inne typy potomne wywodzą się ze wskaźnika, a wskaźniki są w .NET niedozwolone, nie jest możliwe korzystanie również z typu PChar. Oczywiście tak jak w przypadku wskaźników ? typ PChar jest niebezpieczny (unsafe) lecz można użyć dyrektywy {$UNSAFECODE} aby go zastosować. Moim zdaniem, jako, że całe VCL.NET korzysta już jedynie z typu String (lub StringBuilder), warto w swoim programie również zamienić typ łańcucha.

W poprzednich wersjach Delphi typ PChar był wykorzystywany praktycznie jedynie w przypadku funkcji WinAPI ? VCL już wówczas korzystało z typu String. Teraz .NET zrezygnował całkowicie z typu PChar na rzecz String.

Unikod w łańcuchach

Największą zmianą jest traktowanie łańcuchów (typu String) jako unikodu.
Unicode (lub Unikod) to zestaw znaków mających zawierać w założeniu wszystkie pisma używane na świecie. Dzięki temu w dal odchodzi problem wyświetlania i kodowania znaków, a tym samym przystosowania interfejsu aplikacji do innych języków.

Jak dotąd w Delphi istniały trzy typy zmiennych łańcuchowych typu String.

TypMax. długośćRozmiar w pamięci
[[Delphi/ShortString]]255 znakówod 2 do 256 bajtów
[[Delphi/AnsiString]]231 znakówod 4 bajtów do 2 GB
[[Delphi/WideString]]230 znakówod 4 bajtów do 2 GB

Na platformie Win32, przełącznik {$H} określał, czy typ String będzie traktowany jako AnsiString (czyli długi łańcuch), czy ShortString (czyli łańcuch o określonej długości 255 znaków).

ShortString

Typ ShortString to podstawowy typ łańcuchowy Delphi 1, o ograniczonej długości 255 znaków. Użycie tego typu łańcucha jest szybie, ponieważ posiada ograniczoną długość; zmienną korzystającą z łańcucha ShortString można zadeklarować na dwa sposoby:

var
  S1 : ShortString; // dlugość ? 255 znaków
  S2 : String[255]; // długość - 255 znaków

Obie zmienne, w tym przypadku będą zmiennymi typu ShortString. W przypadku zmiennej S2 możesz równie dobrze zadeklarować zmienną, o mniejszej długości wpisując odpowiednią wartość w nawiasie kwadratowym.

Długość łańcucha ShortString umieszczona jest w pierwszym bajcie ? łatwo więc można odczytać rzeczywistą długość tekstu:

var
  S : ShortString;
  Len : Integer;
begin
  S := 'Hello World!';
  Len := Ord(S[0]);
end.

Zmienna Len zawierać będzie wartość 12.

Funkcja Ord służy do zamiany (konwersji) znaku typu Char do wartości liczbowej Integer. Odwrotną funkcję (zamiana wartości Integer do Char) realizuje funkcja Chr.

Typ ShortString zadeklarowany jest w przestrzeni nazw Borland.Delphi.System.

AnsiString

Typ AnsiString pojawił się po raz pierwszy w Delphi 2 ? jest to typ nie posiadający praktycznie ograniczenia w długości, przez co staje się on bardzo uniwersalny. Domyślne ustawienia Delphi nakazywały (w Delphi 2-7) traktować typ String tak samo jak typ AnsiString.

Delphi automatycznie zarządza pamięcią dla zmiennych typu AnsiString ? Ty nie musisz się niczym przejmować ? wadą tego łańcucha jest odrobinie wolniejsze działanie niż w przypadku ShortString, ale zalecane jest jego użycie, ze względu na brak limitów w długości łańcucha.

Odczyt długości łańcucha nie może tutaj odbyć się, z użyciem znaków [] jak to ma miejsce w łańcuchu ShortString, w tym wypadku można skorzystać z funkcji Length.

var
  S : AnsiString;
  Len : Integer;
begin
  S := 'Hello World!';
  Len := Length(S);
end.

WideString

Obecnie typy WideString oraz String są sobie równoważne i wskazują na klasę System.String i to jest jedna z większych zmian, jeśli chodzi o łańcuchy tekstowe w Delphi. Dzięki temu znika problem z tworzeniem aplikacji wielojęzycznych. Do tej pory, z punktu widzenia kompilatora typ String wskazywał na AnsiString ? teraz WideString jest równoważny z klasą String.

Łańcuch z zerowym ogranicznikiem

Pod tą nazwą kryją się w rzeczywistości zmienne typu PChar (wskaźnik do ciągu znaków typu Char). Nazwa wzięła się od tego, że łańcuch reprezentowany przez typ PChar jest zakończony znakiem o kodzie 0 (na wzór typów char* w języku C). Możliwy jest także następujący zapis deklarujący tablicę 256 elementową typu Char:

var
  S : array[0..255] of char;
begin
  S := 'Hello World!';
end.

Po deklaracji tablicy 256 elementowej typu Char możemy przypisywać do niej dane jak do zwykłego łańcucha. Typ S jest zakończony znakiem #0 informującym, że to koniec łańcucha.

Typ PChar jest, w rzeczywistości typem wskaźnikowych (pointers), który wskazuje na tablicę znaków.
Prawdopodobnie nie będziesz często używał typu PChar, tym bardziej, że jest to typ niebezpieczny (ang. unsafe), ale mimo to należało o tym wspomnieć.

Klasa System.String

Wspominałem wcześniej, że .NET jest całkowicie obiektowy. W rzeczywistości typ String jest klasą System.String, która udostępnia parę ciekawych funkcji służących do manipulacji na łańcuchach, znanych na pewno osobom mającym wcześniej styczność z takim językiem jak PHP, czy Perl. Bez nich nadal musielibyśmy ?bawić? się standardowymi procedurami takimi jak Copy, Pos, czy Delete.

Funkcja/ProceduraOpis
[[Delphi/Length]]Zwraca ilość znaków znajdujących się w łańcuchu
[[Delphi/Compare]]Funkcja łączy dwa podane w parametrach łańcuchy w jedną całość
[[Delphi/Copy]]Tworzy nową instancję łańcucha (kopiuje jego zawartość)
[[Delphi/Insert]]Wstawia tekst w określone miejsce łańcucha
[[Delphi/Join]]Funkcja łączy kilka elementów w jeden łańcuch
[[Delphi/Remove]]Funkcja umożliwia usunięcie kawałka łańcucha
[[Delphi/Replace]]Zamień część łańcucha
[[Delphi/Split]]Umożliwia rozdzielenie łańcucha na mniejsze fragmenty na podstawie podanego znaku
[[Delphi/SubString]]Wycina część łańcucha
[[Delphi/ToUpper]]Zwraca łańcuch zapisany wielkimi literami
[[Delphi/ToLower]]Zwraca łańcuch zapisany małymi literami
[[Delphi/Trim]]Usuwa wszystkie spacje na początku i na końcu łańcucha
[[Delphi/TrimEnd]]Usuwa wszystkie spacje na początku i na końcu łańcucha
[[Delphi/TrimStart]]Usuwa wszystkie spacje na początku łańcucha

Klasa StringBuilder

Pragnę zwrócić uwagę na jeszcze jeden fakt związany z działaniem łańcuchów w Delphi 8. Mianowicie nadmierne operowanie na typie String, np. dodawanie do niego nowych wartości (przy pomocy operatora +) jest okropnie wolne. Spójrz na listing 4.

Listing 4. Dodawanie wartości do łańcucha

program Listing_4;

{$APPTYPE CONSOLE}

uses
  Windows,
  SysUtils;

var
  Str : String;
  I : Integer;
  Start, Stop : Integer;
begin
  Str := '';
  Start := GetTickCount;

  for I := 0 to 20000 do
    Str := Str + 'Ala ma kota';

  Stop := GetTickCount;
  
  Writeln('Czas wykonywania: ' + IntToStr(Stop - Start));
  Readln;

end.

Program jest dość prosty, napisany tak, aby mógł zostać skompilowany zarówno w Delphi 8 oraz w Delphi 7. Jego zadaniem jest dodawanie w pętli (20,000 iteracji) napisu Ala ma kota. Delphi 7 poradzi sobie z takim programem w 16 sek. natomiast Delphi 8 potrzebuje na to aż 40 sekund! Aby przyspieszyć działanie aplikacji, możesz skorzystać z klasy StringBuilder, która znajduje się w przestrzeni System.Text. Użycie tej klasy spowoduje przyspieszenie działania aplikacji do 15 sekund, czyli do wartości porównywalnej z Delphi 7. Warto o tym wiedzieć, w sytuacjach, gdy Twoja aplikacja silnie korzysta z typu String.

Listing 5. Program napisany z użyciem klasy StringBuilder

program Listing_5;

{$APPTYPE CONSOLE}

uses
  Windows,
  System.Text,
  SysUtils;

var
  Str : StringBuilder;
  I : Integer;
  Start, Stop : Integer;
begin
  Str := StringBuilder.Create;

  Start := GetTickCount;

  for I := 0 to 20000 do
    Str.Append('Ala ma kota');

  Stop := GetTickCount;
  Writeln('Czas wykonywania: ' + IntToStr(Stop - Start));
  Readln;

  Str.Free;

end.

W klasie StringBuilder korzystamy z metody Append, która jest po prostu szybsza niż operator +. W przypadku pracy z większą ilością danych, zalecam więc użycie klasy StringBuilder. Wadą takiego rozwiązania jest to, iż taki program nie będzie kompatybilny ze starszymi wersjami Delphi (klasa StringBuilder jest charakterystyczna dla .NET).

Programy testowane były na komputerze z procesorem AMD 950 Mhz, 384 MB RAM.

Typy liczbowe

Chyba największą zmianą jeżeli chodzi o typy liczbowe w Delphi, to brak 6-bajtowego typu Real48. Typ był dość specyficzny, jego używanie nie było zalecane już w poprzednich wersjach Delphi, teraz został całkowicie zlikwidowany.

Dodatkowo typ Comp, który nie był zalecany do użycia już w poprzednich wersjach Delphi teraz wskazuje na typ Int64. Tak więc próba użycia typu Comp spowoduje pojawieniem się ostrzeżenia kompilatora: Symbol 'Comp' is deprecated.

Typ Extended w Delphi 8 wskazuje na typ System.Double, podobnie jak typ Real.

Destruktory

Jeżeli do tej pory programowałeś w Delphi lub w C++ lub innym języku posiadającym umiejętność wykorzystania obiektów, wiesz jak ważną rolę podczas programowania klas, odgrywały destruktory. Pełniły w zasadzie dwie funkcje: zwalniały zasoby zajmowane przez obiekt (np. nawiązane wcześniej połączenie z bazą danych) oraz zwalniały pamięć zarezerwowaną na poczet obiektu. Użycie destruktorów często było przyczyną pojawiania się błędów typu AccessViolation spowodowanych odwołaniem do elementu klasy tuż po jej zwolnieniu. W .NET jesteśmy zwolnieniu od obowiązku używania destruktorów dzięki mechanizmowi garbage collection; użycie destruktorów nie ma już więc takiego znaczenia jak wcześniej.

Tworząc nową klasę, możesz natknąć się na błąd kompilacji: unsupported language feature "destructor". W Delphi 8 nie możesz tworzyć destruktorów, lecz jedynie dziedziczyć je z klasy bazowej, tak więc destruktor w naszej klasie musi nosić nazwę Destroy oraz być opatrzony klauzulą Override:

type
  TMyClass = class
  prublic
    constructor Create;
    destructor Destroy; override;
end;

Tylko w taki sposób możesz korzystać z destruktorów.

W .NET istnieją jednak inne drogi do kontrolowania zwalniania obiektów. Mam tu na myśli metodę Finalize oraz Dispose.

Podsumujmy: oto warunki jaki musi spełniać destruktor w Delphi 8:
*Destruktor musi nosić nazwę Destroy;
*Destruktor musi posiadać klauzulę Override;
*Destruktor nie może posiadać parametrów;

Finalize

W klasie System.Object, w sekcji chronionej (w Delphi słowo kluczowe protected) zadeklarowana jest metoda Finalize, która odgrywa ważną rolę w procesie zwalniania obiektów. Mechanizm garbage collector wywołuje tę metodę automatycznie tuż przed zwolnieniem obiektu. Możemy więc kontrolować proces zwalniania klasy, deklarując metodę Finalize we własnej klasie:

type
  TFiat = class
    procedure Main;
    procedure Finalize; override;
    constructor Create;
  end;

Również wymogiem jest inicjalizowanie metody Finalize z użyciem słowa kluczowego Override (pamiętaj o tym!). Poniżej prezentuje fragment źródłowy przykładowego modułu:

{ obsługa zdarzenia Click }
procedure TWinForm2.Button1_Click(sender: System.Object; e: System.EventArgs);
var
  Fiat : TFiat;

begin
  Fiat := TFiat.Create;
  Fiat.Main;
end;

{ TFiat }

constructor TFiat.Create;
begin
  inherited Create;
  MessageBox.Show('Inicjalizacja klasy');
end;

procedure TFiat.Finalize;
begin
  MessageBox.Show('Destrukcja klasy');
  inherited;
end;

procedure TFiat.Main;
begin
  { kod }
end;

end.

W klasie TFiat znajduje się konstruktor oraz metoda Finalize. Na formularzu znajduje się jeden przycisk; wygenerowałem jego zdarzenie Click i wpisałem kod mający na celu inicjalizację klasy, ale nie zwolnienie jej! Zwróć uwagę, że nigdzie nie ma kodu powodującego zwolnienie obiektu.

Po uruchomieniu programu i naciśnięciu przycisku, na ekranie wyświetlony zostaje komunikat: Inicjalizacja klasy. Zamknij teraz program. Zobaczysz, że w momencie zamykania programu wyświetlony zostanie komunikat: Destrukcja klasy. Tutaj właśnie zareagował mechanizm garbage collection, który niezależnie od nas ?wychwycił? moment, gdzie obiekt TFiat nie będzie już potrzebny i wywołał metodę Finalize zwalniającą pamięć (a przy okazji wyświetlającą nasz komunikat).

Dispose

Zwalnianiem obiektu, gdy nie jest potrzebny, zajmuje się automatyczny odśmiecacz. Tuż przed przejęciem obiektu przez odśmiecacz, wywoływana jest metoda Finalize (pod warunkiem, iż jest zadeklarowana w obiekcie). Jeżeli nasza klasa otwiera jakiś plik, nawiązuje połączenie z bazą danych, może zajść potrzeba wywołania metody, która zwolni klasę oraz połączenie z bazą danych, przed tym jak to zrobi odśmiecacz.

W .NET służy do tego metoda Dispose, która zawarta jest w interfejsie IDisposable. W Delphi 8 jest to równoznaczne z zadeklarowaniem destruktora:

destructor Destroy; override; 

Ponieważ wiele osób programujących w Delphi, przyzwyczajonych jest do starego zapisu deklaracji destruktora, kompilator dyskretnie zamieni zapis związany z destruktorem na następujący:

type
  TFiat = class(TObject, IDisposable)
  public
    constructor Create;
    procedure Dispose;
  end;

constructor TFiat.Create;
begin
  inherited;
  MessageBox.Show('Inicjalizacja klasy');
end;

procedure TFiat.Dispose;
begin
  MessageBox.Show('Destrukcja klasy!')
end;

Aby skorzystać z metody Dispose należy zaznaczyć, iż nasza klasa TFiat dziedziczy również z interfejsu IDisposable. Wówczas można zadeklarować metodę Dispose, która działa identycznie jak destruktor:

var
  Fiat : TFiat;
begin
  Fiat := TFiat.Create;
  Fiat.Free;
end;

Wywołanie metody Free równa się wywołaniu metody Dispose. Równie dobrze ten kod mógłby wyglądać następująco:

type
  TFiat = class(TObject)
  public
    constructor Create;
    destructor Destroy; override;
  end;

{ ... metody klasy }
destructor TFiat.Destroy;
begin
  MessageBox.Show('Destrukcja klasy!')
  inherited;
end;

Działanie programu będzie identyczne w obu przypadkach.

Komunikaty

Dla przypomnienia: komunikaty to specyficzny element systemu Windows pozwalający na komunikacje pomiędzy aplikacjami. Każde działanie użytkownika w systemie owocuje wysłaniem komunikatu (ang. message) ? przykładowo: ruch myszką, naciśnięcie klawisza. W takich momentach do danego programu przekazywany jest komunikat z informacją ? np. kod ASCII naciśniętego klawisza lub położenie kursora myszy itp.

Komunikaty stanowią bardziej zaawansowaną metodę reakcji na określone zdarzenia. W Delphi prostszym sposobem jest zastosowanie zdarzeń. Poniższa tabela zawiera komunikaty, których znaczenie lub parametry uległy zmianie.

KomunikatOpis
CM_MOUSEENTERParametr `LPARAM `uległ zmianie. Teraz dostarcza on numer kontrolki, w której ?zasięg? wszedł kursor. Wcześniej dostarczał on uchwyt obiektu.
CM_MOUSELEAVEParametr` LPARAM` uległ zmianie. Teraz dostarcza on numer kontrolki w której ?zasięg? wszedł kursor. Wcześniej dostarczał on uchwyt obiektu.
CM_WINDOWHOOKNie używany. Zaleca się użycie metod `HookMainWindow` oraz `UnhookMainWindow` z klasy `TApplication`.
CM_CONTROLLISTCHANGEZastąpiony przez metodę `ControlListChange` z klasy `TWinControl`.
CM_CHANGEDParametr LPARAM został zmieniony. Teraz zawiera on kod obiektu, który został zmieniony.
CM_DOCKCLIENTZastąpiony przez metodę `DockClient `z klasy `TWinControl`.
CM_UNDOCKCLIENTZastąpiony przez metodę `UnDockClient `z klasy `TWinControl`.
CM_FLOATZastąpiony przez metodę `FloatControl `z klasy `TControl`.

0 komentarzy