Eiffel


4. Eiffel

Twórcą języka Eiffel jest Bertrand Meyer. Pierwsza wersja języka powstała w roku 1985, otrzymując roboczą nazwę SIMULA 85. Był to faktycznie Eiffel * unowoczeœ­niona wersja Simuli, uwolniona od pewnych niepotrzebnych cech i wzbogacona o nowe własnoœci. Pełny opis tej pierwszej wersji języka powstał w firmie B. Meyera, Interactive Software Engineering (ISE), i został opublikowany w roku 1986. Po wprowadzeniu szeregu zmian obowiązująca obecnie wersja języka została opublikowana w roku 1992.

Język Eiffel nie ma standardu ISO, czy też ANSI. Definicja języka jest “własnoœcią publiczną”, nadzorowaną przez społeczną organizację NICE (Non-profit International Consortium for Eiffel). W roku 1995 NICE opublikowała pierwszą wersję biblioteki ELKS-95 (Eiffel Library Kernel Standard), której klasy powinien tłumaczyć każdy kompilator.

Program Ÿródłowy w języku Eiffel składa się wyłącznie z definicji klas, w których zdefiniowano odpowiednie operacje. Tak więc, jeżeli celem programu jest wyprowadzenie na wyjœcie łańcucha znaków, zdefiniujemy klasę SIMPLE, a w niej odpowiednią operację drukowania:

class SIMPLE creation

make

feature

make is

do

io.put_string("Hello, World!;%N")

end

end --class SIMPLE

W klasie SIMPLE słowo kluczowe creation sygnalizuje nazwę operacji (make), która inicjuje jej obiekty. W instrukcji io.put_string("Hello,World!;%N")

obiekt io jest standardowym strumieniem wyjœciowym, otwieranym automatycznie po uruchomieniu programu, zaœ put_string jest operacją zdefiniowaną w klasie strumieni. Argument tej operacji jest łańcuchem znaków; dwa końcowe znaki łańcucha mają znaczenie symbolu nowego wiersza. Komentarze w języku Eiffel pisze się jako łańcuchy znaków po dwóch znakach “--”. Jeżeli klasę SIMPLE umieœcimy w pliku simple.e i wywołamy kompilator poleceniem “compile simple.e -o simple”, to otrzymamy kod wykonalny programu w pliku simple. Żądany napis na ekranie terminala otrzymamy pisząc po prostu simple.

4.1. System typów języka Eiffel

Eiffel jest językiem o typizacji statycznej. Oznacza to, że każdy program musi spełniać następujące reguły zgodnoœci:

Wszystkie typy języka Eiffel są implementowane w klasach. System typów obejmuje dwa ich rodzaje: typ odnoœnikowy (reference type) i typ rozwinięty (expanded type). Wystąpienie typu odnoœnikowego jest zmienną typu implementowanego w pewnej klasie, której wartoœć jest odnoœnikiem do pewnego obiektu tej klasy, lub odnoœnikiem pustym (void). Jeżeli zadeklarujemy zmienną typu odnoœnikowego SOME_CLASS:

x: SOME_CLASS;

to po deklaracji jej wartoœć będzie void. Dopiero gdy utworzymy za pomocą operatora “!!” wystąpienie klasy SOME_CLASS (operator “!!” alokuje odpowiedni obszar pamięci dla obiektu, podobnie jak operator new w innych językach obiektowych), wywołując na zmiennej x metodę kreacji obiektu (najczęœciej o nazwie make; zadaniem tej metody jest zainicjowanie obiektu, a więc pełni ona rolę znanego w innych językach obiektowych konstruktora):

!!x.make;

wówczas wartoœcią zmiennej x będzie odnoœnik do nowo utworzonego obiektu klasy SOME_CLASS.

Wystąpienie typu rozwiniętego jest również zmienną typu implementowanego w pewnej klasie, ale wartoœcią tej zmiennej jest gotowy do użycia obiekt tej klasy. W definicji klasy implementującej typ rozwinięty słowo kluczowe class musi być poprzedzone słowem kluczowym expanded, np.

expanded class INTEGER feature ... end

Zmienna takiej klasy nie wymaga wywołania operatora “!!” przed jej użyciem. Jeżeli chcemy mieć wystąpienie klasy INTEGER, to po prostu deklarujemy:

x: INTEGER;

Powyższa deklaracja automatycznie przydziela zmiennej x pamięć dla obiektu typu INTEGER i inicjuje ten obiekt na wartoœć 0. Jeżeli teraz napiszemy instrukcję

x := 12;

będzie to oznaczać, że obiektowi x przypisano wartoœć 12.

Do typów rozwiniętych należą wszystkie typy proste: INTEGER, REAL, DOUBLE, CHARACTER i BOOLEAN. Zmienne typów prostych są w chwili deklaracji automatycznie inicjowane do niezależnych od implementacji wartoœci początkowych, jak pokazano niżej:

Typ

Wartoœć początkowa

INTEGER

0

REAL

0.0

DOUBLE

0.0

CHARACTER

Null_char

BOOLEAN

False

Język Eiffel stwarza jeszcze dodatkową możliwoœć: jeżeli z jakiegoœ powodu nie chcemy mieć odnoœnika do obiektu klasy zdefiniowanej przez użytkownika (np. o nazwie SOME_CLASS), lecz sam obiekt, to możemy napisać deklarację:

x: expanded SOME_CLASS

W takim przypadku x jest zmienną typu SOME_CLASS, ale zamiast inicjować zmienną operatorem “!!”, cała pamięć potrzebna dla obiektu klasy SOME_CLASS jest przydzielana w punkcie deklaracji, a każdy atrybut klasy SOME_CLASS jest automatycznie inicjowany do wartoœci domyœlnej. Jeżeli atrybut jest typu odnoœnikowego, to zostanie automatycznie zainicjowany wartoœcią void.

Można również postąpić odwrotnie i zamiast korzystać z typów rozwiniętych dla typów prostych, używać zmiennych odnoœnikowych klas INTEGER_REF, REAL_REF, CHARACTER_REF i BOOLEAN_REF. Daje to swobodę wyboru pomiędzy odnoœnikami do obiektów, a samymi obiektami.

4.2. Klasy

Elementy składowe klas, tj. ich atrybuty (pola) oraz wykonywane na nich operacje są łącznie nazywane cechami (feature). Atrybuty mogą być typów prostych, predefiniowanych typów złożonych, lub typów odnoœnikowych. Na przykład definicja klasy BOOK:

class BOOK feature

title: STRING;

publication_date, number_of_pages: INTEGER;

author: PERSON;

end --class BOOK

zawiera atrybut title typu predefiniowanego STRING, dwa atrybuty typu prostego INTEGER oraz zmienną odnoœnikową author do wczeœniej zdefiniowanej klasy PERSON. Domyœlnie wszystkie te atrybuty są publiczne, tzn. bezpoœrednio dostępne w obiektach klasy BOOK.

Klasy wykorzystuje się bądŸ poprzez mechanizm dziedziczenia, bądŸ też poprzez zadeklarowanie po słowie kluczowym feature zmiennej odnoœnikowej typu wczeœniej zdefiniowanego, jak to uczyniono dla atrybutu author. W tym drugim przypadku mówimy, że klasa BOOK jest klientem klasy PERSON, a klasa PERSON jest dostawcą (usług) dla klasy BOOK. Relację taką nazywa się kontraktem; w ogólnoœci kontrakt pomiędzy klasą a jej klientami jest pewnym formalnym uzgodnieniem, wyrażającym prawa i obowiązki każdej ze stron.

Zadeklarujmy zmienną odnoœnikową typu BOOK, np. bref:BOOK; jej (domyœlną) wartoœcią początkową będzie void (odnoœnik pusty), a następnie utwórzmy obiekt klasy BOOK. Ponieważ klasa BOOK nie zawiera frazy `creation' i nie ma zdefiniowanej funkcji inicjującej jej pola do wartoœci innych niż domyœlne, obiekt tej klasy tworzy się po prostu przez wywołanie operatora “!!” z argumentem bref, tj. przez wykonanie instrukcji !!bref. Wykonanie tej instrukcji obejmuje następujące trzy etapy:

Utworzenie nowego wystąpienia klasy BOOK, nazwijmy go ob. Wystąpienie ob będzie zbiorem pól, po jednym polu dla każdego atrybutu.

Zainicjowanie każdego pola BOOK zgodnie ze standardowymi wartoœciami domyœlnymi.

Dołączenie wartoœci bref (odnoœnika) do obiektu ob.

Klasa może zawierać więcej niż jedną sekcję, wprowadzaną słowem kluczowym feature. Eiffel oferuje także prosty, ale jednoczeœnie bardzo giętki mechanizm sterowania dostępem do atrybutów i metod, nazywanych tu cechami. Cechy wymieniane zaraz po słowie feature są dostępne dla wszystkich klientów klasy. Dostęp można ograniczyć, pisząc po słowie feature nazwę klasy, lub przedzielone przecinkami nazwy kilku klas w nawiasach klamrowych; jeżeli chcemy, aby różne cechy miały różne ograniczenia dostępu, definiujemy dwie lub więcej sekcji feature. Np. w klasie

class P feature

f ...

g ...

feature{A,B}

h ...

...

end

cechy f oraz g będą dostępne dla wszystkich klientów, zaœ cecha h jedynie dla klas A i B oraz dla ich klas potomnych. Oznacza to, że dla x:P odwołanie x.h będzie błędem syntaktycznym, chyba że wystąpi w definicji jednej z klas A, B lub jednej z ich klas potomnych.

Jeżeli chcemy ukryć pewną cechę przed wszystkimi klientami, wtedy po słowie feature piszemy w nawiasie klamrowym NONE:

class P feature

exported

feature{NONE}

secret

end

Cecha o nazwie exported jest publicznie dostępna, zaœ cecha o nazwie secret jest prywatną cechą klasy P. NONE jest klasą ze standardowej biblioteki ELKS; nie ma ona ani wystąpień, ani klas potomnych. Przy takiej definicji, odwołanie kwalifikowane x.secret będzie błędem syntaktycznym; do cechy tej można się odwoływać jedynie w definicji metody z klasy P lub jej klasy potomnej.

Szczególnym rodzajem atrybutów są stałe. W programach czysto obiektowych również stałe powinny być obiektami, współdzielonymi przez wiele obiektów tej samej klasy lub obiekty różnych klas. Eiffel rozróżnia stałe typów prostych, stałe typu klasy oraz stałe wprowadzane przez funkcję once. W przypadku typów prostych stałe są atrybutami definiowanymi w klasie; wprowadza się je słowem kluczowym is po deklaracji typu, jak pokazano niżej:

Max: INTEGER is 100;

Pi: REAL is 3.14;

Better_pi: DOUBLE is 3.1415926;

Ok: BOOLEAN is True;

Menu_option: CHARACTER is *M*

Tak zdefiniowane stałe symboliczne Max, Pi, Better_pi, Ok i Menu_option mogą być albo eksportowane (używalne przez obiekty innych klas), albo prywatne w klasie. W przeciwieństwie do innych atrybutów stałe symboliczne nie zajmują dodatkowego obszaru pamięci w obiektach klasy, w której zostały zdefiniowane. Jeżeli w pewnych obliczeniach musimy używać wielu specyficznych stałych bez wiązania ich z konkretnymi obiektami (np. w edytorach tekstu), wówczas grupujemy je w klasie, która nie będzie miała wystąpień, lecz będzie klasą rodzicielską dla klas korzystających z tych stałych. Odpowiednie deklaracje mogą mieć postać:

class EDITOR_CONSTANTS feature

Insert: CHARACTER is `i'

Delete: CHARACTER is `d'; --etc

...

end

class SOME_CLASS_FOR_THE_EDITOR inherit

EDITOR_CONSTANTS

...Inne klasy rodzicielskie

feature

...

end

Zauważmy, że klasa SOME_CLASS_FOR_THE_EDITOR może wymagać innych klas rodzicielskich, a zatem pokazany wyżej schemat wymaga dziedziczenia mnogiego.

Implementacja operacji w klasie może być procedurą lub funkcją; procedura wykonuje pewne działanie, które może zmienić stan obiektu, zaœ funkcja zwykle oblicza pewną wartoœć w oparciu o stan obiektu. Procedury i funkcje nie mogą być zagnieżdżane. Postać definicji procedur i funkcji można zademonstrować na przykładzie klasy POINT:

class POINT

creation make

feature

x, y: REAL;

make(vx, vy: REAL) is

do

x := vx; y := vy;

end;--make

scale(factor:REAL) is

do

x := factor*x;

y := factor*y;

end; --scale

translate(a,b:REAL) is

do

x := x+a; y := y+b;

end; --translate

distance(other:POINT): REAL is

do

Result := sqrt((x-other.x)^2+(y-other.y)^2)

end; --distance

distance_to_origin: REAL is

local

origin: POINT

do

!!origin.make(0,0);

Result := distance(origin)

end --distance_to_origin

end - class POINT

Definicja klasy POINT zawiera definicje dwóch procedur (scale i translate) i dwóch funkcji (distance oraz distance_to_origin). Każda procedura lub funkcja jest wprowadzana słowem kluczowym is, zaœ ich instrukcje, zawarte pomiędzy słowami kluczowymi do i end, przedzielone są œrednikami, pełniącymi rolę separatorów. Funkcja distance_to_origin zawiera ponadto zmienną lokalną origin. Zauważmy, że obydwie funkcje zawierają instrukcję przypisania wartoœci obliczonego wyrażenia do (predefiniowanej) zmiennej Result, przekazującej wynik funkcji. Result jest specyficznym dla języka Eiffel sposobem zwracania wartoœci przez funkcję; przy wywołaniu przybiera automatycznie standardową wartoœć początkową, przewidzianą dla typu zwracanego, zgodnie z regułami języka. Ponieważ w ciele funkcji distance_to_origin jest wywoływana funkcja distance z argumentem typu POINT, tworzony jest obiekt klasy POINT instrukcją !!origin.make(0,0). Jeżeli funkcja distance_to_origin będzie wywoływana wielokrotnie, wówczas pokazana wyżej jej implementacja okaże się nieefektywna, ponieważ za każdym razem zmienna (referencja) origin będzie inicjowana na wartoœć void i za każdym razem będzie tworzony nowy obiekt klasy POINT, reprezentujący punkt (0,0).

4.3. Procedury tworzenia obiektów

Obiekty są wielkoœciami fazy wykonania. Jeżeli w klasie nie zdefiniowano żadnej procedury inicjującej atrybuty do wartoœci innych niż domyœlne, obiekty są tworzone w miarę potrzeby za pomocą operatora “!!” przykładanego do zmiennej odnoœnikowej. Gdyby np. w prezentowanej wyżej klasie POINT nie było procedury make, konstrukcja obiektu tej klasy z domyœlnymi wartoœciami atrybutów (x,y) mogłaby mieć postać deklaracji:

p1: POINT; !!p1;

Taki sposób kreacji jest wystarczający dla obiektów, których atrybuty są całkowicie od siebie niezależne. Jednak w większoœci przypadków atrybuty obiektów muszą spełniać pewne warunki zgodnoœci. Typowym przykładem może być obiekt reprezentujący osobę, w którym mamy atrybut wiek i inny atrybut rok_urodzenia. Wartoœci tych dwóch atrybutów nie mogą być ustawione na dowolne wartoœci: suma atrybutu rok_urodzenia i atrybutu wiek musi dać albo rok bieżący, albo rok poprzedni.

Dla zainicjowania atrybutów do wartoœci innych niż domyœlne w ciele klasy definiuje się procedury kreacji wprowadzane słowem kluczowym creation. Procedury te mają zwyczajowo nazwę make lub też make_ jest przedrostkiem pełnej nazwy procedury. Przykładowa deklaracja może mieć postać:

class A creation

make_1, make_2, ...

feature

...deklaracje cech, w tym procedur make_1, make_2, ...

end

Dla zmiennej odnoœnikowej a:A instrukcja tworzenia obiektu klasy A nie będzie już !!a, lecz !!a.make_1(...)lub !!a.make_2(...) gdzie (...) jest wykazem argumentów aktualnych dla make_1 albo make_2 (wywołania !!a i !!a.make(...) wzajemnie się wykluczają). Efektem wykonania takiej instrukcji jest utworzenie obiektu z domyœlnymi wartoœciami atrybutów (jak w przypadku braku procedur make), a następnie wywołanie na tym obiekcie make z podanymi argumentami. Na przykład w klasie POINT moglibyœmy zadeklarować dwie procedury make:

class POINT1 creation

make_cartesian, make_polar

feature

x,y,:REAL

feature{NONE}

make_cartesian(a,b:REAL) is

do x:=a; y:=b end;

make_polar(r,t:REAL) is

do x:=r*cos(t); y:=r*sin(t) end

end--POINT

Przy takiej postaci deklaracji obydwie procedury make są prywatne w klasie POINT. Nie można ich wywoływać na istniejącym obiekcie, np. my_point.make_cartesian(0,1), ale można przy ich pomocy tworzyć obiekty tej klasy, np. instrukcją !!my_point.make_polar(1,Pi/2). Można także, w razie potrzeby, dać możliwoœć kreacji obiektów danej klasy innym klasom wymienionym w nawiasach klamrowych po słowie kluczowym creation, np.

class C creation{A,B, ...} m1, m2 ... end

W pewnych przypadkach może być potrzebne zarówno inicjowanie obiektu wartoœciami domyœlnymi, jak i zadanymi w procedurach kreacji. Wtedy jedną z procedur deklarowanych po creation powinna być bezargumentowa procedura nothing. Ciało tej procedury jest puste: nothing is do end, a sama procedura jest dziedziczona z klasy ANY. Innym przypadkiem szczególnym może być definicja klasy, która nie daje prawa tworzenia swoich wystąpień żadnemu klientowi:

class C creation

--Pusty wykaz procedur kreacji

feature

...

end

Dla pojedynczej klasy taka definicja nie ma większego sensu. Jeżeli jednak klasa C jest pomyœlana jako klasa rodzicielska dla kolejnych klas potomnych, wtedy jest to konstrukcja wysoce przydatna.

4.4. Obiekty współdzielone

W języku Eiffel nie ma zmiennych globalnych, których wartoœci mogłyby zostać zmienione w procedurach. Nie stosuje się w nim również technik współdzielenia obiektów takich jak opóŸnione inicjowanie, bądŸ wykorzystanie wzorca projektowego Singleton. Zamiast tego używa się predefiniowanej funkcji once, która pozwala współdzielić różnym obiektom wspólne zasoby (jest to mechanizm, podobny do deklaracji zmiennych klasy w innych językach obiektowych). Uzyskuje się dzięki temu następujące korzyœci: