AdaVirtus

 
  • Increase font size
  • Default font size
  • Decrease font size

Rozdział 6: Bezpieczne tworzenie obiektów

Spis treści

W rozdziale tym omówimy kilka spraw związanych z kontrolowaniem obiektów. Przez "obiekt" rozumiemy tutaj zarówno małe obiekty w sensie prostych stałych i zmiennych podstawowych typów takich, jak Integer, oraz dużych obiektów w sensie programowania zorientowanego obiektowo.

W tym zakresie Ada dostarcza dobre mechanizmy kontroli i elastyczność. Kontrole w większości przypadków są opcjonalne, ale dobry programista będzie korzystał z tych dobrodziejstw gdzie to tylko możliwe, a dobry menedżer będzie się domagał ich wykorzystywania wszędzie tam, gdzie to konieczne.

6.1 Zmienne i stałe

Jak wiemy możemy zadeklarować zmienną lub stałą pisząc:

Top: Integer;                     -- zmienna
Max: constant Integer := 100;     -- stała

Top to zmienna i może mieć przypisywane nowe wartości. Natomiast Max stała, której wartość nie może ulec zmianie. Zwróćmy uwagę, żegdy deklarujemy stałą musimy podać jej wartość, gdyż później nie będziemy mogli tego uczynić. Zmienna zaś, może opcjonalnie mieć podaną wartość początkową.

Zaletą wykorzystania stałych jest to, że nie zmienimy jej wartości przypadkowo. Nie jest to tylko użyteczne zabezpieczenie. Korzystanie ze stałych pomaga osobom czytającym program, informując o ich statusie. Ważną kwestią jest, że wartość stałej nie musi być statyczna - może być ona wyliczana w trakcie działania programu. Przykład był podany w programie liczącym stopy procentowe, gdzie zadeklarowaliśmy stałą nazwaną Factor:

function Nfv_2000 (X: Float) return Float is
  Factor: constant Float := 1.0 + X/100.0;
begin
  return 1000.0 * Factor**2 + 500.0 * Factor – 2000.0;
end Nfv_2000;

Każde wywołanie funkcji Nfv_2000 ma inną wartość X, a tym samym wartość Factor za każdym razem jest inna. Lecz Factor jest stałą w każdym indywidualnym wywołaniu. Mimo, iż jest to banalny przykład i jest jasne, że Factor nie jest zmieniana podczas wykonywania indywidualnego wywołania, winniśmy wpoić sobie nawyk wpisywania w kod programu słowa constant gdzie to tylko możliwe.

Parametry podprogramów są innym przykładem zmiennych i stałych.

Parametry mogą być w trzech trybach: in, in out, out. Jeśli żaden z trybó·w nie jest podany, przez domniemanie przyjmuje się in. Wszystkie parametry funkcją muszą być w trybie in.

Parametr w trybie in jest stałą, której wartość jest podawana jako parametr aktualny. I tak: parametr X funkcji Nfv_2000 jest w trybie in, jest więc stałą. Oznacza to po prostu, że nie możemy do niego przypisywać, co zapewnia niezmienność jego wartości. Parametr aktualny może być dowolnym wyrażeniem danego typu.

Parametry w trybach in out i out są zmiennymi. Parametr aktualny w takim wypadku musi również być zmienną. Różnica tkwi w wartościach początkowych. Parametr w trybie in out jest zmienną o ustalonej wcześniej wartości początkowej podaną jako parametr aktualny. W trybie out zaś, zmienna nie ma wartości początkowej (chyba, że typ posiada takową jak null w przypadku typów dostępowych).

Przykłady wszystkich trzech trybów występują w procedurach Push i Pop w rozdziale Bezpieczna architektura.

procedure Push(S: in out Stack; X: in Float);
procedure Pop(S: in out Stack; X: out Float);

Reguły dotyczące parametrów aktualnych zapewniają, że niezmienność nie zostanie nigdy naruszona. Tym samym nie możemy przekazać do procedury Pop stałej Factor ponieważ odpowiedni parametr jest w trybie out, co pozwaliłoby procedurze Pop zmienić wartość Factor.

Różnice pomiędzy zmiennymi i stałymi mają zastosowanie również w przypadku typów dostępowych i obiektów. Jeśli mamy:

type Int_Ptr is access all Integer;
K: aliased Integer;
KP: Int_Ptr := K'Access;
CKP: constant Int_Ptr := K'Access;

to wartość KP może być zmieniana, zaś CKP - nie. Oznacza to, że CKP zawsze będzie odnosić się do K. Mimo, że nie możemy zmienić odwołania CKP na inne, stała ta może być wykorzystana do zmiany wartości K:

CKP.all := 47;         -- zmiana wartości K na 47

Z drugiej strony można tak:

type Const_Int_Ptr is access constant Integer;
J: aliased Integer;
JP: Const_Int_Ptr := J'Access;
CJP: constant Const_Int_Ptr := J'Access;

gdzie typ dostępowy ma również cechę constant. Oznacza to tyle, że nie możemy zmienić wartości obiektu J pośrednio poprzez odwołanie, obojętne czy użyjemy do tego celu JP czy CJP. Oczywiście wartość J może być zawsze zmieniona poprzez bezpośredne przypisanie.

6.2 Perspektywy stałych i zmiennych

Czasami wygodnie jest zezwolić klientowi na odczyt zmiennej bez możliwości zapisu. Inaczej mówiąc dać klientowi stałą perspektywę zmiennej. Można to uczynić za pomocą tak zwanych stałych odroczonych i już opisanych typów dostępowych.

Stała odroczona jest stałą zadeklarowaną w części widocznej pakietu bez podania wartości początkowej. Ta musi zostać ustalona w części prywatnej. Rozważmy:

package P is
   type Const_Int_Ptr is access constant Integer;
   The_Ptr: constant Const_Int_Ptr;                          -- stała odroczona
 
private
   The_Variable: aliased Integer;
   The_Ptr: constant Const_Int_Ptr := The_Variable'Access;
   ...
end P;

Klient może odczytać wartość The_Variable pośrednio poprzez obiekt The_Ptr typu Const_Int_Ptr pisząc:

K := The_Ptr.all;                    -- pośredni odczyt The_Variable

Lecz ponieważ jest zadeklarowany typ dostępowy Const_Int_Ptr jako access constant, wartość obiektu wskazywanego przez The_Ptr nie może być zmieniona zapisem:

The_Ptr.all := K;       -- operacja niedozwolna, nie można zmienić The_Variable pośrednio

Jednakże dowolny podprogram zadeklarowany w pakiecie P ma bezpośredni dostęp do zmiennej The_Variable, dzięki czemu może zmieniać jej wartość. Technika ta jest szczególnie użyteczna w przypadku tablic, gdzie dana tablica jest przetwarzana dynamicznie, ale nie chcemy by klient miał możliwość zmieniania jej zawartości.

Tak naprawdę nie są wymagane nazwane typy dostępowe od momentu, gdy możemy już zapisywać deklaracje w postaci:

package P is
   The_Ptr: constant access constant Integer;                    -- stała odroczona
 
private
   The_Variable: aliased Integer;
   The_Ptr: constant access constant Integer := The_Variable'Access;
   ...
end P;

Zwróćmy uwagę na podwójne wystąpienie słowa kluczowego constant w deklaracji stałej The_Ptr. Pierwsze mówi, że The_Ptr sama jest stałą. Drugie zaś, że do zmiany wartości obiektu nie można korzystać z pośredniego odwołania.

6.3 Typy ograniczone

Typy, które poznaliśmy do tej pory (Integer, Float, Date, Circle etc.) związane są z różnymi operacjami. Niektóre z nich są predefiniowane jak na przykład równość, operacja do porównywania dwóch wartości (o symbolu =). Niektóre zaś, zdefiniowane są przez programistę (na przykład Area  w przypadku typu Circle). Dla wszystkich wspomnianych do tej pory typów dostępna jest również operacja przypisania.

Czasami przypisanie jest operacją niepożądaną. Są dwa powody, dla których taka sytuacja występuje:

  • typ może reprezentować jakieś zasoby takie, jak np. prawa dostępu i kopiowanie mogłoby oznaczać naruszenie bezpieczeństwa,
  • typ może być zaimplementowany jako struktura połączonych danych i kopiowanie mogloby spowodować proste powielenie początku struktury, a nie wszystkich danych.

Można zapobiec przypisaniu poprzez zadeklarowanie typu jako ograniczony (limited). Dobrym przykładem drugiego z wymienionych problemów jest implementacja stosu przy pomocy listy:

package Linked_Stacks is
   type Stack is limited private;
 
   procedure Clear(S: out Stack);
   procedure Push(S: in out Stack; X: in Float);
   procedure Pop(S: in out Stack; X: out Float);
 
private
   type Cell is
      record
         Next: access Cell;
         Value: Float;
   end record;
 
   type Stack is access all Cell;
end Stacks;
 
package body Stacks is
   procedure Clear(S: out Stack) is
   begin
      S := null;
   end Clear;
 
   procedure Push(S: in out Stack; X: in Float) is
   begin
      S := new Cell'(S, X);
   end Push;
 
   procedure Pop(S: in out Stack; X: out Float) is
   begin
      X := S.Value;
      S := Stack(S.Next);
   end Pop;
end Stacks;

Wykorzystana jest normalna implementacja listy. Zwróćmy uwagę, że typ Stack jest zadeklarowany jako typ prywatny ograniczony, więc przypisanie stosu jak poniżej:

This_One, That_One: Stack;
...
This_One := That_One;        -- niedozwolone, typ Stack jest ograniczony

jest zabronione. Jeżeli operacja przypisania byłaby dozwolona, wszystko co mogłoby się wydarzyć to to, że This_One mógłby ostatecznie wskazywać na początek listy definiującej wartość That_One. Wywołanie procedure Pop na This_One mogłoby po prostu przemieścić go w dół łańcucha reprezentującego That_One. Tego typu problem znany jest jako aliasing - mielibyśmy dwa sposoby odwołań do tego samego bytu, co bardzo często jest nierozsądne.

W przykładzie tym nie ma problemu z deklaracją stosu: jest automatycznie inicjalizowany jako null, co oznacza pusty stos. Jednakże czasami potrzebujemy utworzyć obiekt z określoną wartością początkową (niezbędne w przypadku tworzenia stałej). Nie możemy tego uczynić poprzez przypisanie:

type T is limited ...
...
X: constant T := Y;        -- niedozwolone, nie można kopiować wartości w zmiennej Y

ponieważ to zakłada kopiowanie, które w przypadku typów ograniczonych jest zabronione.

Są dwa sposoby. Jeden uwzględnia wykorzystanie agregatów, drugi - funkcji. Rozważmy wpierw agregaty. Załóżmy, że mamy typ reprezentujący pewien zbiór kluczy ze składowymi podającymi datę wydania klucza oraz numer wewnętrznego kodu:

type Key is limited
   record
      Issued: Date;
      Code: Integer;
   end record;

Typ jest typem ograniczonym - kopiowanie jest zabronione (jest trochę zbyt widoczny, ale do tego wrócimy za chwilę). Mimo to możemy zapisać:

K: Key := (Today, 27);

ponieważ w przypadku typów ograniczonych operacja ta nie kopiuje wartości zdefiniowanej przez agregat, a raczej podaje wartości poszczególnych składowych. Innymi słowy: wartość K jest utworzony in situ (w miejscu).

Byłoby bardziej rzeczywistym utworzenie typu jako prywatny, co oczywiście zaskutkuje niemożnością użycia agregatu, gdyż poszczególne składowe nie są widoczne. Zaradzić temu możemy wykorzystując funkcję tworzącą. Rozważmy:

package Key_Stuff is
   type Key is limited private;
 
   function Make_Key( ... ) return Key;
   ...
private
   type Key is limited
      record
         Issued: Date;
         Code: Integer;
      end record;
end Key_Stuff;
 
package body Key_Stuff is
   function Make_Key( ... ) return Key is
   begin
      return New_Key: Key do
         New_Key.Issued := Today;
         New_Key.Code := ... ;
      end return;
   end Make_Key;
   ...
end Key_Stuff;

Zewnętrzny klient (dla którego typ jest typem prywatnym) może teraz skorzystać:

My_Key: Key := Make_Key( ... );           -- bez udziału kopiowania

przy założeniu, że parametry Make_Key są wykorzystane do wyliczenia wewnętrznego tajnego kodu.

Warto uważnie przeanalizować funkcję Make_Key. Wykorzystuje ona rozszerzoną instrukcję powrotu rozpoczynającą się deklaracją obiektu zwracanego New_Key. Gdy typ wyniku jest ograniczony (jak tutaj), obiekt zwracany jest faktycznie tworzony w miejscu przeznaczenia wyniku wywołania (takim jak My_Key). Jest to podobne zachowanie jak w sposobie, w którym składowe agregatu były faktycznie utworzone in situ. Tak więc, ponownie nie mamy do czynienia z kopiowaniem.

To czysty zysk, że Ada dostarcza sposobu na tworzenie wartości początkowych obiektów deklarowanych przez klienta, a jednocześnie zabezpiecza przed tworzeniem kopii. Mechanizm typów ograniczonych stanowi dostawcę zasobów takich jak przeważająca - w stosunku do wykorzystania - kontrola kluczy.

6.4 Typy kontrolowane

Kolejnym mechanizmem Ady służącym bezpiecznej obsłudze obiektów jest wykorzystanie typów kontrolowanych. Pozwala on na tworzenie specjalnego kodu wykonywanego, gdy:

  • obiekt jest tworzony,
  • przestaje istnieć,
  • jest kopiowany, a nie jest typem ograniczonym.
Mechanizm bazuje na typach zwanych Controlled i Limited_Controlled, zadeklarowanych w predefiniowanym pakiecie:
package Ada.Finalization is
   type Controlled is abstract tagged private;
 
   procedure Initialize(Object: in out Controlled) is null;
   procedure Adjust(Object: in out Controlled) is null;
   procedure Finalize(Object: in out Controlled) is null;
 
   type Limited_Controlled is abstract tagged limited private;
 
   procedure Initialize(Object: in out Limited_Controlled) is null;
   procedure Finalize(Object: in out Limited_Controlled) is null;
 
private
   ...
end Ada.Finalization;   

Kluczową ideą (dla typów nieograniczonych) jest to, że programista deklaruje typ, który jest rozszerzeniem typu Controlled, a następnie dostarcza deklaracji przesłaniających trzech procedur: Initialize, Adjust i Finalize. Procedury te są wywoływane odpowiednio gdy obiekt jest tworzony, kopiowany i gdy przestaje istnieć. Należy pamiętać, że wywołania tych procedur generowane są automatycznie przez system, więc programista nie ma potrzeby ich jawnego wywoływania. Tak samo dzieje się w przypadku typów ograniczonych, będących rozszerzeniem typu Limited_Controlled, z tym, że brak jest procedury Adjust jako, że kopiowanie jest tutaj zabronione. Operacje te zazwyczaj służą do skomplikowanej inicjalizacji, głębokiego kopiowania struktur połączonych, odzyskiwania pamięci po zakończeniu życia przez obiekt oraz do innego gospodarowania bytami specyficznymi dla danego typu.

Dla przykładu przypuśćmy, że rozważamy ponownie problem stosu i decydujemy, że wykorzystamy mechanizm list (w efekcie czego znika problem ograniczenia wielkości stosu), ale chcemy mieć możliwość kopiowania z jednego do drugiego stosu. Możemy zapisać:

package Linked_Stacks is
   type Stack is private;
 
   procedure Clear(S: out Stack);
   procedure Push(S: in out Stack; X: in Float);
   procedure Pop(S: in out Stack; X: out Float);
 
private
   type Cell is
      record
         Next: access Cell;
         Value: Float;
      end record;
 
   type Stack is new Controlled with
      record
         Header: access Cell;
      end record;
 
   overriding
   procedure Adjust(S: in out Stack);
end Linked_Stacks;

Teraz typ Stack jest typem prywatnym. Pełna deklaracja typu uwidacznia, że w rzeczywistości jest to typ znakowany będący rozszerzeniem typu Controlled oraz, że ma składową Header,która faktycznie jest właściwym stosem. Innymi słowy, wprowadziliśmy osłonę (wrapper). Zwróćmy uwagę na to, że użytkownik nie widzi, że typ jest kontrolowany i znakowany. Ponieważ chcemy, aby przypisywanie pracowało jak należy, musimy przesłonić procedurę Adjust. Dzięki znacznikowi przesłaniania kompilator może dwojako sprawdzić, że Adjust rzeczywiście ma prawidłowe parametry.

Treść pakietu mogłaby wyglądać tak:

package body Linked_Stacks is
   procedure Clear(S: out Stack) is
   begin
      S := (Controlled with Header => null);
   end Clear;
 
   procedure Push(S: in out Stack; X: in Float) is
   begin
      S.Header := new Cell'(S.Header, X);
   end Push;
 
   procedure Pop(S: in out Stack; X: out Float) is
   begin
      X := S.Header.Value;
      S.Header := S.Header.Next;
   end Pop;
 
   function Clone(L: access Cell) return access Cell is
   begin
      if L = null then
         return null;
      else
         return new Cell'(Clone(L.Next), L.Value);
      end if;
   end Clone;
 
   procedure Adjust(S: in out Stack) is
   begin
      S.Header := Clone(S.Header);
   end Adjust;
end Linked_Stacks;

Przypisanie będzie teraz działać prawidłowo. Przypuśćmy, że zapiszemy:

This_One, That_One: Stack;
...
This_One := That_One;        -- automatyczne wywołanie Adjust

Nie przetworzone przypisanie That_One to This_One po prostu kopiuje rekord zawierający składową Header. Następnie jest automatycznie wywoływana procedura Adjust z parametrem This_One. Adjust z kolei, wywołuje rekurencyjną funkcję Clone, która faktycznie tworzy kopię. Proces ten często zwany jest kopiowaniem głębokim. Wynik jest taki, że This_One i That_One zawierają te same elementy, lecz w całkowicie rozłącznych strukturach.

Innym godnym uwagi punktem jest to, że procedura Clear ustawia parametr S na rekord, którego składowa Header to null - struktura taka nazywana jest agregatem rozszerzenia. Pierwsza część takiego agregatu podaje nazwę typu bazowego (lub wartość obiektu tego typu), a część po słowie kluczowym with podaje wartości dodatkowych składowych (jeśli takowe są). Procedury Pop i Push nie wymagają omówienia.

Czytający może zastanawiać się nad odzyskiwaniem nieużytków powstających po wywołaniu Pop, skutkującym usunięciem elementu ze stosu oraz po wywołaniu Clear, opróżniającym stos. Omówimy ten problem w następnym rozdziale, gdy będziemy rozważać zagadnienia związane z zarządzaniem pamięcią.

Zauważmy jeszcze, że Initialize i Finalize nie zostały przesłonięte, co skutkuje dziedziczeniem procedurami null typu Controlled. Tak więc nic specjalnego się nie dzieje gdy stos jest deklarowany: jest to prawidłowe, gdyż otrzymujemy rekord, którego składowa Header ma jako wartość domyślną null, a nic więcej nie jest potrzebne. Nic specjalnego się nie dzieje również, gdy obiekt typu Stack przestaje istnieć. Ponownie wraca kwestia odzyskiwania nieużytków pamięci- zapraszamy do następnego rozdziału.

Rozdział 5: Bezpieczne programowanie obiektowe Rozdział 7: Bezpieczne zarządzanie pamięcią
Tekst oryginalny w języku angielskim - pdf2
Zmieniony: Środa, 24 Marzec 2010 12:41  

Dodaj swój komentarz

Imię:
Temat:
Komentarz: