Czasami chcemy wprowadzić nowy typ podobny do istniejącego
Transkrypt
Czasami chcemy wprowadzić nowy typ podobny do istniejącego
Antoni M. Zajączkowski: Algorytmy i podstawy programowania – Operacje podstawowe i typy pochodne 7 czerwca 2015 OPERACJE PODSTAWOWE I TYPY POCHODNE Czasami chcemy wprowadzić nowy typ podobny do istniejącego, ale różny od niego. Jeżeli T jest istniejącym typem, to możemy napisać type S is new T; W takim przypadku S nazywamy typem pochodnym (derived type), a typ T nazywamy tytypem macierzystym (parent type) typu S. Mówimy czasami, że typ pochodny należy do tej samej klasy (class) co typ macierzysty. Oznacza to między innymi, że zapis literałów i agregatów jest taki sam, oraz domyślne wyrażenia początkowe typu pochodnego, albo jego składowych są takie same jak w przypadku typu macierzystego. W szczególności jeżeli typ macierzysty jest typem rekordowym, to typ pochodny jest też typem rekordowym, a jego składowe mają te same identyfikatory. Zbiór wartości typu pochodnego jest kopią zbioru wartości typu macierzystego, ale typy te są różne i nie można wartości jednego typu przypisywać obiektom drugiego typu. Konwersja między danymi tych różnych typów jest jednak możliwa. Operacjami podstawowymi (primitive operations) nazywamy następujące operacje: - Zdefiniowane wstępnie operacje takie jak podstawienie, relacja równości, odpowiednie atrybuty, - W przypadku typu pochodnego, operacje podstawowe odziedziczone po typie macierzystym, które mogą być jednak zastąpione przez operacje zdefiniowane na nowo, - W przypadku typu zadeklarowanego w pakiecie definicyjnym, podprogramy zadeklarowane w tym pakiecie, posiadające parametry formalne lub wynik zadeklarowanego typu. Warto wspomnieć, że wartości typu wyliczeniowego też są zaliczane do operacji podstawowych, ponieważ traktowane są jak funkcje bezparametrowe o wartościach tego typu (Barnes, 2006). Przykład 1. W przypadku typu Boolean, literały False i True są operacjami podstawowymi, ponieważ traktowane są jakby były funkcjami takimi jak function True return Boolean is begin return Boolean'Val(1); end; Ogólna idea jest taka, że typ pochodny posiada pewne operacje podstawowe, dziedziczone po typie macierzystym i można do zbioru tych operacji dodać nowe operacje podstawowe. Przykład Przykład 2. Pakiet Wektory_Na_Plaszczyznie i program Test_Wektory_Na_Plaszczyznie. Wspomnieliśmy już, że operacje dziedziczone mogą być zastąpione przez nowe operacje. Dokładniej, operacje dziedziczone zastępujemy przez nowe zadeklarowane podprogramy o tych samych identyfikatorach jak podprogramy należące do zbioru operacji podstawowych typu macierzystego, przy czym podprogramy zastępujące muszą być zadeklarowane w tym samym obszarze deklaracji, w którym definiowany jest typ pochodny. Deklarując typ pochodny należy przestrzegać dwóch zasad (Barnes, 2006): 1 Antoni M. Zajączkowski: Algorytmy i podstawy programowania – Operacje podstawowe i typy pochodne 7 czerwca 2015 1. Nie można tworzyć typu pochodnego z typu prywatnego przed podaniem pełnej deklaracji tego typu. 2. Jeżeli tworzymy typ pochodny w tym samym pakiecie definicyjnym, w którym deklarujemy typ macierzysty, to typ pochodny dziedziczy wszystkie operacje po typie macierzystym i nie można dodać nowych operacji do typu macierzystego po deklaracji typu pochodnego. Mimo że, każdy typ pochodny jest różny, to dzięki pokrewieństwu typów pochodnych wyprowadzonych od wspólnego przodka, wartość jednego typu pochodnego można łatwo zamienić na wartość innego typu powstałej klasy. Przykład 3 (Barnes, 2006). Niech będą dane deklaracje: type Light is new Colour; type Signal is new Colour; type Flare is new Signal; Typy te tworzą pewną hierarchię typów, która zaczyna się od typu Colour. Możemy swobodnie dokonywać konwersji wartości tych typów. Na przykład możemy pisać: L : Light; F : Flare; ... F := Flare(L); Ostatnia instrukcja jest legalna i nie musimy pisać: F := Flare(Signal(Colour(L))); Wprowadzenie typów pochodnych rozszerza możliwości konwersji typów tablicowych. Wartość typu tablicowego może być zamieniona na nową wartość innego typu tablicowego, jeżeli podtypy składowych są statycznie zgodne, a typy indeksów są identyczne, albo mogą być konwertowane jeden na drugi. Powstaje pytanie o korzyści płynące z wprowadzenia typów pochodnych. Jednym z istotnych powodów jest unikanie mieszania obiektów koncepcyjnie należących do różnych typów. Przykład 4 (Barnes, 2006). Załóżmy, że chcemy liczyć jabłka i pomarańcze. W związku z tym możemy napisać type Apples is new Integer; type Oranges is new Integer; ... No_Of_Apples : Apples; No_Of_Oranges : Oranges; Obydwa typy są typami pochodnymi typu Integer, a więc obydwa dziedziczą dwuargumentową operację dodawania, co pozwala napisać No_Of_Apples := No_Of_Apples + 1; No_Of_Oranges := No_Of_Oranges + 1; Oczywiście, nie wolno pisać No_Of_Apples := No_Of_Oranges; Możemy jednak dokonać konwersji No_Of_Apples := Apples'(No_Of_Oranges); Przypuśćmy teraz, że mamy dwie procedury obsługujące sprzedaż obydwu rodzajów owoców. procedure Sell (N : Apples); 2 Antoni M. Zajączkowski: Algorytmy i podstawy programowania – Operacje podstawowe i typy pochodne 7 czerwca 2015 procedure Sell (N : Oranges); Możemy wywołać jedną z nich Sell (No_Of_Oranges); natomiast wywołanie Sell (5); jest niejednoznaczne. W celu uniknięcia tej niejednoznaczności trzeba pisać Sell (Oranges'(5)); Jeżeli podprogram jest dziedziczony, to w rzeczywistości nie jest tworzony nowy podprogram. Wywołanie odziedziczonego podprogramu jest w rzeczywistości wywołaniem podprogramu macierzystego, przy czym parametry rodzajów in i in out są niejawnie konwertowane na typ macierzysty tuż przed wywołaniem, natomiast parametry rodzajów out i in out są konwertowane niejawnie zaraz po wywołaniu. Jeżeli więc mamy wyrażenie My_Apples + Your_Apples to w rzeczywistości wykonywana jest instrukcja Apples(Integer(My_Apples) + Integer(Your_Apples)) Zajmijmy się teraz ograniczeniami zbioru wartości typu pochodnego. Możemy tworzyć typy pochodne, których wartości należą do pewnego zakresu. Dla przykładu deklaracja type Probability is new Float range 0.0..1.0; jest równoważna deklaracjom type Anonim is new Float; subtype Probability is Anonim range 0.0..1.0; Oznacza to, że podtyp Probability jest podtypem anonimowego typu pochodnego. Zauważmy, że zbiór wartości typu pochodnego jest kopią zbioru wartości typu macierzystego Float. Operacje "+", ">" i inne, działają w całym nieograniczonym zbiorze wartości. Przykład 5 (Barnes, 2006). Niech będzie dana deklaracja P : Probability; Można legalnie napisać P > 2.0 mimo, że nie można podstawić wartości 2.0 do zmiennej P. Wyrażenie P > 2.0 jest zawsze nieprawdziwe, chyba że nie jest zainicjowane odpowiednio i przez przypadek ma złą wartość. Rozpatrzymy teraz ograniczenia występujące w przypadku dziedziczonych podprogramów. Podprogram odziedziczony jest podprogramem macierzystym, w którym wszystkie egzem egzemplarze zemplarze (instances) typu macierzystego są wymienione na typ pochodny. Podtypy są wymienione na równoważne podtypy z odpowiednimi ograniczeniami, a domyślne wyrażenia inicjujące są konwertowane przez dodanie konwersji typów. Dowolny parametr, albo wynik innego typu pozostaje niezmieniony. Przykład 6 (Barnes, 2006). Niech type T is ... ; subtype S is T range L .. R; function F(X : T; Y : T := E; Z : Q) return S; 3 Antoni M. Zajączkowski: Algorytmy i podstawy programowania – Operacje podstawowe i typy pochodne 7 czerwca 2015 przy czym E jest wyrażeniem typu T, natomiast Q nie należy do klasy typu T, a więc jest całkowicie niezależny. Jeżeli napiszemy type TT is new T; to z tego wynika, że napisaliśmy też subtype SS is TT range TT(L) .. TT(R); a nagłówek funkcji dziedziczonej F ma postać function F(X : TT; Y : TT := TT(E); Z : Q) return SS; W nagłówku typ T został zastąpiony przez TT, podtyp S przez SS, dokonana została konwersja wyrażenia E na wartość typu TT, natomiast typ Q pozostał niezmieniony. Warto zauważyć, że identyfikatory parametrów formalnych pozostały takie same. Typy pochodne pod pewnymi względami są alternatywą do typów prywatnych. Typy pochodne mają zaletę dziedziczenia literałów, ale często mają wadę dziedziczenia zbyt wielu rzeczy po typie macierzystym. Przykład 7 (Barnes, 2006). Długość i powierzchnia. powierzchnia Przypuśćmy, że chcemy obliczać pola powierzchni podstawowych płaskich figur geometrycznych. W tym celu definiujemy typy type Length is new Float; type Area is new Float; W ten sposób określone typy dziedziczą operacje po typie Float. Należy jednak zwrócić uwagę na to, że część z tych operacji nie ma sensu, jeżeli zauważymy, że mnożenie dwóch egzemplarzy typu Length powinno dać wynik typu Area, a mnożenie dwóch egzemplarzy typu Area nie powinno być możliwe. Podobnie, dzielenie Area przez Length daje Length, ale dzielenie Area przez Area, lub Length przez Length nie ma sensu. Poza tym, potęga całkowita dla typu Length daje wynik Area jedynie wtedy, gdy wykładnik jest równy 2. Takie niepożądane operacje można, jeżeli trzeba, nadpisać podprogramami podprogramami abstrakcyjnymi (abstract subprograms), takimi jak function "*" (Left, Right : Length) return Length is abstract; Podprogram abstrakcyjny nie ma treści i nie można go wywołać. Każda taka próba jest wykrywana podczas kompilacji. Innym problemem jest czytanie danych typu Length, albo Area, ponieważ wielkości tych typów powinny być nieujemne. Mając powyższe rozważania na uwadze deklarujemy pakiet definicyjny Length_Area postaci: package Length_Area is type Length is new Float; type Area is new Float; function "*" (X : Length; Y : Length) return Length is abstract; function "*" (X : Length; Y : Length) return Area; function "*" (X : Area; Y : Area) return Area is abstract; function "/" (X : Length; Y : Length) return Length is abstract; function "/" (X : Area; Y : Length) return Length; function "/" (X : Area; Y : Area) return Area is abstract; function "**" (X : Length; Y : Integer) return Length is abstract; function "**" (X : Length; Y : Integer) return Area; function "**" (X : Area; Y : Integer) return Area is abstract; procedure Read_Length (X : out Length); 4 Antoni M. Zajączkowski: Algorytmy i podstawy programowania – Operacje podstawowe i typy pochodne 7 czerwca 2015 procedure Read_Area (X : out Area); procedure Write_Length (X : in Length); procedure Write_Area (X : in Area); end Length_Area; W pakiecie implementacyjnym umieszczamy następującą treść funkcji "*", która nie jest abstrakcyjna: function "*" (X : Length; Y : Length) return Area is begin -- Type conversion is necessary return Area(Float(X)*Float(Y)); end "*"; Zadanie 1. 1. Napisać treści funkcji function "/" (X : Area; Y : Length) return Length; function "**" (X : Length; Y : Integer) return Area; Zwrócić uwagę na to, że wykładnik potęgi może być tylko równy 2. Zadanie 2. 2. Napisać treści procedur Read_Length i Read_Area. W tym celu najlepiej skorzystać z procedury Read_Float udostępnianej przez pakiet My_Robust_Input. Zadanie 3. 3. Korzystając z zasobów udostępnianych przez pakiet Length_Area napisać funkcje Triangle_Area i Square_Area, których parametry opisujące podstawę i wysokość trójkąta oraz bok kwadratu są typu Length, a wynik jest typu Area i które obliczają odpowiednio pole trójkąta S△ = ah / 2 i pole kwadratu S □ = b 2 . Jeżeli jest wiele niepożądanych operacji dziedziczonych po typie macierzystym, często lepiej użyć typu prywatnego i zdefiniować te operacje których potrzebujemy. LITERATURA Barnes, J. (2006). Programming in Ada 2005. Addison Wesley, Harlow, England. 5