AdaVirtus

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

Rozdział 4: Bezpieczna architektura

Spis treści

Dobra architektura (rozumiana jako budownictwo) jest jedyną, która zapewnia wymaganą wytrzymałość w naturalny i nie rzucający się w oczy sposób, a tym samym dostarcza bezpieczne środowisko dla korzystających osób. Eleganckim przykładem jest rzymski Panteon, którego sferyczny kształt daje olbrzymią wytrzymałość i zapewnia uporządkowaną przestrzeń. Wiele starożytnych katedr nie jest zbyt udanych, przez co wymagają podpór mocowanych na zewnątrz celem wzmocnienia ścian. W roku 1624 sir Henry Wooton podsumował ten temat w swojej książce The Elements of Architecture pisząc: "Dobre budowle spełniają trzy warunki: użytkowość, stabilność i wywołanie zachwytu". Innymi słowy, dobra budowla powinna pracować, być silna i piękna.

Dobra architektura w programie podobnie powinna zapewniać dyskretne bezpieczeństwo konkretnym działaniom wewnętrznym elementom wewnątrz przejrzystego szkieletu. Powinna zezwalać na współdziałanie tam, gdzie to konieczne oraz winna zapobiegać akcjom na niepożądane ingerencje w inne działania. Dobry język, natomiast, powinien umożliwić pisanie programów o dobrej architekturze.

Mamy tutaj być może analogię z architekturą pomieszczeń biurowych. Aranżacja wnętrza, w którym każdy ma oddzielne biuro może uniemożliwiać komunikację i przepływ informacji. Z drugiej strony, biuro o otwartej przestrzeni często powoduje problemy, gdyż hałas i inne zakłócenia ujemnie wpływają na wydajność.

Struktura programu w Adzie oparta jest w pierwszym rzędzie na koncepcji pakietów z grupami powiązanych ze sobą bytów oraz dostarcza naturalne konstrukcje do ukrywania przed klientami szczegółów implementacyjnych.

4.1 Specyfikacje pakietów i treści

Wczesne języki programowania miały strukturę płaską; każdy element był dokładnie na tym samym poziomie. W konsekwencji wszystkie dane (w odróżnieniu od od danych lokalnych podprogramów) były widzialne w każdym miejscu programu. To wskazuje na podobieństwo do biur o otwartej przestrzeni. Ta sama płaska struktura pojawia się również w języku C, aczkolwiek C dostarcza pewien stopień enkapsulacji, umożliwiający programiście kontrolę nad zewnętrzną widzialnością funkcji oraz zmiennych o zasięgu pliku.

Inne języki (Algol, Pascal) mają prostą strukturę blokową, podobną do rosyjskich lalek typu Wańka-wstańka. To czyni te języki trochę lepszymi, lecz wygląda to raczej na biuro o otwartej przestrzeni podzielone na mniejsze tego samego typu. Ciągle istnieją duże problemy komunikacyjne.

Rozważmy prosty problem stosu liczb. Chodzi nam o taki protokół, w którym element dodajemy do stosu wywołaniem procedury Push, a element ze szczytu stosu zdejmujemy wywołaniem procedury Pop. Zakładamy również istnienie procedure Clear, czyniącą stos pustym. Nie przewidujemy innego sposobu manipulowania stosem, ponieważ chcemy, aby protokół ten był niezależny od sposobu implementacji.

Na początek rozważmy poniższą implementację stosu zrealizowaną w Pascalu. Stos jest reprezentowany przy pomocy tabeli liczb rzeczywistych. Mamy tutaj trzy operacje: Push umieszczająca element na stosie, Pop zdejmująca element ze szczytu stosu, oraz Clear czyniąca stos pustym. Deklarujemy również stałą max, przypisując jej wartość 100. Dzięki temu unikniemy zapisu 100 w kolejnych miejscach. Sposób ten ma również minus, objawiący się w momencie, gdy wskutek naszych zamierzeń zajdzie konieczność zmiany wymaganej wielkości stosu.

const max = 100;
var   top : 0 .. max;
      a   : array[1..max] of real;
 
procedure Clear;
begin
  top := 0
end;
 
procedure Push(x : real);
begin
  top := top + 1;
  a[top] := x
end;
 
function Pop : real;
begin
  top := top – 1;
  Pop:= a[top + 1]
end

Głównym problemem z taką wersją implementacji jest to, że max, top i a muszą być zadeklarowane poza Push, Pop i Clear, z którego to powodu mogą być dostępne. Dodatkowo z dowolnego miejsca w programie, zktórego możemy wywołać Push, Pop i Clear możemy również bezpośrednio zmienić a i top, a więc omijamy protokół, a stos staje się niespójny.

Taka sytuacja jest źródłem niebezpieczeństwa. Jeśli chcemy monitorować jak wiele razy zmieniany jest stos, dodanie instrukcji monitorujących zliczających wywołania Push, Pop i Clear nie jest wystarczające. Podobnie, jeżeli będziemy przeglądać duży program i szukać miejsc, gdzie jest zmieniany stos, musimy prześledzić wszystkie odwołania do top i a oraz wywołania Push, Pop i Clear.

Jest to problem występujący w C, Fortranie oraz Pascalu. Języki te w pewnym stopniu przezwyciężają to ograniczenie poprzez dodanie pewnych form możliwości oddzielnej kompilacji. Byty, które mają być widzialne w innych,oddzielnie kompilowanych jednostkach mogą być oznaczone specjalną instrukcją np. extern lub poprzez użycie pliku nagłówkowego. Jednakże, oddzielna kompilacja z natury jest sama w sobie płaska i niestrukturalna. Dodatkowo, kontrola typów w tych językach jest słabsza w przypadku wielu jednostek kompilacji niż w przypadku jednego pliku.

Technika stosowana w Adzie polega na wykorzystaniu pakietu do enkapsulacji i ukrycia danych współdzielonych przez Push, Pop i Clear, dzięki czemu do danych tych mają dostęp tylko wymienione podprogramy. Pakiet składa się z dwóch elementów: specyfikacji, która opisuje interfejs do innych jednostek, oraz treść, która opisuje implementację interfejsu. Parafrazując, możemy stwierdzić, że specyfikacja mówi nam co pakiet robi, natomiast treść wyjaśnia jak. Specyfikacja mogłaby wyglądać tak:

package Stack is
  procedure Clear;
  procedure Push(X: Float);
  function Pop return Float;
end Stack;

Właśnie ona opisuje interfejs do świata zewnętrznego. Tak więc na zewnątrz pakietu wszystko co jest dostępne to właśnie trzy podprogramy. Specyfikacja zapewnia wystarczającą ilość informacji dla klienta z zewnątrz, by mógł on wywoływać podprogramy oraz dla kompilatora, celem kompilacji wywołań. Treść pakietu mogłaby zaś wyglądać tak:

package body Stack is
 
  Max: constant := 100;
  Top: Integer range 0 .. Max := 0;
  A: array (1 .. Max) of Float;
 
  procedure Clear is
  begin
    Top := 0;
  end Clear;
 
  procedure Push(X: Float) is
  begin
    Top := Top + 1;
    A(Top) := X;
  end Push;
 
  function Pop return Float is
  begin
    Top := Top – 1;
    return A(Top + 1);
  end Pop;
 
end Stack;

Treść podaje wszystkie szczegóły podprogramów oraz deklaruje ukryte obiekty: Max, Top i A. Zwróćmy uwagę na wartość początkową Top ustaloną na zero.

By klient mógł skorzystać z bytów zadeklarowanych w pakiecie jego kod musi wymienić ten pakiet przy pomocy klauzuli with. Tak więc mamy:

with Stack;
  procedure Some_Client is
    F: Float;
  begin
    Stack.Clear;
    Stack.Push(37.4);
    ...
    F := Stack.Pop;
    ...
    Stack.Top := 5;      -- niedozwolone!
end Some_Client;

Teraz wiemy, że wymagany protokół został narzucony. Klient nie może przypadkowo lub celowo kolidować z wewnętrznymi działaniami stosu. Zwróćmy uwagę, że bezpośrednie przypisanie do Stack.Top jest zabronione ponieważ zmienna Top nie jest widoczna dla klienta (nie jest ona wymieniona w specyfikacji stosu).

Przyglądając się dokładnie widzimy trzy byty do rozważenia: specyfikację pakietu, jego treść oraz oczywiście klienta.

Istnieją ważne zasady odnoszące się do kompilacji powyższych bytów. Klient nie może być skompilowany bez dostępnej specyfikacji. To samo dotyczy kompilacji treści. Ale za to nie ma podobnych ograniczeń odnoszących się do klienta i treści. Jeśli zdecydujemy zmienić jakiś szczegół implementacji nie wymagający zmian w specyifkacji, to klient nie musi być ponownie kompilowany.

Pakiety i podprogramy na najwyższym poziomie (dokładnie: nie będące zagnieżdżone w innych pakietach czy podprogramach) zawsze mogą być (i zazwyczaj są) oddzielnie kompilowane. Często nazywane są jednostkami bibliotecznymi i mówi się o nich, że są na poziomie biblioteki.

Zauważmy, że pakiet Stack jest wymieniany za każdym razem, gdy wykorzystywany jest którykolwiek z jego bytów. To zapewnia przejrzystość kodu klienta. Czasami jednak ciągłe powtarzanie nazwy pakietu jest uciążliwe. W takiej sytuacji wystarczy dodać klazulę use:

with Stack; use Stack;
procedure Client is
begin
  Clear;
  Push(37.4);
  ...
end Client;

Oczywiście, jeśli będziemy mieli dwa pakiety Stack1 i Stack2, w których obydwu zadeklarujemy procedurę zwaną Clear, a następnie spróbujemy użyć klauzul with i use, kod stanie się niejednoznaczny, co zasygnalizuje kompilator. W takim wypadku rozwiązaniem jest jawne podanie żądanej nazwy pakietu., na przykład Stack2.Clear.

Konkludując, specyfikacja definiuje kontrakt pomiędzy klientem a pakietem. Treść zapewnia implementację specyifkacji, a klient zapewnia, że wykorzysta pakiet w sposób opisany w specyfikacji. W końcu kompilator upewnia się, że obie strony dotrzymują umowy. Wrócimy do tych zagdanień w ostatnim rozdziale, gdy spojrzymy na pomysły kryjące się w narzędziach Spark.

"Gwoździem programu" jeśli chodzi o Adę jest to, że silna kontrola typów jest narzucona na wskroś granic jednostek kompilacyjnych. Przeprowadzane są dokładnie te same kontrole niezależnie od tego czy kompilowana jest jedna, czy też wiele jednostek kompilacyjnych w różnych plikach.

4.2 Typy prywatne

Inną cechą pakietu jest część specyfikacji, która może być ukryta przed klientem. Wykorzystana jest do tego celu tak zwana część prywatna. Powyższy pakiet Stack implementuje tylko pojedynczy stos. Byłoby bardzo użytecznym zadeklarować pakiet, który umożliwi nam deklarację wielu stosów. W tym celu wprowadzimy koncepcję typu stosu.

Można zapisać:

package Stacks is                                 -- część widzialna
  type Stack is private;                          -- typ prywatny
  procedure Clear(S: out Stack);
  procedure Push(S: in out Stack; X: in Float);
  procedure Pop(S: in out Stack; X: out Float);
 
private                                           -- część prywatna
  Max: constant := 100;
  type Vector is array (1 .. Max) of Float;
  type Stack is                                   -- pełny typ
    record
      A: Vector;
      Top: Integer range 0 .. Max := 0;
    end record;
end Stacks;

Treść mogłaby wyglądać następująco:

package body Stacks is
 
  procedure Clear(S: out Stack) is
  begin
    S.Top := 0;
  end Clear;
 
  procedure Push(S: in out Stack; X: in Float) is
  begin
    S.Top := S.Top + 1;
    S.A(Top) := X;
  end Push;
 
-- procedure Pop similarly
 
end Stacks;
 

Użytkownik może teraz zadeklarować wiele stosów i działać na nich niezależnie:

with Stacks; use Stacks;
procedure Main is
  This_One: Stack;
  That_One: Stack;
begin
  Clear(This_One); Clear(That_One);
  Push(This_One, 37.4);
  ...
 

Szczegóły dotyczące typu Stack są podane w części prywatnej pakietu i - mimo, że widoczne dla czytającego - nie są bezpośrednio dostępne dla kodu klienta. Tak więc, specyfikacja jest logicznie podzielona na dwie części: widzialną (wszystko do słowa kluczowego private) i prywatną.

Jeśli zostanie zmieniona tylko część prywatna pakietu, to treść klienta nie musi być zmieniana, ale za to musi zostać ponownie skompilowana ponieważ kod obiektu mógł zostać zmieniony nawet jeśli kod źródłowy nie.

Jakiekolwiek niezbędne ponowne kompilacje są zapewnione przez system kompilatora i mogą być dokonywane automatycznie, jeśli zajdzie taka potrzeba. Zwróćmy szczególną uwagę na fakt, iż cecha ta jest wymagana przez język Ada i nie jest tylko po prostu własnością konkretnej implementacji. Decyzja o ponownej kompilacji nigdy nie jest w rękach użytkownika, dzięki czemu nie zachodzi ryzyko konsolidacji zbioru niespójnych jednostek - duże niebezpieczeństwo w przypadku języków, w których nie są precyzyjnie określone zależności pomiędzy kompilacją, procesem wiązania i konsolidacją.

Na koniec zwróćmy uwagę na tryby parametrów: in, out oraz in out. Dotyczą one przepływu informacji, a omówione są w rozdziale Bezpieczne tworzenie obiektów.

4.3 Rodzajowy model kontraktowy

Szablony są ważną cechą języków takich jak C++ (teraz również Java). Odpowiadają one jednostkom rodzajowym Ady. W rzeczywistości szablony C++ bazują częściowo na koncepcji jednostek rodzajowych Ady. W przypadku Ady ze względu na zastosowany model kontraktowy uzyskujemy jednostki rodzajowe bezpieczne pod względem typów.

Możemy rozszerzyć nasz przykład stosu, aby umożliwiał deklarować stosy dowolnego typu i wielkości. Rozważmy:

generic
  Max: Integer;                               -- formalne parametry rodzajowe
  type Item is private;
package Generic_Stacks is
  type Stack is private;
  procedure Clear(S: out Stack);
  procedure Push(S: in out Stack; X: in Item);
  procedure Pop(S: in out Stack; X: out Item);
 
private                                       -- część prywatna
  type Vector is array (1 .. Max) of Item;
  type Stack is
    record
      A: Vector;
      Top: Integer range 0 .. Max := 0;
    end record;
end Generic_Stacks;

razem z treścią uzyskaną po prostu przez zamianę Float na Item.

Pakiet rodzajowy jest właściwie szablonem i aby mógł on być wykorzystany w programie wpierw musi być skonkretyzowany z odpowiednimi parametrami aktualnymi odpowiadającymi dwóm rodzajowym parametrom formalnym Max i Item. Wynikiem konkretyzacji pakietu rodzajowego jest deklaracja pakietu aktualnego. Na przykład, jeśli chcemy mieć stos liczb całkowitych o maksymalnej wielkości 50, zapiszemy:

package Integer_Stacks is
  new Generic_Stacks(Max => 50, Item => Integer);
 

Powyższe deklaruje pakiet zwany Integer_Stacks, który może być następnie wykorzystany w tradycyjny sposób. Istotą modelu kontraktowego jest to, że jeśli dostarczymy parametrów, które będą zgodne ze specyfikacją rodzajową, to pakiet uzyskany w wyniku konkretyzacji zostanie skompilowany i wykonany prawidłowo.

Inne języki programowania nie posiadają tej pożądanej cechy. W języku C++, na przykład, pewne niezgodności są wychwytywane w trakcie konsolidacji, a nie kompilacji. W innych - nawet w trakcie wykonywania programu, co skutkuje zgłoszeniem wyjątku.

W Adzie mamy szeroki wachlarz form parametrów rodzajowych. Pisząc:

type Item is private;
 

zezwalamy, by typem aktualnym był najczęściej dowolny typ. Pisząc zaś:

type Item is ();

zezwalamy, by typem aktualnym był typ całkowity (Integer lub Long_Integer) lub typ wyliczniowy (taki jak Signal). Wewnątrz jednostki rodzajowej możemy następnie wykorzystać wszystkie własności wspólne dla typów całkowitych i typów wyliczeniowych z pewnikiem, że typ aktualny rzeczywiście udostępnia takie własności.

Rodzajowy model kontraktowy jest bardzo ważny. Umożliwia rozwój elastycznych, ale bezpiecznych bibliotek ogólnego przeznaczenia. Ważną przesłanką jest, by użytkownik Ady nigdy nie będzie musiał ślęczeć nad kodem jednostki rodzajowej celem odgadnięcia co poszło nie tak.

4.4 Jednostki potomne

System adowy może mieć hierarchiczną (podobną do drzewa) strukturę jednostek, który umożliwia elastyczne ukrywanie informacji oraz łatwość modyfikacji. Jednostki potomne mogą być publiczne oraz prywatne. Mając pakiet nazwany Parent możemy zadeklarować publiczny pakiet potomny:

package Parent.Child is ...

oraz prywatny pakiet potomny:

private package Parent.Slave ...

Oba pakiety mają treść i mogą, jak zwykle, mieć część prywatną. Zasadniczą różnicą jest to, że publiczny pakiet potomny zasadniczo rozszerza specyfikację pakietu macierzystego (rozszerzenie to jest widoczne dla klienta), podczas gdy prywatny pakiet potomny rozszerza prywatną część i treść pakietu macierzystego (które to rozszerzenie z kolei jest niewidoczne dla klienta). Struktura umożliwia tworzenie kolejnych poziomów "potomstwa".

Mamy do czynienia z różnymi zasadami dotyczącymi widzialności. Pakiety potomne nie muszą jawnie używać klauzuli with odnośnie swych pakietów macierzystych (widzialność jest automatyczna). Jednakże, treść pakietu macierzystego może wykorzystać tę klauzulę odnoszącą się do jego pakietu potomnego, jeśli jest wymagana jakaś funkcjonalność zdefiniowana w tym pakiecie. Ale, ponieważ specyfikacja pakietu macierzystego musi być dostępna przed kompilacją pakietów potomnych (ponieważ pakiety owe współdzielą nazwę rodzica), specyfikacja ta nie może zawierać normalnej klauzuli with odnoszącej się do pakietu potomnego. Więcej na ten temat później.

Inną regułą jest to, że widzialna część prywatnego pakietu potomnego ma dostęp do części prywatnej rodzica (dokładnie tak samo jak w przypadku treści rodzica). Ale już w przypadku publicznego pakietu potomnego tylko jego część prywatna oraz jego treść (a nie część widzialna) ma taki dostęp do pakietu macierzystego.

Specjalna forma klauzulu with (klauzula private with) jest dopuszczalna ze specyfikacją pakietu. Pozwala ona prywatnej części na dostęp do interesującej go jednostki. Jest to użyteczne na przykład wtedy, gdy prywatna część publicznego pakietu potomnego potrzebuje informacji dostarczanej prze prywatny pakiet potomny. Tak oto możemy mieć pakiet aplikacji App oraz dwa pakiety potomne: App.User_View oraz App.Secret_Details:

private package App.Secret_Details is
  type Inner is ...
  ...                    -- różne działania na Inner etc.
end App.Secret_Details;
 
private with App.Secret_Details;
package App.User_View is
 
  type Outer is private;
  ...                    -- różne działania na Outer widoczne dla użytkownika
 
                         -- typ Inner jest tutaj niewidoczny
private
                         -- typ Inner tutaj jest widoczny
 
  type Outer is
    record
      X: Secret_Details.Inner;
      ...
    end record;
 
  ...
end App.User_View;
 

Normalne użycie klauzuli with w stosunku do pakietu Secret_Details nie jest dozwolone w User_View, gdyż to mogłoby zezwolić klientowi na wgląd w informacje w pakiecie Secret_Details poprzez widzialną część User_View. Ada skutecznie blokuje wszelkie próby omijania wymagającej kontroli widzialności.

3.5 Testowanie jednostek

Jednym z problemów, przed którymi staje testowanie kodu jest pewność, że testowanie nie wprowadza zamieszania w testowanym programie. Jest tutaj analogia z mechaniką kwantową, wedle której sama obserwacja cząstki (takiej na przykład jak elektron) powoduje zmianę stanu owej cząstki.

Jednym z problemów w procesie projektowania dobrego oprogramowania jest to, że usiłujemy ukryć szczegółowe informacje w celu stworzenia dobrej abstrakcji, na przykład poprzez wykorzystanie typów prywatnych. Lecz w momencie testowania systemu często chcielibyśmy dokładnie obserwować zachowanie takiego ukrytego materiału.

Weźmy banalny przykład: chcemy znać wartość Top dla konkretnego stosu zadeklarowanego z wykorzystaniem pakietu Stacks (chodzi o ten, w którym Stack jest typu prywatnego). Jednak nie udostępniliśmy takiego mechanizmu. Można by dodać do Stacks funkcję Size, lecz może to wprowadzić zamieszanie w pakiecie i wymagać ponownej kompilacji, a w następstwie całego kodu klienta. Również moglibyśmy popełnić błędy w testowanym pakiecie lub - co gorsza - popełnić je, gdy później usuwalibyśmy kod testujący.

Jednostki potomne umożliwiają w praktyczny i wygodny sposób ominąć te niedogodności. Możemy zapisać:

package Stacks.Monitor is
   function Size(S: Stack) return Integer;
end Stacks.Monitor;
 
package body Stacks.Monitor is
   function Size(S: Stack) return Integer is
   begin
      return S.Top;
   end Size;
end Stacks.Monitor;

I to działa, ponieważ treść jednostki potomnej ma dostęp do części prywatnej swej jednostki macierzystej. Tak więc możemy wywołać funkcję Size w momencie testowania, po czym, gdy jesteśmy już usatysfakcjonowani poprawnością programu, możemy usunąć pakiet potomny, a pakiet macierzysty Stacks nie będzie musiał być wcale zmieniany.

3.6 Typy wzajemnie zależne

Wiele języków posiada odpowiedniki typów prywatnych, szczególnie w połączeniu z mechanizmami obiektowymi. Ogólnie rzecz biorąc, operacje pierwotne (metody) przyneleżne do typu są deklarowane w pakiecie (czy klasie) razem z danym typem. Tak więc operacjami pierwotnymi typu Stack są: Clear, Push i Pop. Taka sama struktura w C++ wyglądałaby mniej więcej tak:

class Stack {
   ...                      /* szczegóły struktury stosu */
public:
   void Clear();
   void Push(float);
   float Pop();
};
 

Metoda C++ jest wygodna w tym, że ma tylko jeden poziom nazewnictwa: Stack. W Adzie natomiast, mamy zarówno nazwę pakietu, jak i nazwę typu, czyli Stacks.Stack. Jednakże w praktyce styl Ady nie jest jakimś obciążeniem, szczególnie, że mamy możliwość skorzystać z klauzuli use. Dodatkowo użytkownik Ady ma możliwość wyboru sposobu nazywania poprzez nadanie typowi jakiejś neutralnej nazwy jak Data czy Object, dzięki czemu można później stosować nazwy Stacks.Data czy Stacks.Object.

Teraz z innej beczki: jeśli mamy dwa typy, które chcą współdzielić prywatne informacje, w Adzie łatwo można to zrealizować:

package Twins is
   type Dum is private;
   type Dee is private;
   ...
private
   ...                            -- współdzielona część prywatna
end Twins;

Część prywatna definiuje zarówno Dum jak i Dee, dzięki czemu mają one wzajemny dostęp do czegokolwiek w części prywatnej.

W innych językach nie jest tak prosto, co wymaga konstrukcji takich, jak wielokrotnie omawiany mechanizm friend w C++. W Adzie nie ma możliwości złego dostępu czy naruszenia prywatności w nieoczekiwany sposób. Mechanizm jest symetryczny.

Inne przykłady obrazują wzajemną rekursję. Przypuśćmy, że chcemy zanalizować wzorce punktów i linii, przy czym przez każdy punkt biegną trzy linie, a każda linia zawiera w sobie trzy punkty. (To nie jest przypadkowy przykład. Dwa z najważniejszych twierdzeń geometrii rzutowej. Strukturami takimi interesowali się Pappus i Desargues). Wykorzystamy typy dostępowe. Prostym podejściem jest jeden pakiet:

package Points_and_Lines is
   type Point is private;
   type Line is private;
   ...
private
   type Point is
      record
         L, M, N: access Line;
      end record;
   type Line is
      record
         P, Q, R: access Point;
      end record;
end Points_and_Lines;
 

Jeśli zdecydujemy, że każdy typ zajmuje osobny pakiet, to moglibyśmy nadal zdefiniować wzajemną strukturę rekursyjną z wykorzystaniem klauzuli limited with. (Dwa pakiety nie mogą korzystać z klauzuli with odnoszącej się do każdego z nich, gdyż tworzy to zamknięte koło czyniące inicjalizację niemożliwą). Możemy zapisać:

limited with Lines;
package Points is
   type Point is private;
   ...
private
   type Point is
      record
         L, N, N: access Lines.Line;
      end record;
end Points;
 

I podobnie czynimy dla pakietu Lines. Klauzula limited with daje tzw. niekompletny widok typów w danym pakiecie, co oznacza z grubsza, że mogą być one wykorzystane tylko w formie typów dostępowych.

Rozdział 3: Bezpieczne wskaźniki Rozdział 5: Bezpieczne programowanie obiektowe
Tekst oryginalny w języku angielskim - pdf2
Zmieniony: Czwartek, 25 Marzec 2010 11:04  

Dodaj swój komentarz

Imię:
Temat:
Komentarz: