AdaVirtus

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

Rozdział 3: Bezpieczne wskaźniki

I tutaj mam problem (tłumaczący). W czym rzecz? Otóż w literaturze polskiej — skąpej co prawda — na temat języka Ada odnośnie tematu poruszanego w tym rozdziale została przyjęta nomenklatura pochodząca z języka C. Ja jednak uważam, że twórcy Ady nie bez kozery podkreślili odmienność podejścia języka do poruszanego zagadnienia definiując nowe nazewnictwo odnoszące się do tzw. wskaźników. Stąd moja wersja tłumaczenia owego będzie swoistym manifestem, aby nie umniejszać zalet Ady, lecz raczej podkreślać jej odmienność, in plus oczywiscie! Zrównując — nawet tylko poprzez nazwę typu — oba języki czynimy rzecz równie śmieszną, jak próba zrównania poziomu artystycznego "gry" np. tzw. "mroktorów" (tu język C) z tym co "wyprawiał" na scenie Ś.P. Gustaw Holoubek (tutaj oczywiście Ada). Bo czymże jest pojęcie wskaźnika pochodzącego z języka C w porównaniu z tym co oferują typy dostępowe Ady?


Spis treści

Człowiek pierwotny odkrywając ogień uczynił wielki krok na drodze swego rozwoju. Odkrycie to pozwoliło nie tylko ogrzać się, przygotować gorącą strawę, czy zdobywać coraz wiecej terenów. Ogień umożliwił powstanie narzędzi metalowych, co w konsekwencji przyczyniło się do powstania społeczeństw uprzemysłowionych. Lecz ogień jest również bardzo niebezpieczny, jeśli używać go nierozważnie. Może spowodować wiele nieszczęść i strat. Nie bez przyczyny w każdym prawie społeczeństwie mamy służby do walki z ogniem, który wymknął się spod kontroli.

Wraz z wprowadzeniem pojęcia wskaźników lub referencji oprogramowanie również uczyniło wielki krok naprzód w zakresie swoich możliwości. Lecz zabawa ze wskaźnikami jest jak zabawa z ogniem. Wykorzystanie wskaźników niesie ze sobą ogromne korzyści, lecz niewłaściwe ich używanie może spowodować konkretne nieszczęścia typu błękitny ekran na przykład... Niewłaściwe ich użycie może przyczynić się również do tego, że program zniszczy swoje (i nie tylko swoje) dane, czy też do otwarcia furtki dla różnego rodzaju wirusów.

Oprogramowanie wysokiej integralności zazwyczaj drastycznie ogranicza wykorzystanie wskaźników. Typy dostępowe Ady mają semantykę wskaźników, lecz dodatkowo zaopatrzone są w wiele zabezpieczeń przed niewłaściwym ich użyciem, co czyni je dopuszczalnymi w przypadku wszystkich, nawet najbardziej wymagających programów typu safety-critical.

3.1 Referencje, wskaźniki i adresy

Wskaźniki wprowadzają wiele okazji do popełniania błędów.

  • Naruszenie bezpieczeństwa typów — utworzenie obiektu danego typu, a następnie próba dostępu do niego (poprzez wskaźnik) w taki sposób, jakby był innego typu. Lub też wykorzystanie wskaźnika do dostępu do obiektu w sposób, który jest niespójny z własnościami semantyki obiektu (na przykład, przypisanie do stałej lub naruszenie zakresu zawężenia).
  • Wiszące odwołania — dostęp do obiektu poprzez wskaźnik po wcześniejszym zwolnieniu tego obiektu. Albo zmienna lokalna, która stała się niedostępna (znalazła się poza zasięgiem), czy też dynamicznie alokowany obiekt, który został jawnie zwolniony przy pomocy innego wskaźnika.
  • Ubytki pamięci — alokowanie obiektu, który później staje się niedostępny (przyjmuje status „śmiecia”), a nigdy nie zostaje zwolniony.

Chociaż detale są różne, naruszenia bezpieczeństwa typów i wiszące odwołania pojawiają się w równym stopniu, jeśli tylko język zezwala na używanie wskaźników do podprogramów.

Historycznie rzecz biorąc, języki mają różne podejścia do tych problemów. Wczesne języki takie jak Fortran, COBOL czy Algol 60 "nie miały pomysłu" na wskaźniki na poziomie programu użytkownika. Programy tworzone we wszystkich tych językach używają adresów do podstawowych działań takich, jak wywołanie podprogramu, lecz w żadnym z nich adresów tych nie można przetwarzać bezpośrednio.

Język C (oraz C++) dopuszcza wskaźniki na obiekty alokowane zarówno na stercie, jak i deklarowane (alokowane na stosie). Chociaż języki te oferują pewne kontrole, cała odpowiedzialność za prawidłowe korzystanie ze wskaźników spada na programistę.

Na przykład: ponieważ C traktuje tablicę jako wskaźnik do jej pierwszego elementu i pozwala stosować działania arytmetyczne na wskaźnikach jako ekwiwalentu indeksowania tej tablicy, dostępne są wszystkie niezbędne składniki niskiego poziomu, co może wywołać niemałe kłopoty.

Java i inne „czyste” języki obiektowe nie ujawniają wskaźników w aplikacji, lecz są uzależnione od nich i dynamicznej alokacji, jako podstawowej semantyki języka. Kontrola typów jest zapewniona, wiszące odwołania niedopuszczalne (brak jawnego zwalniania pamięci), lecz w celu uniknięcia ubytków pamięci język wymaga implementacji mechanizmu odzyskiwania nieużytków (garbage collection). Jest to uzasadnione podejście w przypadku niektórych rodzajów aplikacji. Ale technologia ta pozostawia wiele do życzenia w przypadku aplikacji czasu rzeczywistego, szczególnie w przypadku systemów safety-critical i security-critical.

Historia Ady związana ze wskaźnikami jest dosyć interesująca. Wersja oryginalna języka, Ada 83, udostępniała tylko wskaźniki na obiekty dynamicznie alokowane (co skutkowało brakiem wskaźników na obiekty deklarowane czy podprogramy) oraz umożliwiała jawną operację zwalniania pamięci znaną jako Unchecked_Deallocation. To podejście zapewniało bezpieczeństwo typów oraz nie dopuszczało do wiszących odwołań spowodowanych przez wskaźniki na zmienne lokalne, które znalazły się poza zasięgiem, lecz wprowadzało możliwość wiszących odwołań poprzez niewłaściwe użycie Unchecked_Deallocation.

Decyzja o włączeniu Unchecked_Deallocation była nieunikniona, ponieważ jedyna alterantywa — wymaganie implementacji odzyskiwania nieużytków — nie wchodziła w grę w przypadku aplikacji czasu rzeczywistego i systemów wysokiej integralności (które stanowią podstawową dziedzinę zastosowań Ady). Jednakże filozofią Ady jest to, że jeśli dana cecha blokuje zwyczajowo wykonywane kontrole, musi być ona użyta jawnie. I rzeczywiście. Jeśli mamy wykorzystać Unchecked_Deallocation, musimy włączyć ten pakiet przy pomocy klauzuli with, a następnie skonkretyzować procedurę rodzajową (koncepcje związane z włączaniem pakietów oraz konkretyzacja omówione są w następnym rozdziale). Ta nieco przyciężka składnia zapobiega przypadkowym użyciom, jak również czyni nasze zamiary bardziej czytelnymi dla każdego, kto analizuje nasz kod.

Ada 95 rozszerza mechanizm Ady 83 zezwalając na wskaźniki na obiekty zadeklarowane oraz wskaźniki do podprogramów. Ada 2005 idzie jeszcze dalej — na przykład ułatwia przekazywanie podprogramów (oczywiście przy pomocy wskaźników) jako parametrów systemu wykonawczego. Jak to czynić bez uszczerbku na bezpieczeństwie będzie tematem niniejszego rozdziału.

Ostatnia uwaga przed wejściem w szczegóły. Być może z tego powodu, źe wskaźniki i referencje mają cokolwiek sprzętowe konotacje, Ada korzysta z terminu typy dostępowe (patrz: uwagi tłumaczącego do niniejszego rozdziału). To narzuca punkt widzenia, że wartości typu dostępowego umożliwiają dostęp do innych obiektów danego typu (są jak dynamiczne nazwy takich obiektów) i nie powinno się o nich myśleć jak o adresach maszynowych. W rzeczywiśtosci, na poziomie implementacyjnym reprezentacja wartości typów dostępowych może różnić się od wskaźnika fizycznego.

3.2 Typy dostępowe a ścisła typizacja

Możemy zadeklarować zmienną, której wartości umożliwiają dostęp do obiektów typu T:

Ref : access T;

Jeśli nie podamy żadnej wartości początkowej, zastosowanie ma specjalna wartość oznaczona null. X może odnosić się do normalnie zadeklarowanego obiektu typu T (musi być on oznaczony):

Obj : aliased T;
   ...
Ref : Obj’Access;

Analogiczna wersja w C:

t* ref;
t obj;
ref = &obj;

T może być typem rekordowym:

type Data is
  record
    Day   : Integer range 1 .. 31;
    Month : Integer range 1 .. 12;
    Year  : Integer;
  end record;

dzięki czemu możemy zapisać:

Birthday : aliased Date := (Day => 10, Month => 12, Year => 1815);
AD : access Date := Birthday’Access;

a następnie w celu odczytywania poszczególnych składowych daty, do której odwołujemy się za pośrednictwem AD piszemy na przykład tak:

The Day : Integer := AD.Day;

Zmienne takie jak AD mogą odnosić się również do obiektów dynamicznie alokowanych na stercie (zwanej w Adzie pulą pamięci - storage pools):

AD := new Date’(Day => 27, Month => 11, Year => 1852);

Dwie powyższe daty to odpowiednio: dzień urodzenia i dzień śmierci Ady, hrabiny Lovelace, na cześć której został nazwany omawiany język.

Powszechnym zastosowaniem typów dostępowych jest tworzenie list — można zacząć tak:

type Cell is
  record
    Next  : access Cell;
    Value : Integer;
  end record;

a następnie można utworzyć łańcuch obiektów typu Cell połączonych ze sobą.

Czasami dogodnie jest nadać nazwę typowi dostępowemu:

type Date Ptr is access all Date;

Słowo kluczowe all oznacza, że nazwany dany typ może odnosić się zarówno do obiektów na stercie, jak i zadeklarowanych lokalnie na stosie, pod warunkiem, że są one aliasowane (oznaczone słowem kluczowym aliased).

Konieczność oznaczania obiektów jako aliasowanych jest pożytecznym zabezpieczeniem. Zwraca uwagę programisty na fakt, że obiekt może być dostępny pośrednio (analiza przepływu informacji), a także informuje kompilator, że obiekt nie może być poddany optymalizacji polegającej na umieszczeniu jego wartości w rejestrze (raczej trudno byłoby odwoływać się do niego...).

Jednak kluczową cechą tej materii jest to, że typ dostępowy zawsze identyfikuje typ obiektu, do którego wartości się odwołujemy, dzięki czemu ścisła typizacja egzekwowana jest w przypadku przypisań, przekazywania parametrów i w innych sytuacjach. Dodatkowo, wartość dostępowa zawsze ma ustaloną wartość (może to być null). W czasie działania programu, gdziekolwiek nastąpi dostęp do obiektu przy pomocy obiektu typu Date_Ptr, dokonywana jest kontrola, której celem jest upewnienie się, że wartość obiektu jest różna od null. Jeśli kontrola stwierdzi niepowodzenie — zgłaszany jest wyjątek Constraint_Error.

Można jawnie zasygnalizować, że wartość dostępowa nie może być równa null:

WD : not null access Date := Wedding Day’Access;

W takim wypadku musi być oczywiście podana wartość początkowa różna od null. Zaletą tak zwanego wykluczania null (null exclusion) jest to, że gwarantujemy, iż nie wystąpi wyjątek przy dostępie pośrednim do obiektu.

Na koniec zwróćmy uwagę, że wartość dostępowa może być zawarta w składowej struktury złożonej, w której typ składowej został oznaczony jako aliasowany:

A : array (1 .. 10) of aliased Integer := (1,2,3,4,5,6,7,8,9,10);
P : access Integer := A(4)Access;

Jednak na P nie możemy dokonywać operacji typu P++ czy P+1, aby móc odwołać się do składowej A(5) (tak jak jest to możliwe w C). Takie działania w C są źródłem błędów, ponieważ nic nie zabrania wskazywania poza koniec tablicy.

3.3 Typy dostępowe a dostępność

Wiemu już, że ścisła typizacja Ady zapewnia, iż wartości dostępowe nigdy nie mogą odwoływać się do obiektów niewłaściwego typu. Innym wymaganiem naszego języka jest zapewnienie, że obiekt, do którego się odwołujemy nie może przestać istnieć w tym czasie. Jest to osiągnięte tzw. notacją dostępności. Rozważmy:

package Data is
  type AI is access all Integer;
  Ref1 : AI;
end Data;
 
with data; use Data;
 
procedure P is
  K : aliased Integer;
  Ref2 : AI;
begin
  Ref2 := K’Access;   -- niedozwolone
  Ref1 := Ref2;
  ...
end P;

To dosyć sztuczny przykład, ale ilustruje kluczowe zagadnienia w jednym miejscu. Pakiet Data zawiera typ dostępowy AI oraz obiekt tego typu oznaczony Ref1. Procedura P deklaruje zmienną lokalną K oraz lokalną zmienną dostępową Ref2 typu AI. Procedura ta próbuje przypisać dostęp do K do zmiennej Ref2. Jest to zabronione. Nie chodzi tu o to, że odwołanie do Ref2 jest niebezpieczne, gdyż i Ref2, i K przestają istnieć po powrocie z wywołania procedury P — niebezpieczeństwo tkwi w tym, że próbujemy przypisać wartość Ref2 do zmiennej globalnej Ref1, która mogłaby zawierać odwołanie do K i mogłaby być wykorzystana po tym, jak K przestaje istnieć.

Podstawową zasadą jest to, że czas życia obiektu, do którego się odwołujemy (takiego jak K) musi być co najmniej tak długi, jak czas życia określonego typu dostępowego (w tym wypadku AI). W powyższym przykładzie warunek ten nie jest spełniony, dlatego próba zdobycia wskaźnika do K jest niedopuszczalna.

Zasady wyrażane są poziomami dostępu (jak głęboko sięga deklaracja czegoś) i najczęściej są statyczne, dzięki czemu mogą być sprawdzane przez kompilator - brak kosztów w czasie wykonywania programu. Lecz zasady dotyczące parametrów podprogramów będących anonimowymi typami dostępu mają naturę dynamiczną (wymagają kontroli w czasie działania programu). Daje to więcej elastyczności programiście niż byłoby to możliwe w przeciwnym wypadku.

W tak krótkim wprowadzeniu do Ady nie jest wykonalnym bardziej szczegółowy opis problemu. Wystarczy powiedzieć, że reguły dostępności Ady zapobiegają wiszącym odwołaniom, mogącym być źródłem wielu subtelnych i trudnych do wykrycia błędów (jak to jest w przypadku wielu "pobłażliwych" języków programowania).

3.4 Referencje do podprogramów

Ada zezwala na przetwarzanie referencji do procedur i funkcji w podobny sposób jak w przypadku innych obiektów. Obowiązuje tutaj zarówno ścisła typizacja, jak i reguły dostępności. Dla przykładu możemy zapisać:

A_Func: access function (X: Float) return Float;

co czyni A_Func obiektem, który może odwoływać się jedynie do funkcji z argumentem typu Float i zwracającej wynik tego samego typu (tak jak w przypadku predefiniowanej funkcji Sqrt).

Dzięki temu możemy zapisać:
A_Func := Sqrt'Access;

a następnie:

X: Float := A_Func (4.0);   -- wywołanie pośrednie

co spowoduje wywołanie Sqrt z argumentem 4.0, co w efekcie da - miejmy nadzieję - 2.0.

Ada gruntownie sprawdza takie parametry i wynik zawsze się zgadza, tak więc nie możemy wywołać pośrednio funkcji z niewłaściwą liczbą czy niewłaściwymi typami parametrów. Lista parametrów oraz typ wyniku tworzy coś, co technicznie zwie się profilem funkcji.

Weźmy więc teraz pod lupę predefiniowaną funkcję Arctan (funkcja odwrotna tangensa). Ma ona dwa parametry:

function Arctan (Y: Float; X: Float) return Float;

i zwraca kąt θ (w radianach) taki, że tan θ = Y / X. Jeśli spróbujemy zapisać:

A_Func := Arctan'Access;   -- niedozwolone
Z := A_Func(A);            -- wywołanie pośrednie zabronione

to kompilator odrzuci kod, gdyż prodil Arctan nie zgadza się z A_Func. Takie zachowanie jest właściwe, gdyż w przeciwnym wypadku funkcja Arctan odczytałaby dwa elementy ze stosu, podczas gdy pośrednie wywołanie A_Func umieściłoby na tym stosie tylko jeden parametr. W ten sposób uzyskalibyśmy bezwartościowy wynik obliczeń.

Odpowiednie kontrole w Adzie występują również poza obszarem jednostki kompilacji (jest to taka jednostka, która może być kompilowana oddzielnie - omówimy to w rozdziale Bezpieczna architektura. Podobne niezgodności nie są zabronione w języku C. Tym samymjest to źródłem wielu poważnych błędów.

Mamy także do czynienia z bardziej złożonymi sytuacjami, gdyż podprogram może mież jak parametr inny podprogram. Weźmy na przykład funkcję, której przeznaczeniem jest rozwiązanie równania Fn(x) = 0, gdzie Fn to podprogram przekazany jako parametr:

function Solve (Trial: Float; Accuracy: Float;
                Fn: access function (X: Float) return Float)
                return Float;

gdzie: Trial to domniemana wartość początkowa, Accuracy to wymagana dokładność,a Fn to równanie do rozwiązania.

Przypuśćmy dla przykładu, że inwestujemy 1000 dolarów dzisiaj i 500 dolarów w skali roku: jaka musiałaby być stopa procentowa, by po dwóch latach od teraz ozyskać dokładnie 2000 dolarów? Jeśli przez x% oznaczymy stopę procentową, to uzyskanie Net Final Value (Nfv) dokonane zostanie przez:

Nfv(x) = 1000 × (1 + x/100)2 + 500 × (1 + x/100)

Odpowiedź na zadane pytanie uzyskamy deklarując poniższą funkcję zwracającą 0.0, jeśli X ma wartość taką, że wartość końcowa będzie wynosiła dokładnie 2000.0.

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;

Następnie możemy zapisać:

Answer: Float :=
  Solve (Trial => 5.0, Accuracy => 0.01, Fn => Nfv_2000'Access);

Nasza przypuszczalna odpowiedź to około 5%, uzyskać chcemy wynik z dokładnością do dwóch miejsc po przecinku, natomiast Nfv_2000'Access określa nasz problem. Czytającego proszę o oszacowanie stopy procentowej - odpowiedź znajduje się na końcu tego rozdziału. Jeszcze tylko uwaga: Net Final Value i Net Present Worth to standardowe zwroty używane przez finansistów (przynajmniej anglojęzycznych...).

Kluczowym elementem tej dyskusji jest podkreślenie, że Ada uzgadnia również parametry funkcji będącej parametrem. W rzeczywistości zagnieżdżanie profili może być realizowane do dowolnego stopnia, a Ada na pewno sobie z tym poradzi. Większość języków programowania poddaje się po jednym poziomie zagnieżdżenia.

Zauważmy, że parametr Fn jest typem anonimowym. Typy dostępu do odprogramów mogą być nazwane lub anonimowe (dokładnie jak w przypadku innych obiektów).

Można wykorzystać również wyłączenie null. Tak naprawdę powinniśmy stworzyć zapis:

A_Func: not null access function (X: Float) return Float := Sqrt'Access;

Zaletą takiego zapisu jest gwarancja z naszej strony, że wartość A_Func w czasie wywołania pośredniego nigdy nie będzie wynosiła null.

Jeśli wydawać się będzie, że inicjalizacja A_Func jako Sqrt'Access będzie niezbyt pasowała, zawsze możemy zadeklarować:

function Default (X: Float) return Float is
begin
  Put ("Value not set"); return 0.0;
end Default;
...
A_Func: not null access function (X: Float) return Float := Default'Access;

Tym samym powinniśmy oczywiście dodać not null do profilu Solve:

function Solve (Trial: Float; Accuracy: Float;
                Fn: not null access function (X: Float) return Float)
                return Float;

Teraz mamy pewność co do tego, że aktualna funkcja odpowiadająca Fn nie może być null.

3.5 Zagnieżdżone podprogramy jako parametry

Wspomnieliśmy wcześniej, że reguły dostępności dotyczą również wartości typu dostęp do podprogramu. Przypuśćmy, że zadeklarowaliśmy Solve, tak, że parametr Fn jest typem nazwanym, a sama funkcja Solve znajduje się w jakimś pakiecie:

package Algorithms is
  type A_Function is not null access function (X: Float) return Float;
 
  function Solve (Trial: Float; Accuracy: Float; Fn: A_Function)
           return Float;
  ...
end Algorithms;

Przypuśćmy, że teraz decydujemy się na wyrażenie interesującego nas przykładu z wartością docelową przekazaną jako argument. Możemy spróbować:

with Algorithms; use Algorithms;
 
function Compute_Interest(Target: Float) return Float is
  function Nfv_T (X: Float) return Float is
    Factor: constant Float := 1.0 + X/100.0;
  begin
    return 1000.0 * Factor**2 + 500.0 * Factor – Target;
  end Nfv_T;
begin
  return Solve (Trial => 5.0, Accuracy => 0.01, Fn => Nfv_T'Access);
                                                     -- niedozwolone
end Compute_Interest;

Nfv_T'Access jest niedozwolony jako parametr Fn, gdyż narusza reguły dostępności. Kłopot polega na tym, że funkcja Nfv_T znajduje się na wewnętrznym poziomie w stosunku do typu A_Function (dlatego, że musi być możliwość uzyskania wartości parametru Target.) Jeśli zezwolilibyśmy na skorzystanie z Nfv_T'Access, to moglibyśmy przypisać tę wartośc do zmiennej globalnej typu A_Function, co spowodowałoby po powrocie z funkcji Compute_Interest, że ciągle mielibyśmy odwołanie do Nfv_T, nawet jeśli ustałby dostęp do niej. Na przykład:

Dodgy_Fn: A_Function := Default'Access;   -- zmienna globalna
 
function Compute_Interest (Target: Float) return Float is
  function Nfv_T (X: Float) return Float is
    ...
  end Nfv_T;
begin
  Dodgy_Fn := Nfv_T'Access;   -- niedozwolone
  ...
end Compute_Interest;

Teraz przypuśćmy, że po wywołaniu Compute_Interest wykonujemy:

Answer := Dodgy_Fn (99.9);   -- byłyby wyniki nie do przewidzenia

Wywołanie Dodgy_Fn spowodowałoby próbę wywołania Nfv_T, lecz to jest już niemożliwe, gdyż jest ona lokalna względem Compute_Interest i próbowałaby uzyskać dostęp do parametru Target, który już nie istnieje. Jeśli Ada nie zabroniłaby tego, w konsekwencji otrzymalibyśmy wynik nie do przewidzenia (rezultat bezsensowny lub zgłoszenie wyjątku). Zauważmy, że użycie typu anonimowego jako parametru w poprzednim podrozdziale pozwala przekazać funkjcę zagnieżdżoną jako parametr, lecz reguły dostępności zabraniają przypisania jej do zmiennej Dogdy_Fn. Kontrole systemu wykonawczego wykryłyby, że Nfv_T jest bardziej zagnieżdżona niż docelowy typ dostępowy A_Function, a tym samym zgłoszony byłby wyjątek Program_Error. Rozwiązaniem jest zmiana pakietu Algorithms, tak więc:

package Algorithms is
  function Solve(Trial: Float; Accuracy: Float;
                 Fn: not null access function (X: Float) return Float)
                 return Float;
end Algorithms;

a oryginalna funkcja Compute_Interest jest dokładnie taka, jak przedtem (z tym wyjątkiem, że komentarz -- niedozwolone należy usunąć).

To zamieszanie myślowe może sugerować, że problem leży w zagnieżdżeniu Nfv_T wewnątrz Compute_Interest. Rzeczywiście można by zadeklarować Nfv_T na najbardziej zewnętrznym poziomie, co pozwoliłoby uniknąć problemów z dostępnością, lecz zarazem zmusiłoby nas do przekazywania wartości Target globalnie jakimś pakietem - w stylu bloku Common znanego z Fortranu. Nie możemy dodać tej wartości jako dodatkowego parametru Nfv_T, ponieważ parametry Nfv_T muszą odpowiadać parametrom Fn. Natomiast przekazywanie danych globalnych w ten sposób nie jest dobrą praktyką. Narusza ona zasady ukrywania informacji oraz abstrakcji, a ponadto sposób ten nie będzie skuteczny w programach wielozadaniowych. Przy okazji: praktyka zagnieżdżania funkcji wewnątrz innej funkcji, w którym to przypadku funkcja wewnętrzna korzysta ze zmiennej nielokalnej (takiej jak Target) często zwana jest domknięciem zstępującym (downward closure).

Domknięcie zstępujące - czyli przekazanie wskaźnika do podprogramu zagnieżdżonego jak parametru w czasie wykonywania programu - jest mechanizmem wykorzystywanym w kilku fragmentach predefiniowanej biblioteki Ady, jako wspomaganie takich aplikacji jak iteracja na strukturach danych. Zagnieżdżanie podprogramów to naturalna potrzeba takich aplikacji, ponieważ wymagają one przekazywania informacji nie będących lokalnymi. Realizacja tych mechanizmów jest trudniejsza w "płaskich" językach typu C, C++ czy Java. Jakkolwiek w niektórych językach można do modelowania zagnieżdżania podprogramów wykorzystywać rozszerzenia typów, sposób ten jest mniej czytelny i może powodować problemy w czasie konserwacji programu.

I w końcu, pewne aplikacje mogą wymagać grupy algorytmów w stylu zagnieżdżonym. Tak więc pakiet Algorithms możemy wzbogacić o inne użyteczne elementy:

package Algorithms is
  function Solve(Trial: Float; Accuracy: Float;
                 Fn: not null access function (X: Float) return Float)
           return Float;
 
  function Integrate (Lo, Hi: Float; Accuracy: Float;
                      Fn: not null access function (X: Float) return Float)
           return Float;
 
  type Vector is array (Positive range ) of Float;
 
  procedure Minimize(V: in out Vector; Accuracy: Float;
                     Fn: not null access function (V: Vector) return Float);
end Algorithms;

Funkcja Integrate jest podobna do Solve. Oblicza całkę oznaczoną funkcji (przekazanej w parametrze) w podanym zakresie. Procedura Minimize troszeczkę się różni. Znajduje ona te wartości elementów tablicy V, które czynią wartość przekazanej w parametrze funkcji minimalną. Mogłaby zajść sytuacja, gdy koszt funkcji byłby minimalizowany, sama funkcja byłaby wynikiem całkowania, a w obliczeniach korzystałoby się z wartości V (sytuacja dość nieprawdopodobna, lecz sam autor spędził kilka pierwszych lat swego życia programisty robiąc podobne obliczenia dla przemysłu chemicznego).

Struktura mogłaby wyglądać tak:

with Algorithms; use Algorithms;
procedure Do_It is
 
  function Cost(V: Vector) return Float is
 
    function F(X: Float) return Float is
      Result: Float;
    begin
      ... -- obliczenie wartości Result korzystając z V oraz X
      return Result;
      end F;
 
  begin
    return Integrate(0.0, 1.0, 0.01, F'Access);
  end Cost;
 
  A: Vector(1 .. 10);
begin
  ... -- albo odczyt, albo ustalenie próbnych wartości wektora A
  Minimize(A, 0.01, Cost'Access);
  ... -- wyjście końcowych wartości wektora A.
end Do_It;

To wszystko działa w Adzie 2005 jak marzenie! Jak w Algolu 60! W innych językach programowania takie działania są albo trudne do zrealizwoania, albo wymagają niebezpiecznych konstrukcji z możliwością generowania wiszących odwołań.

Dalsze przykłady wykorzystania typów dostępowych do podprogramów znajdziemy w rozdziale Bezpieczna komunikacja.

Na zakończenie: stopa procentowa dająca 2000 dolarów w ciągu dwóch lat po zainwestowaniu 1000 i 500 dolarów wynosi około 18.6%. Fajny procent, jeśli jest możliwość by go uzyskać!

Rozdział 2: Bezpieczny system typów Rozdział 4: Bezpieczna architektura
Tekst oryginalny w języku angielskim - pdf2
Zmieniony: Środa, 24 Marzec 2010 09:28  

Dodaj swój komentarz

Imię:
Temat:
Komentarz: