59. Typy metod: konstruktory i destruktory, selektory, zapytania,
iteratory.
Konstruktor – typ metody charakterystyczny dla programowania obiektowego; jej zadaniem jest
zainicjowanie obiektu danej klasy.
Podczas wywoływania konstruktora wykonywane są następujące zadania:
−
obliczanie rozmiaru obiektu,
−
alokacja obiektu w pamięci,
−
wyczyszczenie (zerowanie) obszaru pamięci przydzielonej obiektowi (nie dotyczy
wszystkich języków programowania),
−
wpisanie do obiektu informacji łączącej go z odpowiadającą mu klasą (połączenie z
metodami klasy) ,
−
wykonanie kodu klasy bazowej (nie dotyczy wszystkich języków programowania),
−
wykonanie kodu wywoływanego konstruktora
Z wyjątkiem ostatniego punktu powyższe zadania są wykonywane wewnętrznie i są wszyte w
kompilator lub interpreter języka, a w niektórych językach stanowią kod klasy bazowej.
Metodę będącą konstruktorem oznacza się różnie, w zależności od języka, np.:
−
w C++, Javie, C#, PHP 4 nazwa konstruktora musi odpowiadać nazwie klasy zawierającej
ten konstruktor,
−
w Pascalu nazwa konstruktora musi być poprzedzona słowem constructor
−
w PHP 5 nazwa konstruktora to __construct
Typy konstruktorów (na przykładzie języka C++)
Konstruktor domyślny
−
konstruktor, który może być wywołany bez podania parametrów; szczególnym przypadkiem
konstruktora domyślnego jest konstruktor, w którym wartości wszystkich parametrów mają
wartości domyślne, w efekcie czego (w C++) można go wywołać bez podawania ich.
Konstruktor zwykły
−
konstruktor, który można wywołać, podając co najmniej jeden parametr. Jest to zwykły
konstruktor stworzony przez twórcę klasy. Jego zadeklarowanie nie powoduje niejawnego
generowania konstruktora domyślnego. Z reguły parametry takiego zwykłego konstruktora
spełniają funkcję inicjalizatorów, które przypisują odpowiednie wartości wewnętrznym zmiennym
tworzonego obiektu.
Konstruktor kopiujący
−
konstruktor, którego jedynym argumentem niedomyślnym jest referencja do obiektu swojej
klasy. Jest on używany niejawnie wtedy, gdy działanie programu wymaga skopiowania obiektu (np.
przy przekazywaniu obiektu do funkcji przez wartość). Gdy konstruktor kopiujący nie został
zdefiniowany, jest on generowany niejawnie (nawet gdy są zdefiniowane inne rodzaje
konstruktorów) i domyślnie powoduje kopiowanie wszystkich składników po kolei.
Konstruktor konwertujący
−
konstruktor, którego jedynym argumentem niedomyślnym jest obiekt dowolnej klasy lub typ
wbudowany. Powoduje niejawną konwersję z typu argumentu na typ klasy własnej konstruktora.
Obiekt konwertowanej klasy musi być przekazywany do funkcji przez wartość. Przekazywanie
przez referencję spowoduje błąd kompilacji z powodu niezgodności typów. Nie zaleca się
stosowania niejawnie takich konwersji. Zmniejszają czytelność kodu oraz mogą spowolnić program
(obiekt do funkcji jest przekazywany przez wartość, co wymusza kopiowanie również dla wywołań
bez konwersji).
Kolejność wywołań konstruktorów
Kolejność wywołań konstruktorów klasy bazowej, czy też obiektów składowych danej klasy, jest
określona kolejnością:
−
konstruktory klas bazowych w kolejności w jakiej znajdują się w sekcji dziedziczenia w
deklaracji klasy pochodnej,
−
konstruktory obiektów składowych klasy w kolejności, w jakiej obiekty te zostały
zadeklarowane w ciele klasy,
−
konstruktor klasy.
Destruktor - w obiektowych językach programowania specjalna metoda, wywoływana przez
program przed usunięciem obiektu i niemal nigdy nie jest wywoływana wprost w kodzie
używającym obiektu. Pod względem funkcjonalnym jest to przeciwieństwo konstruktora.
Destruktor ma za zadanie wykonać czynności przygotowujące obiekt do fizycznego usunięcia. Po
jego wykonaniu obiekt znajduje się w stanie osobliwym i zazwyczaj nie można już z tym obiektem
zrobić nic poza fizycznym usunięciem. Destruktor zwykle wykonuje takie czynności, jak
zamknięcie połączenia z plikiem/gniazdem/potokiem, wyrejestrowanie się z innych obiektów, czasem również zanotowanie faktu usunięcia, a także usunięcie obiektów podległych, które obiekt
utworzył lub zostały mu przydzielone jako podległe (jeśli jest ich jedynym właścicielem) lub
wyrejestrowanie się z jego użytkowania (jeśli jest to obiekt przezeń współdzielony).
W większości języków programowania (np. Object Pascal) destruktor jest dziedziczony jak każda
inna metoda. Wiele obiektów nie musi mieć wcale destruktora, jeżeli poza zwolnieniem pamięci
obiektu nie wymagają innych czynności i takie obiekty nazywamy trywialnie-destruowalnymi
(ang. trivially-destructible). W takiej sytuacji wykorzystywany jest destruktor domyślny, tworzony
automatycznie przez kompilator języka.
Istnienie destruktora i jego konstrukcja zależy od użytego języka programowania; choć w każdym
języku obiekt musi być zniszczony pod koniec swojego życia, nie zawsze jest to oczywiste lub
widoczne dla programisty, w niektórych językach istnieje mechanizm rozpoznawania czy obiekt
jest używany i następuje automatycznie jego usuwanie (np. Java, Python); można stworzyć nawet
bardzo rozbudowane hierarchie klas bez napisania jednego destruktora. Tak nie jest np. w C++,
gdzie zarządzanie pamięcią spoczywa na programiście i większość nietrywialnych klas musi
posiadać jawne destruktory.
Destruktor oznacza się różnie, w zależności od języka, np.:
−
w C++ nazwa destruktora odpowiada nazwie klasy i poprzedzona jest tyldą (~)
−
w Pascalu nazwa destruktora musi być poprzedzona słowem destructor
−
w PHP 5 nazwa destruktora to __destruct
Zapominanie o stosowaniu destruktorów w językach programowania, w których jest to wymagane,
często powoduje wycieki pamięci (program nie zwalnia pamięci, mimo że już z niej nie korzysta)
Iterator - w programowaniu obiektowym jest to obiekt pozwalający na sekwencyjny dostęp do
wszystkich elementów lub części zawartych w innym obiekcie, zwykle kontenerze lub liście.
Iterator jest czasem nazywany kursorem, zwłaszcza w zastosowaniach związanych z bazami
danych.
Iterator można rozumieć jako rodzaj wskaźnika udostępniającego dwie podstawowe operacje:
odwołanie się do konkretnego elementu w kolekcji (dostęp do elementu) oraz modyfikację samego
iteratora tak, by wskazywał na kolejny element (sekwencyjne przeglądanie elementów). Musi także
istnieć sposób utworzenia iteratora tak, by wskazywał na pierwszy element, oraz sposób określenia,
kiedy iterator wyczerpał wszystkie elementy w kolekcji. W zależności od języka i zamierzonego
zastosowania iteratory mogą dostarczać dodatkowych operacji lub posiadać różne zachowania.
Podstawowym celem iteratora jest pozwolić użytkownikowi przetworzyć każdy element w kolekcji
bez konieczności zagłębiania się w jej wewnętrzną strukturę. Pozwala to kolekcji przechowywać
elementy w dowolny sposób, podczas gdy użytkownik może traktować ją jak zwykłą sekwencję lub
listę. Klasa iteratora jest zwykle projektowana wraz z klasą odpowiadającej mu kolekcji i jest z nią
ściśle powiązana. Zwykle to kolekcja dostarcza metod tworzących iteratory.
Niektóre języki obiektowe, np. Perl, Python czy Java posiadają wbudowane mechanizmy iteracji po
elementach kontenera bez wprowadzania jawnego obiektu iteratora. Przejawia się to zwykle
istnieniem operatora w rodzaju " for-each" lub operatora o podobnej funkcjonalności.
Przykłady iteratorów
W C++ iteratory są szeroko wykorzystywane w bibliotece STL. Wszystkie standardowe szablony
kontenerów dostarczają bogatego i spójnego zestawu iteratorów. Składnia standardowych
iteratorów została zaprojektowana tak, by przypominała składnię zwykłej arytmetyki na
wskaźnikach w C, gdzie do wskazania elementu, na który wskazuje iterator używa się operatorów *
i ->, a operatory arytmetyki wskaźnikowej takie, jak ++ przesuwają iterator do następnego
elementu.
Iteratory stosuje się zwykle w parach, gdzie jeden jest używany do właściwej iteracji, zaś drugi
oznacza koniec kolekcji. Iteratory tworzone są przez odpowiadający im kontener standardowymi
metodami, takimi jak begin() i end(). Iterator zwrócony przez begin() wskazuje na
pierwszy element, podczas gdy iterator zwrócony przez end() wskazuje na pozycję za ostatnim
elementem kontenera
Gdy iterator przejdzie za ostatni element, jest on z definicji równy specjalnej wartości iteratora
końcowego.
Od wersji 1.2 interfejs java.util.Iterator umożliwia iterowanie po kolekcjach. Każdy
Iterator posiada metody next() i hasNext(), oraz opcjonalnie może implementować
metodę remove(). Iteratory tworzone są metodą iterator() odpowiedniej klasy kolekcji.
Metoda next() przesuwa iterator i zwraca wartość, na którą wskazuje iterator. Zaraz po
utworzeniu iterator wskazuje na specjalną wartość przed pierwszym elementem, tak by pierwszy
element był pobrany przy pierwszym wywołaniu next(). Do sprawdzenia, czy odwiedzono
wszystkie elementy kolekcji stosuje się metodę hasNext(). Dla kolekcji, które obsługują tę
funkcjonalność, ostatnio odwiedzony element można usunąć z kolekcji metodą remove()
iteratora. Większość innych rodzajów modyfikacji kolekcji podczas iteracji nie gwarantuje
bezpieczeństwa.
Iteratory są jednym z podstawowych elementów Pythona i często są w ogóle niezauważalne, gdyż
są niejawnie wykorzystywane w pętlach for. Wszystkie standardowe typy sekwencyjne w
Pythonie, jak również wiele klas w bibliotece standardowej, udostępniają iterację.
Zapytanie – pojęcie stosowane głównie w ramach interakcji z bazą danych; kwerenda utworzona w
danym języku (zapytań) umożliwiająca znalezienie/wyświetlenie/zmienienie informacji żądanych
przez użytkownika bazy.
Obecnie zapytania do baz danych opierają się głównie na języku SQL (Standard Query Language) –
mimo że między poszczególnymi rodzajami baz danych występują różnice w językach zapytań, to
ogólna ich struktura opiera się właśnie na SQL.
W najprostszym ujęciu zapytanie obejmuje wskazanie tabel, z których są pobierane/zmieniane
dane, i wybranie interesujących użytkownika pól. Tworząc zapytanie można je znacznie
rozbudować i uszczegóławiać, definiując szczegółowe kryteria i określać różnego rodzaju warunki
logiczne, według których zawężany jest zbiór wynikowy zapytania.
Zapytania można podzielić na następujące podstawowe rodzaje:
−
zapytania typu Insert – tworzą one nowe rekordy w wybranej tabeli/tabelach bazy danych
przykład: INSERT INTO TABLE osoby VALUES ('Jan', 'Kowalski')
−
zapytania typu Select – pobierają one dane z wybranej tabeli/tabel bazy danych (dane te są
zazwyczaj zawężane przez zdefiniowane w zapytaniu kryteria)
przykład: SELECT imie FROM osoby WHERE nazwisko='Kowalski'
−
zapytania typu Update – zmieniają one istniejące dane w bazie danych.
przykład: UPDATE osoby SET imie='Adam'
−
zapytania typu Delete – usuwają one istniejące dane z bazy danych.
przykład: DELETE FROM osoby WHERE nazwisko='Kowalski'
−
zapytania typu CREATE – tworzą określone struktury danych – bazę, tabelę, widok...
przykład: CREATE TABLE OSOBY (imię VARCHAR(50), nazwisko VARCHAR(50))
60. Dziedziczenie i dynamiczny polimorfizm
Dziedziczenie (ang. inheritance) to w programowaniu obiektowym operacja polegająca na
stworzeniu nowej klasy na bazie klasy już istniejącej.
Jeżeli w programie wyniknie potrzeba użycia dodatkowej klasy, która różni się od innej klasy
jedynie w kilku szczegółach funkcjonalnych, to dzięki dziedziczeniu nie trzeba tworzyć takiej klasy
od zera, a można zamiast tego wprowadzić jedynie konieczne modyfikacje w stosunku do klasy już
istniejącej.
Klasa, która dziedziczy po klasie bazowej nazywana jest klasą pochodną.
W różnych językach szczegóły dziedziczenia mogą wyglądać odmiennie, np. w CLOS klasa pochodna może wpływać na metody odziedziczone po klasie podstawowej; ogólna zasada
dziedziczenia pozostaje jednak taka sama.
Dziedziczenie wielokrotne (ang. multiple inheritance) nazywane także dziedziczeniem
wielobazowym to operacja polegająca na dziedziczeniu po więcej niż jednej klasie bazowej.
Dziedziczenie wielokrotne stosowane jest na przykład w języku C++. W innych językach
programowania (np. w Javie) dopuszczalne jest wyłącznie dziedziczenie jednokrotne, zaś do
uzyskania efektu, który w C++ osiąga się poprzez dziedziczenie wielokrotne używa się interfejsów.
Zarówno dziedziczenie wielokrotne, jak i interfejsy pozwalają na uzyskanie równoważnego efektu
-- możliwości traktowania obiektu polimorficznie ze względu na wiele, niespokrewnionych ze sobą
typów. Wielodziedziczenie jednakże jest techniką znacznie bardziej niebezpieczną, gdyż w
przeciwieństwie do interfejsów, łączy w sobie środki do współdzielenia implementacji ze środkami
współdzielenia zewnętrznego kontraktu klasy, a zatem dwie funkcje o radykalnie różnych
zastosowaniach. Dlatego też użycie wielokrotnego dziedziczenia wymaga znacznej wiedzy o
mechanizmach języka i ścisłej dyscypliny od stosującego je programisty, w przeciwnym wypadku
bowiem istnieje niebezpieczeństwo stworzenia hierarchii klas, w której zmiana szczegółów
implementacyjnych może pociągnąć za sobą konieczność zmiany kontraktu lub też sytuacji, w
której nie będzie możliwe stworzenie hierarchii o pożądanym kształcie bez wprowadzania
nieprawidłowego zachowania obiektów.
Dla kontrastu, w przypadku użycia interfejsów, czynności dziedziczenia (współdzielenia
implementacji) i dzielenia interfejsu (czyli zewnętrznego kontraktu) są celowo rozdzielone. W ten
sposób nie jest możliwe przypadkowe pomylenie tych dwóch pojęć, co miałoby opłakane skutki.
Argumentem podnoszonym na rzecz wielokrotnego dziedziczenia bywa fakt, że umożliwia ono
proste wykorzystanie istniejącej implementacji z więcej niż jednej klasy bazowej. Jest to prawda,
jednak w rzeczywistości bardzo rzadko ten właśnie efekt jest tym co naprawdę ma zastosowanie w
danej sytuacji, często zaś istnieje fałszywe wrażenie, iż jest to potrzebne. Jeśli istotnie zachodzi
potrzeba wykorzystania więcej niż jednej implementacji, w przypadku użycia interfejsów można
wykorzystać techniki osadzania i delegacji; powoduje to konieczność większej pracy ze strony
programisty, jednak zazwyczaj jest to pożądane, gdyż zmusza do głębszego zastanowienia się nad
pomysłem łączenia niespokrewnionych klas, a dodatkowo powoduje, że nowa klasa zachowuje się
dokładnie tak jak oczekuje tego programista, a nie tak jak stanowi definicja języka (która rzadko
pokrywa się z intuicją w bardziej skomplikowanych obszarach, a wielodziedziczenie należy do
najbardziej skomplikowanych).
Należy również pamiętać, że powyższe argumenty odnoszą się głównie do "tradycyjnych" języków
o statycznym systemie typów, takich jak C++. W innych językach, takich jak Python, również istnieje mechanizm wielodziedziczenia, jednakże konstrukcja systemu typów i mechanizmu klas
jest radykalnie odmienna, co powoduje że powyższa dyskusja traci swoją aktualność.
Polimorfizm - (z gr. wielopostaciowość) to mechanizmy pozwalające programiście używać
wartości, zmiennych i podprogramów na kilka różnych sposobów; inaczej to możliwość
wyabstrahowania wyrażeń od konkretnych typów.
Podczas pisania programu wygodnie jest traktować nawet różne dane w jednolity sposób.
Niezależnie czy należy wydrukować liczbę czy napis, czytelniej (zazwyczaj) jest gdy operacja taka nazywa się po prostu drukuj, a nie drukuj_liczbę i drukuj_napis. Jednak napis musi być drukowany inaczej niż liczba, dlatego będą istniały dwie implementacje polecenia drukuj ale nazwanie ich
wspólną nazwą tworzy wygodny abstrakcyjny interfejs niezależny od typu drukowanej wartości.
Czasami nawet nie trzeba dostarczać różnych implementacji, przykładowo podczas implementacji
stosu nie jest bardzo istotne jakiego typu wartości będą na nim przechowywane. Można napisać
ogólne algorytmy obsługujące stos i ewentualne ukonkretnienie pozostawić systemowi.
Mechanizmy umożliwiające takie udogodnienia nazywane są właśnie polimorfizmem.
Wiele mechanizmów polimorficznych można napisać ręcznie, jednak wiąże się to często z
koniecznością powielania kodu z jedynie niewielkimi poprawkami, a co za tym idzie rozrost kodu
źródłowego i jego zaciemnienie. Istotą polimorfizmu jest to aby to system decydował o
szczegółach, nie programista. Przez system należy tu rozumieć kompilator i system czasu
wykonania. Niektóre decyzje mogą być podjęte już na etapie kompilacji, mamy wtedy do czynienia
z polimorfizmem statycznym (czasu kompilacji). Czasami jednak decyzja musi zostać odwleczona
do momentu wykonywania programu - polimorfizm dynamiczny (czasu wykonania).
Polimorfizm dynamiczny - możliwość dynamicznego (późnego, realizowanego w fazie
wykonania) wiązania nazwy operacji do wielu implementacji (metod) tej operacji w różnych
klasach pozostających w relacji dziedziczenia. Wiązaniu towarzyszy mechanizm wyboru konkretnej
implementacji. Wybór implementacji zależy od nazwy metody oraz od typu dynamicznego tego
obiektu, dla którego została wywołana operacja, a nie od typu zmiennej, wskazującej ten obiekt.
Polimorfizm dynamiczny wiąże się z wirtualizacją metod. W języku takim jak C++ programista
musi sam oznaczyć metodę klasy jako wirtualną, w Javie wszystkie metody są domyślnie wirtualne.
61. Klasy abstrakcyjne
Klasy abstrakcyjne - w programowaniu obiektowym jest to klasa, która nie może mieć swoich
reprezentantów pod postacią obiektów. Stosuje się ją zazwyczaj do zdefiniowania interfejsów.
Zależnie od użytego języka programowania klasy abstrakcyjne tworzy się na różne sposoby.
Idea klasy abstrakcyjnej
Klasa abstrakcyjna jest pewnym uogólnieniem innych klas (na przykład dla występujących w
rzeczywistości obiektów), lecz sama jako taka nie istnieje. Ustalmy, że przez "figurę" będziemy
rozumieć "koło", "kwadrat" lub "trójkąt". Te obiekty matematyczne mogą być reprezentowane przez pewne klasy. Obiekty te posiadają już konkretne właściwości takie jak promień (dla
konkretnego koła) czy długość boku (dla konkretnego kwadratu). Klasy tych obiektów wywodzą się
z pewnej uogólnionej klasy określanej jako po prostu figura. Jednak nie jesteśmy w stanie określić
jaką konstrukcję miałby obiekt klasy figura, ponieważ figura geometryczna jako taka nie istnieje.
Istnieją natomiast wywodzące się od niej klasy koło czy kwadrat. Dodatkowo oczywistym jest, że
figura nie posiada konkretnej wartości pola czy obwodu, jednak już na tym etapie wiemy, że każda
figura tak zdefiniowana (koło, kwadrat czy trójkąt) posiada pole i obwód, które będzie różnie
obliczane dla różnych figur. Dzięki temu figura definiuje pewien interfejs dla klas wywodzących
się od niej.
Każda klasa, która posiada przynajmniej jedną metodę czysto wirtualną jest klasą abstrakcyjną.