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ą.