Menu
Za darmo
Rejestracja
dom  /  PRZEZ/ PHP statyczne pisanie. Dynamiczne pisanie

Typowanie statyczne php. Dynamiczne pisanie

Aby jak najprościej wyjaśnić dwie zupełnie różne technologie, zacznijmy od początku. Pierwszą rzeczą, z którą spotyka się programista podczas pisania kodu, jest deklaracja zmiennych. Możesz zauważyć, że na przykład w języku programowania C++ musisz określić typ zmiennej. Oznacza to, że jeśli deklarujesz zmienną x, musisz dodać int - do przechowywania danych całkowitych, float - do przechowywania danych zmiennoprzecinkowych, char - do danych znakowych i inne dostępne typy. Dlatego język C++ używa typowania statycznego, podobnie jak jego poprzednik, C.

Jak działa pisanie statyczne?

W momencie deklarowania zmiennej kompilator musi wiedzieć, jakich funkcji i parametrów może w odniesieniu do niej użyć, a jakich nie. Dlatego programista musi od razu jednoznacznie wskazać typ zmiennej. Należy również zauważyć, że typ zmiennej nie może zostać zmieniony podczas działania kodu. Ale możesz stworzyć własny typ danych i używać go w przyszłości.

Rozważmy mały przykład. Podczas inicjalizacji zmiennej x (int x;) podajemy identyfikator int - jest to skrót dla którego przechowuje tylko liczby całkowite z zakresu od - 2 147 483 648 do 2 147 483 647. Kompilator rozumie więc, że może wykonać matematyczne wartości tej zmiennej - suma, różnica, mnożenie i dzielenie. Ale na przykład funkcja strcat(), która łączy dwie wartości char, nie może być zastosowana do x. W końcu, jeśli usuniesz ograniczenia i spróbujesz połączyć dwie wartości int metodą symboliczną, wystąpi błąd.

Po co nam języki z dynamicznym pisaniem?

Pomimo pewnych ograniczeń, typowanie statyczne ma wiele zalet i nie powoduje dużego dyskomfortu przy pisaniu algorytmów. Jednak do różnych celów mogą być potrzebne bardziej „luźne zasady” dotyczące typów danych.

Dobrym przykładem jest JavaScript. Ten język programowania jest zwykle używany do osadzania w frameworku w celu uzyskania funkcjonalnego dostępu do obiektów. Dzięki tej funkcji zyskał dużą popularność w technologiach internetowych, gdzie pisanie dynamiczne jest idealne. Czasami pisanie małych skryptów i makr jest uproszczone. Istnieje również zaleta ponownego wykorzystania zmiennych. Ale ta możliwość jest używana dość rzadko, ze względu na możliwe zamieszanie i błędy.

Jaki typ pisania jest najlepszy?

Debata, że ​​pisanie dynamiczne jest lepsze niż pisanie ścisłe, trwa do dziś. Zwykle występują one u wysoko wyspecjalizowanych programistów. Oczywiście twórcy stron internetowych codziennie wykorzystują wszystkie zalety dynamicznego pisania, aby tworzyć wysokiej jakości kod i końcowy produkt programowy. Jednocześnie programiści systemowi, którzy opracowują złożone algorytmy w językach programowania niskiego poziomu, zwykle nie potrzebują takich możliwości, więc wystarczy im statyczne typowanie. Są oczywiście wyjątki od reguły. Na przykład pisanie dynamiczne jest w pełni zaimplementowane w Pythonie.

Dlatego konieczne jest określenie przywództwa danej technologii na podstawie jedynie parametrów wejściowych. Dynamiczne pisanie jest lepsze do tworzenia lekkich i elastycznych platform, podczas gdy silne pisanie jest lepsze do tworzenia masywnej i złożonej architektury.

Podział na typowanie „mocne” i „słabe”.

Wśród zarówno rosyjskojęzycznych, jak i anglojęzycznych materiałów dotyczących programowania można spotkać się z określeniem „silne” pisanie. Nie jest to odrębne pojęcie, a raczej takie pojęcie w ogóle nie istnieje w leksykonie fachowym. Chociaż wielu próbuje interpretować to inaczej. W rzeczywistości „mocne” pisanie należy rozumieć jako takie, które jest dla Ciebie wygodne iz którym pracuje się najwygodniej. „Słaby” system to dla Ciebie system niewygodny i nieefektywny.

Funkcja dynamiczna

Na pewno zauważyłeś, że na etapie pisania kodu kompilator analizuje zapisane konstrukcje i generuje błąd, jeśli typy danych się nie zgadzają. Ale nie JavaScript. Jego wyjątkowość polega na tym, że wykona operację w każdym przypadku. Oto prosty przykład — chcemy dodać znak i liczbę, co nie ma sensu: „x” + 1.

W językach statycznych, w zależności od samego języka, operacja ta może mieć różne konsekwencje. Ale w większości przypadków nie będzie można nawet skompilować, ponieważ kompilator zgłosi błąd natychmiast po napisaniu takiej konstrukcji. Po prostu uzna to za niepoprawne i będzie miał całkowitą rację.

W językach dynamicznych taką operację można wykonać, ale w większości przypadków błąd pojawi się już na etapie wykonywania kodu, ponieważ kompilator nie analizuje typów danych w czasie rzeczywistym i nie może decydować o błędach w tym zakresie. JavaScript jest wyjątkowy, ponieważ wykonuje taką operację i kończy się zestawem nieczytelnych znaków. W przeciwieństwie do innych języków, które po prostu zakończą działanie programu.

Czy możliwe są sąsiednie architektury?

W tej chwili nie ma powiązanej technologii, która mogłaby jednocześnie obsługiwać statyczne i dynamiczne pisanie w językach programowania. I możemy śmiało powiedzieć, że się nie pojawi. Ponieważ architektury różnią się od siebie fundamentalnie i nie mogą być używane jednocześnie.

Niemniej jednak w niektórych językach można zmienić typowanie za pomocą dodatkowych ram.

  • W języku programowania Delphi podsystem Variant.
  • W języku programowania AliceML - pakiety dodatkowe.
  • W języku programowania Haskell biblioteka Data.Dynamic.

Kiedy silne pisanie jest naprawdę lepsze niż pisanie dynamiczne?

Przewagę silnego pisania nad dynamicznym możesz jednoznacznie zaakceptować tylko wtedy, gdy jesteś początkującym programistą. Zgadzają się z tym absolutnie wszyscy specjaliści IT. Ucząc podstawowych i podstawowych umiejętności programowania, lepiej jest używać mocnego pisania, aby uzyskać pewną dyscyplinę podczas pracy ze zmiennymi. Następnie w razie potrzeby możesz przełączyć się na dynamikę, ale umiejętności nabyte przy mocnym pisaniu będą odgrywać ważną rolę. Dowiesz się, jak dokładnie sprawdzać zmienne i uwzględniać ich typy podczas projektowania i pisania kodu.

Korzyści z dynamicznego pisania

  • Minimalizuje ilość znaków i linijek kodu ze względu na zbędną predeklarację zmiennych i określanie ich typu. Typ zostanie określony automatycznie po nadaniu wartości.
  • W małych blokach kodu wizualna i logiczna percepcja konstrukcji jest uproszczona ze względu na brak „dodatkowych” linii deklaracji.
  • Dynamika ma pozytywny wpływ na szybkość kompilatora, ponieważ nie bierze pod uwagę typów i nie sprawdza ich zgodności.
  • Zwiększa elastyczność i pozwala na tworzenie wszechstronnych projektów. Na przykład podczas tworzenia metody, która musi współdziałać z tablicą danych, nie trzeba tworzyć oddzielnych funkcji do pracy z tablicami liczbowymi, tekstowymi i innymi typami tablic. Wystarczy napisać jedną metodę, a będzie działać z dowolnymi typami.
  • Upraszcza wyprowadzanie danych z systemów zarządzania bazami danych, więc pisanie dynamiczne jest aktywnie wykorzystywane w tworzeniu aplikacji internetowych.

Dowiedz się więcej o językach programowania z typowaniem statycznym

  • C++ jest najczęściej używanym językiem programowania ogólnego przeznaczenia. Dziś ma kilka głównych wydań i dużą armię użytkowników. Popularność zyskał dzięki swojej elastyczności, możliwości nieograniczonej rozbudowy oraz obsłudze różnych paradygmatów programowania.

  • Java to język programowania, który wykorzystuje podejście obiektowe. Zyskał popularność dzięki wieloplatformowości. Podczas kompilacji kod jest interpretowany na kod bajtowy, który można wykonać w dowolnym systemie operacyjnym. Język Java i typowanie dynamiczne są niezgodne, ponieważ język jest silnie typowany.

  • Haskell jest również jednym z popularnych języków, którego kod można zintegrować z innymi językami i wchodzić z nimi w interakcje. Ale pomimo takiej elastyczności ma mocne pisanie. Wyposażony w duży wbudowany zestaw typów oraz możliwość tworzenia własnych.

Dowiedz się więcej o językach programowania z dynamicznym pisaniem

  • Python to język programowania, który powstał przede wszystkim w celu ułatwienia pracy programisty. Posiada szereg usprawnień funkcjonalnych, dzięki którym zwiększa czytelność kodu i jego pisanie. Pod wieloma względami osiągnięto to dzięki dynamicznemu typowaniu.

  • PHP to język skryptowy. Szeroko stosowany w tworzeniu stron internetowych, zapewniający interakcję z bazami danych w celu tworzenia interaktywnych, dynamicznych stron internetowych. Dzięki dynamicznemu typowaniu praca z bazami danych jest znacznie ułatwiona.

  • JavaScript to wspomniany już język programowania, który znalazł zastosowanie w technologiach webowych do tworzenia skryptów webowych uruchamianych po stronie klienta. Typowanie dynamiczne służy do ułatwienia pisania kodu, ponieważ zwykle jest on podzielony na małe bloki.

Dynamiczny rodzaj pisania – wady

  • Jeśli podczas używania lub deklarowania zmiennych popełniono literówkę lub błąd, kompilator tego nie wyświetli. Podczas wykonywania programu pojawią się problemy.
  • W przypadku typowania statycznego wszystkie deklaracje zmiennych i funkcji są zwykle umieszczane w osobnym pliku, co ułatwia tworzenie dokumentacji w przyszłości lub nawet wykorzystanie samego pliku jako dokumentacji. W związku z tym pisanie dynamiczne nie pozwala na korzystanie z takiej funkcji.

Podsumować

Typowanie statyczne i dynamiczne służy do zupełnie innych celów. W niektórych przypadkach programiści dążą do korzyści funkcjonalnych, aw innych motywów czysto osobistych. W każdym razie, aby samemu określić rodzaj pisania, musisz dokładnie przestudiować je w praktyce. W przyszłości, podczas tworzenia nowego projektu i wybierania dla niego typowania, odegra to dużą rolę i da zrozumienie skutecznego wyboru.

Prostota pisania w podejściu obiektowym jest konsekwencją prostoty modelu przetwarzania obiektowego. Pomijając szczegóły, możemy powiedzieć, że podczas wykonywania systemu OO występuje tylko jeden rodzaj zdarzenia – wywołanie funkcji:


oznaczający operację F nad obiektem, do którego jest przymocowany X, z przekazaniem argumentu arg(prawdopodobnie wiele argumentów lub żaden). Programiści Smalltalk mówią w tym przypadku o „przekazywaniu obiektu X wiadomości F z argumentem arg”, ale jest to tylko różnica terminologiczna, a zatem nieistotna.

To, że wszystko opiera się na tej Podstawowej Konstrukcji, wyjaśnia częściowo poczucie piękna w pomysłach OO.

Z Podstawowej Konstrukcji wynikają te nietypowe sytuacje, które mogą wystąpić w procesie wykonania:

Definicja: naruszenie typu

Naruszenie typu w czasie wykonywania lub w skrócie naruszenie typu występuje w momencie wywołania. xf(argument), Gdzie X przymocowany do przedmiotu OBJ jeśli:

[X]. nie ma pasującego elementu F i dotyczy OBJ,

[X]. istnieje jednak taki składnik, argument arg dla niego nie do przyjęcia.

Problem z pisaniem polega na unikaniu takich sytuacji:

Problem z typowaniem dla systemów OO

Kiedy stwierdzamy, że w wykonaniu systemu obiektowego może wystąpić naruszenie typu?

Kluczowe słowo to Gdy. Prędzej czy później zdasz sobie sprawę, że doszło do naruszenia typu. Na przykład próba uruchomienia komponentu „Wystrzelenie torpedy” na obiekcie „Pracownik” nie zadziała, a wykonanie zakończy się niepowodzeniem. Jednak może wolisz znaleźć błędy tak wcześnie, jak to możliwe, a nie później.

Typowanie statyczne i dynamiczne

Chociaż możliwe są opcje pośrednie, przedstawiono tutaj dwa główne podejścia:

[X]. Dynamiczne pisanie: poczekaj na zakończenie każdego połączenia, a następnie podejmij decyzję.

[X]. Typowanie statyczne: biorąc pod uwagę zestaw reguł, określ na podstawie tekstu źródłowego, czy podczas wykonywania możliwe są naruszenia typów. System jest wykonywany, jeśli reguły gwarantują brak błędów.

Terminy te są łatwe do wyjaśnienia: przy pisaniu dynamicznym sprawdzanie typu odbywa się podczas działania systemu (dynamicznie), podczas gdy przy pisaniu statycznym sprawdzanie typu jest wykonywane na tekście statycznie (przed wykonaniem).

Typowanie statyczne obejmuje automatyczne sprawdzanie, za które zwykle odpowiada kompilator. W rezultacie mamy prostą definicję:

Definicja: język o typie statycznym

Język obiektowy jest typowany statycznie, jeśli zawiera zestaw spójnych reguł, sprawdzanych przez kompilator, które zapewniają, że wykonanie systemu nie doprowadzi do naruszeń typów.

Termin " mocny pisanie" ( mocny). Odpowiada to ultimatum charakteru definicji, wymagającej całkowitego braku naruszenia typu. Możliwe i słaby (słaby) formy typowania statycznego, w których reguły eliminują pewne naruszenia, nie eliminując ich całkowicie. W tym sensie niektóre języki OO są statycznie słabo typowane. Będziemy walczyć o najsilniejsze typowanie.

W językach z dynamicznym typowaniem, zwanych językami bez typu, nie ma deklaracji typu, a dowolne wartości mogą być dołączane do encji w czasie wykonywania. Statyczne sprawdzanie typów nie jest w nich możliwe.

Zasady pisania

Nasza notacja OO jest typowana statycznie. Jej zasady dotyczące typów zostały wprowadzone na poprzednich wykładach i sprowadzają się do trzech prostych wymagań.

[X]. Deklarując każdą jednostkę lub funkcję, należy określić jej typ, np. konto: KONTO. Każdy podprogram ma 0 lub więcej argumentów formalnych, których typ musi być podany, na przykład: put(x: G; i: LICZBA CAŁKOWITA).

[X]. W każdym zadaniu x:= y i przy każdym wywołaniu podprogramu, w którym y jest faktycznym argumentem dla argumentu formalnego X, rodzaj źródła y musi być zgodny z typem docelowym X. Definicja zgodności opiera się na dziedziczeniu: B kompatybilny z A, jeśli jest jego potomkiem, - uzupełnione regułami dla parametrów ogólnych (patrz Wykład 14).

[X]. Dzwonić xf(argument) wymaga tego F był komponentem klasy bazowej dla typu docelowego X, I F należy wyeksportować do klasy, w której występuje wywołanie (patrz 14.3).

Realizm

Chociaż definicja języka typowanego statycznie jest dość precyzyjna, to jednak nie wystarczy – przy tworzeniu reguł typowania potrzebne są nieformalne kryteria. Rozważmy dwa skrajne przypadki.

[X]. Idealnie poprawny język, w którym każdy system poprawny składniowo jest również poprawny typowo. Reguły deklaracji typu nie są potrzebne. Takie języki istnieją (pomyśl o polskiej notacji dodawania i odejmowania liczb całkowitych). Niestety, żaden prawdziwy język uniwersalny nie spełnia tego kryterium.

[X]. Całkowicie błędny język, który można łatwo utworzyć, biorąc dowolny istniejący język i dodając regułę pisania, która sprawia, że każdy system jest nieprawidłowy. Z definicji ten język jest typowany: ponieważ nie ma systemów zgodnych z regułami, żaden system nie spowoduje naruszeń typów.

Można powiedzieć, że języki pierwszego typu pasować, Ale bezużyteczny, to ostatnie może być przydatne, ale nieodpowiednie.

W praktyce potrzebny jest system typów, który jest jednocześnie użyteczny i użyteczny: na tyle potężny, aby zaspokoić potrzeby obliczeń i na tyle wygodny, aby nie zmuszać nas do komplikacji w celu spełnienia reguł pisania.

Powiemy, że język realistyczny jeśli jest to użyteczne i przydatne w praktyce. W przeciwieństwie do definicji statycznego typowania, która daje stanowczą odpowiedź na pytanie: „ Czy X jest typowany statycznie?", definicja realizmu jest częściowo subiektywna.

W tym wykładzie upewnimy się, że zaproponowana przez nas notacja jest realistyczna.

Pesymizm

Typowanie statyczne prowadzi z natury do „pesymistycznej” polityki. Próba zagwarantowania tego wszystkie obliczenia nie prowadzą do niepowodzeń, odrzuca obliczeń, które mogłyby zakończyć się bezbłędnie.

Rozważ zwykły, nieobiektywny język podobny do Pascala z różnymi typami PRAWDZIWY I LICZBA CAŁKOWITA. Podczas opisywania n: LICZBA CAŁKOWITA; r:Prawdziwe operator n:=r zostaną odrzucone jako niezgodne z regulaminem. W ten sposób kompilator odrzuci wszystkie następujące instrukcje:


Jeśli pozwolimy im działać, zobaczymy, że [A] zawsze będzie działać, ponieważ każdy system liczbowy ma dokładną reprezentację liczby rzeczywistej 0,0, co przekłada się jednoznacznie na liczby całkowite 0. [B] prawie na pewno również zadziała. Wynik działania [C] nie jest oczywisty (czy wynik chcemy uzyskać zaokrąglając czy odrzucając część ułamkową?). [D] wykona zadanie, podobnie jak operator:


jeślin^2< 0 then n:= 3.67 end [E]

który zawiera nieosiągalne zadanie ( n^2 jest kwadratem liczby N). Po wymianie n^2 NA N tylko seria przebiegów da prawidłowy wynik. Zadanie N duża wartość rzeczywista, której nie można przedstawić jako liczba całkowita, spowoduje błąd.

W językach maszynopisanych wszystkie te przykłady (działające, niedziałające, czasem działające) są bezwzględnie traktowane jako naruszenie zasad opisu typów i odrzucane przez każdy kompilator.

Pytanie nie brzmi będziemy czy jesteśmy pesymistami, a tak naprawdę ile możemy sobie pozwolić na pesymizm. Wracając do wymogu realizmu: jeśli reguły typów są tak pesymistyczne, że uniemożliwiają łatwe pisanie obliczeń, odrzucimy je. Ale jeśli osiągnięcie bezpieczeństwa typu zostanie osiągnięte przez niewielką utratę siły wyrazu, zaakceptujemy je. Na przykład w środowisku programistycznym, które zapewnia funkcje zaokrąglania i wyodrębniania części całkowitej - okrągły I ścięty, operatora n:=r uważane za niepoprawne, słusznie, ponieważ zmusza cię do jawnego napisania konwersji liczby rzeczywistej na całkowitą, zamiast używania niejednoznacznych konwersji domyślnych.

Typowanie statyczne: jak i dlaczego

Chociaż zalety pisania statycznego są oczywiste, warto o nich jeszcze raz porozmawiać.

Zalety

Na początku wykładu wymieniliśmy powody stosowania typowania statycznego w technologii obiektowej. To niezawodność, łatwość zrozumienia i skuteczność.

Niezawodność ze względu na wykrycie błędów, które inaczej mogłyby ujawnić się tylko podczas pracy i tylko w niektórych przypadkach. Pierwsza z reguł, która wymusza deklarowanie encji i funkcji, wprowadza redundancję do tekstu programu, co pozwala kompilatorowi za pomocą pozostałych dwóch reguł wykryć niezgodności między zamierzonym a faktycznym użyciem encji, komponentów i wyrażenia.

Wczesne wykrycie błędów jest również ważne, ponieważ im dłużej będziemy zwlekać z ich znalezieniem, tym bardziej wzrośnie koszt naprawy. Ta właściwość, intuicyjnie zrozumiała dla wszystkich profesjonalnych programistów, jest ilościowo potwierdzona przez szeroko znane prace Boehma. Zależność kosztu korekty od czasu znalezienia błędów przedstawia wykres zbudowany na podstawie wielu dużych projektów przemysłowych i eksperymentów przeprowadzonych z małym projektem kontrolowanym:

Ryż. 17.1. Porównawcze koszty naprawy błędów (opublikowane za zgodą)

Czytelność Lub Łatwość zrozumienia(czytelność) ma swoje zalety. We wszystkich przykładach w tej książce pojawienie się typu na obiekcie dostarcza czytelnikowi informacji o jego przeznaczeniu. Czytelność jest niezwykle ważna na etapie konserwacji.

Wreszcie, efektywność potrafi przesądzić o sukcesie lub porażce technologii obiektowej w praktyce. W przypadku braku statycznego pisania podczas wykonywania xf(argument) może to zająć dowolną ilość czasu. Powodem tego jest to, że w czasie wykonywania nie można znaleźć F w podstawowej klasie docelowej X, poszukiwanie będzie kontynuowane w jego potomkach, a to jest pewna droga do nieefektywności. Możesz złagodzić ten problem, usprawniając wyszukiwanie komponentu w hierarchii. Autorzy języka własnego dobra robota, aby wygenerować lepszy kod dla języka o typie dynamicznym. Ale to statyczne pisanie pozwoliło takiemu produktowi OO zbliżyć się lub dorównać wydajnością tradycyjnemu oprogramowaniu.

Kluczem do typowania statycznego jest już stwierdzona idea, że ​​kompilator generuje kod dla konstrukcji xf(argument), zna typ X. Ze względu na polimorfizm nie jest możliwe jednoznaczne określenie odpowiedniej wersji komponentu F. Ale deklaracja zawęża zestaw możliwych typów, pozwalając kompilatorowi na skonstruowanie tabeli, która zapewnia dostęp do poprawnych F przy minimalnych kosztach, z ograniczoną stałą trudność dostępu. Wykonano dodatkowe optymalizacje wiązanie statyczne I podstawienia (inline)- są również ułatwione dzięki statycznemu typowaniu, całkowicie eliminując narzuty tam, gdzie ma to zastosowanie.

Argumenty dla dynamicznego typowania

Mimo to pisanie dynamiczne nie traci swoich zwolenników, zwłaszcza wśród programistów Smalltalka. Ich argumenty opierają się przede wszystkim na omówionym powyżej realizmie. Uważają, że pisanie statyczne jest zbyt restrykcyjne, uniemożliwiając im swobodne wyrażanie kreatywnych pomysłów, czasami nazywając to „pasem cnoty”.

Można zgodzić się z tą argumentacją, ale tylko w przypadku języków o typie statycznym, które nie obsługują wielu funkcji. Warto zauważyć, że wszystkie pojęcia związane z pojęciem typu i wprowadzone na poprzednich wykładach są konieczne – odrzucenie któregokolwiek z nich obarczone jest poważnymi ograniczeniami, a ich wprowadzenie wręcz przeciwnie nadaje elastyczności naszym działaniom i daje nam możliwość pełnego korzystania z praktyczności statycznego pisania.

Typizacja: składniki sukcesu

Jakie są mechanizmy realistycznego pisania statycznego? Wszystkie zostały wprowadzone na poprzednich wykładach, dlatego pozostaje nam tylko pokrótce je przypomnieć. Ich wspólne wyliczenie pokazuje spójność i siłę ich związku.

Nasz system typów jest całkowicie oparty na koncepcji klasa. Klasy to nawet takie podstawowe typy jak LICZBA CAŁKOWITA, a zatem nie potrzebujemy specjalnych reguł do opisywania predefiniowanych typów. (W tym miejscu nasza notacja różni się od języków „hybrydowych”, takich jak Object Pascal, Java i C++, w których system typów starych języków jest połączony z technologią obiektową opartą na klasach.)

Rozszerzone typy daj nam większą elastyczność, zezwalając na typy, których wartości oznaczają obiekty, a także typy, których wartości oznaczają referencje.

Decydujące słowo w tworzeniu elastycznego systemu typów należy do dziedzictwo i związane z nim pojęcie zgodność. Pokonuje to główne ograniczenie klasycznych języków typowanych, na przykład Pascal i Ada, w których operator x:= y wymaga, aby typ X I y był taki sam. Ta reguła jest zbyt surowa: zabrania używania bytów, które mogą oznaczać obiekty powiązanych typów ( KONTO OSZCZĘDNOŚCIOWE I SPRAWDZANIE KONTA). W przypadku dziedziczenia wymagamy jedynie zgodności typów y z typem X, Na przykład, X ma typ KONTO, y- KONTO OSZCZĘDNOŚCIOWE, a druga klasa jest następcą pierwszej.

W praktyce język o typie statycznym wymaga wsparcia wielokrotne dziedziczenie. Znane są podstawowe zarzuty typowania statycznego, że nie daje ono możliwości interpretacji obiektów na różne sposoby. Tak, przedmiot DOKUMENT(dokument) może być przesyłany przez sieć, dlatego wymaga obecności komponentów związanych z typem WIADOMOŚĆ(wiadomość). Ale ta krytyka jest prawdziwa tylko w przypadku języków, które są ograniczone do pojedynczego dziedziczenia.

Ryż. 17.2. Wielokrotne dziedziczenie

Wszechstronność niezbędne na przykład do opisania elastycznych, ale bezpiecznych struktur danych kontenerów (np klasa LISTA[G]...). Bez tego mechanizmu typowanie statyczne wymagałoby zadeklarowania różnych klas dla list z różnymi typami elementów.

W niektórych przypadkach wymagana jest wszechstronność ograniczać, co pozwala na użycie operacji, które mają zastosowanie tylko do jednostek typu ogólnego. Jeśli klasa generyczna SORTABLE_LIST obsługuje sortowanie, wymaga bytów typu G, Gdzie G- parametr ogólny, obecność operacji porównania. Osiąga się to poprzez połączenie z G klasa definiująca ogólne ograniczenie - PORÓWNYWALNY:


klasa SORTABLE_LIST...

Dowolny rzeczywisty parametr ogólny SORTABLE_LIST musi być dzieckiem klasy PORÓWNYWALNY, który ma wymagany składnik.

Kolejnym istotnym mechanizmem jest próba przypisania- organizuje dostęp do obiektów, których oprogramowanie nie kontroluje. Jeśli y jest obiektem bazy danych lub obiektem uzyskanym przez sieć, to instrukcja x ?= y przydzielać X oznaczający y, Jeśli y jest zgodnego typu, a jeśli nie, da X oznaczający Próżnia.

Sprawozdania, skojarzone w ramach idei Design by Contract z klasami i ich składowymi w postaci warunków wstępnych, warunków końcowych i niezmienników klas, umożliwiają opisanie ograniczeń semantycznych, których nie obejmuje specyfikacja typu. Języki takie jak Pascal i Ada mają typy zakresów, które mogą ograniczyć wartość encji na przykład do 10-20, ale nie można ich użyć do wymuszenia wartości I była ujemna, zawsze dwa razy większa J. Na ratunek przychodzą niezmienniki klas, zaprojektowane tak, aby dokładnie odzwierciedlały wprowadzone ograniczenia, bez względu na to, jak bardzo są złożone.

Przypięte reklamy są potrzebne, aby w praktyce uniknąć powielania kodu. ogłaszanie y: jak x, masz gwarancję, że y zmieni się po wszelkich powtarzających się deklaracjach typu X u potomka. W przypadku braku tego mechanizmu programiści nieustannie deklarowaliby ponownie, starając się zachować spójność różnych typów.

Lepkie deklaracje to szczególny przypadek ostatniego potrzebnego nam mechanizmu językowego - kowariancja, co zostanie omówione bardziej szczegółowo później.

Podczas rozwijania się systemy oprogramowania w rzeczywistości potrzebna jest jeszcze jedna właściwość, nieodłącznie związana z samym środowiskiem programistycznym - szybka rekompilacja przyrostowa. Kiedy piszesz lub modyfikujesz system, chcesz jak najszybciej zobaczyć efekt zmian. W przypadku pisania statycznego należy dać kompilatorowi czas na ponowne sprawdzenie typów. Tradycyjne procedury kompilacji wymagają ponownej kompilacji całego systemu (i jej zgromadzenie), a proces ten może być boleśnie długi, zwłaszcza przy przejściu na systemy wielkoskalowe. Zjawisko to stało się argumentem za interpretacja systemy, takie jak wczesne środowiska Lisp lub Smalltalk, które uruchamiały system z niewielkim lub zerowym przetwarzaniem, bez sprawdzania typu. Teraz ten argument jest zapomniany. Dobry nowoczesny kompilator wykrywa, jak kod zmienił się od ostatniej kompilacji i przetwarza tylko znalezione zmiany.

„Czy dziecko jest typizowane”?

Nasz cel - ścisły pisanie statyczne. Dlatego musimy unikać wszelkich luk w naszym „przestrzeganiu zasad”, a przynajmniej dokładnie je identyfikować, jeśli takie istnieją.

Najczęstszą luką w statycznie typowanych językach jest istnienie konwersji, które zmieniają typ encji. W języku C i jego językach pochodnych nazywane są one „rzutowaniem typów” lub rzutowaniem. Nagranie (OTHER_TYPE) x wskazuje, że wartość X jest postrzegany przez kompilator jako posiadający typ OTHER_TYPE, z zastrzeżeniem pewnych ograniczeń dotyczących możliwych typów.

Mechanizmy takie jak ten omijają ograniczenia sprawdzania typów. Rzutowanie jest powszechne w programowaniu C, w tym w dialekcie ANSI C. Nawet w C++ rzutowanie typów, choć mniej powszechne, pozostaje powszechne i być może konieczne.

Przestrzeganie zasad typowania statycznego nie jest takie proste, skoro w każdej chwili można je obejść rzucając.

Wpisywanie i linkowanie

Chociaż, jako czytelnik tej książki, z pewnością odróżnisz typowanie statyczne od statycznego wiążący Cóż, są ludzie, którzy tego nie potrafią. Może to częściowo wynikać z wpływu języka Smalltalk, który opowiada się za dynamicznym podejściem do obu problemów i może prowadzić do błędnego przekonania, że ​​mają one to samo rozwiązanie. (W tej książce argumentujemy, że pożądane jest połączenie typowania statycznego i dynamicznego łączenia w celu tworzenia solidnych i elastycznych programów).

Zarówno typowanie, jak i wiązanie dotyczą semantyki konstrukcji podstawowej xf(argument) ale odpowiedz na dwa różne pytania:

Wpisywanie i linkowanie

[X]. Pytanie o typowanie: kiedy musimy wiedzieć na pewno, że operacja odpowiadająca F Ma zastosowanie do obiektu dołączonego do encji X(z parametrem arg)?

[X]. Kwestia linkowania: Kiedy musimy wiedzieć, jaką operację inicjuje dane wywołanie?

Wpisywanie odpowiada na pytanie o dostępność przynajmniej jeden operacji, wiązanie jest odpowiedzialne za wybór niezbędny.

W ramach podejścia przedmiotowego:

[X]. Problem z pisaniem jest związany z wielopostaciowość: ponieważ XW czasie wykonywania możemy oznaczać obiekty kilku różnych typów, musimy mieć pewność, że operacja reprezentująca F, dostępny w każdym z tych przypadków;

[X]. spowodowany problem z wiązaniem powtarzające się ogłoszenia: ponieważ klasa może zmieniać odziedziczone komponenty, mogą istnieć dwie lub więcej operacji, które twierdzą, że reprezentują F w tym wezwaniu.

Oba problemy można rozwiązać zarówno dynamicznie, jak i statycznie. W istniejące języki Przedstawiono wszystkie cztery rozwiązania.

[X]. Wiele nieobiektywnych języków, takich jak Pascal i Ada, implementuje zarówno typowanie statyczne, jak i statyczne łączenie. Każda jednostka reprezentuje obiekty tylko jednego statycznie zdefiniowanego typu. Zapewnia to niezawodność rozwiązania, której ceną jest jego elastyczność.

[X]. Smalltalk i inne języki OO zawierają dynamiczne łączenie i dynamiczne pisanie. Preferowana jest elastyczność kosztem niezawodności języka.

[X]. Niektóre nieobiektywne języki obsługują dynamiczne pisanie i statyczne łączenie. Wśród nich są języki asemblera i wiele języków skryptowych.

[X]. Idee pisania statycznego i dynamicznego wiązania są zawarte w notacji zaproponowanej w tej książce.

Zwróć uwagę na specyfikę języka C ++, który obsługuje pisanie statyczne, chociaż nie jest ścisłe ze względu na obecność rzutowania typów, wiązanie statyczne (domyślnie), wiązanie dynamiczne, gdy jest wirtualne ( wirtualny) reklamy.

Powód wyboru statycznego typowania i dynamicznego wiązania jest oczywisty. Pierwsze pytanie brzmi: „Kiedy dowiemy się o istnieniu komponentów?” - sugeruje statyczną odpowiedź: " Im wcześniej tym lepiej”, co oznacza: w czasie kompilacji. Drugie pytanie „Którego komponentu użyć?” sugeruje dynamiczną odpowiedź: „ ten, którego potrzebujesz", odpowiadające dynamicznemu typowi obiektu określonemu w czasie wykonywania. Jest to jedyne dopuszczalne rozwiązanie, jeśli wiązanie statyczne i dynamiczne dają różne wyniki.

Poniższy przykład hierarchii dziedziczenia pomoże wyjaśnić te pojęcia:

Ryż. 17.3. Rodzaje samolotów

Rozważ wezwanie:


my_aircraft.lower_landing_gear

Pytanie o wpisywanie: kiedy upewnić się, że komponent będzie tutaj dolny_podwozie(„zwolnij podwozie”), mające zastosowanie do obiektu (np COPTER w ogóle nie będzie) Kwestia oprawy: którą z kilku możliwych wersji wybrać.

Wiązanie statyczne oznaczałoby, że ignorujemy typ dołączonego obiektu i polegamy na deklaracji encji. W rezultacie, mając do czynienia z Boeingiem 747-400, prosilibyśmy o wersję przeznaczoną dla konwencjonalnych samolotów pasażerskich serii 747, a nie o ich modyfikację 747-400. Wiązanie dynamiczne stosuje operację wymaganą przez obiekt i jest to prawidłowe podejście.

Przy statycznym typowaniu kompilator nie odrzuci wywołania, jeśli można zagwarantować, że podczas wykonywania programu do encji mój_samolot zostanie dołączony przedmiot dostarczony wraz z odpowiednim komponentem dolny_podwozie. Podstawowa technika uzyskiwania gwarancji jest prosta: z obowiązkową deklaracją mój_samolot klasa bazowa tego typu musi zawierać taki składnik. Dlatego mój_samolot nie można zadeklarować jako SAMOLOT, ponieważ ten ostatni nie ma dolny_podwozie na tym poziomie; helikoptery, przynajmniej w naszym przykładzie, nie wiedzą, jak wypuścić podwozie. Jeśli zadeklarujemy podmiot jako SAMOLOT, - klasa zawierająca wymagany komponent - wszystko będzie dobrze.

Dynamiczne pisanie w stylu Smalltalk wymaga odczekania na połączenie, aw momencie jego wykonania sprawdzenia obecności żądanego komponentu. Takie zachowanie jest możliwe w przypadku prototypów i eksperymentalnych opracowań, ale niedopuszczalne w systemach przemysłowych - w czasie lotu jest już za późno, aby zapytać, czy masz podwozie.

Kowariancja i ukrywanie dzieci

Gdyby świat był prosty, rozmowa o pisaniu na klawiaturze mogłaby się zakończyć. Zidentyfikowaliśmy cele i korzyści typowania statycznego, zbadaliśmy ograniczenia, jakie muszą spełniać realistyczne systemy typów, i sprawdziliśmy, czy proponowane metody pisania spełniają nasze kryteria.

Ale świat nie jest prosty. Połączenie pisania statycznego z pewnymi wymaganiami inżynierii oprogramowania stwarza bardziej złożone problemy, niż na pierwszy rzut oka. Problemy wynikają z dwóch mechanizmów: kowariancja- zmiana typów parametrów podczas redefinicji, ukrywanie się potomka- zdolność klasy potomnej do ograniczania statusu eksportu dziedziczonych komponentów.

kowariancja

Co dzieje się z argumentami komponentu, gdy jego typ jest ponownie zdefiniowany? Jest to poważny problem i widzieliśmy już wiele jego przykładów: urządzenia i drukarki, listy połączone pojedynczo i podwójnie itd. (zob. sekcje 16.6, 16.7).

Oto kolejny przykład, który pomoże wyjaśnić naturę problemu. I choć daleki od rzeczywistości i metaforyczny, to jego bliskość do schematów programowych jest oczywista. Ponadto analizując go, często będziemy wracać do problemów z praktyki.

Wyobraź sobie uniwersytecką drużynę narciarską przygotowującą się do mistrzostw. Klasa DZIEWCZYNA obejmuje narciarzy, którzy są częścią zespołu kobiet, CHŁOPAK- narciarzy. W rankingu znajduje się wielu uczestników obu drużyn, wykazujących dobre wyniki w poprzednich rozgrywkach. To dla nich ważne, bo teraz to oni będą biec pierwsi, zyskując przewagę nad innymi. (Ta zasada, która uprzywilejowuje już uprzywilejowanych, jest być może tym, co sprawia, że ​​slalom i narciarstwo biegowe są tak atrakcyjne w oczach wielu ludzi, będąc dobrą metaforą samego życia.) Mamy więc dwie nowe klasy: RANKED_DZIEWCZYNA I RANKED_CHŁOPIEC.

Ryż. 17.4. Klasyfikacja narciarzy

Zarezerwowano kilka pokoi dla sportowców: tylko dla mężczyzn, tylko dla dziewcząt, tylko dla zwycięzców. Aby to wyświetlić, używamy równoległej hierarchii klas: POKÓJ, POKÓJ_DZIEWCZYNY I RANKED_DZIEWCZYNKA_POKÓJ.

Oto szkic klasy NARCIARZ:


- Współlokator.
... Inne możliwe składniki pominięte w tej i kolejnych klasach...

Interesują nas dwa komponenty: atrybut współlokator i procedura udział, który „umieszcza” tego narciarza w tym samym pomieszczeniu z obecnym narciarzem:


Podczas deklarowania podmiotu Inny możesz zrezygnować z typu NARCIARZ na korzyść stałego typu jak współlokator(Lub jak prąd Dla współlokator I Inny jednocześnie). Zapomnijmy jednak na chwilę o przypinaniu typów (wrócimy do nich później) i spójrzmy na problem kowariancji w jego oryginalnej postaci.

Jak wprowadzić nadpisywanie typów? Zasady wymagają oddzielnego zamieszkania chłopców i dziewcząt, zwycięzców i innych uczestników. Aby rozwiązać ten problem, podczas redefinicji zmienimy typ komponentu współlokator, jak pokazano poniżej (dalej nadpisane elementy są podkreślone).


- Współlokator.

Przedefiniuj odpowiednio argument procedury udział. Bardziej kompletna wersja klasy wygląda teraz tak:


- Współlokator.
-- Wybierz innego jako sąsiada.

Podobnie powinieneś zmienić wszystkie wygenerowane z NARCIARZ klas (teraz nie używamy ustalania typów). W rezultacie mamy hierarchię:

Ryż. 17,5. Hierarchia członków i redefinicje

Ponieważ dziedziczenie jest specjalizacją, reguły typu wymagają tego, w tym przypadku, przy zastępowaniu wyniku komponentu współlokator, nowy typ był dzieckiem oryginału. To samo dotyczy redefinicji typu argumentu. Inny rutyny udział. Strategia ta, jak wiemy, nazywana jest kowariancją, gdzie przedrostek „ko” wskazuje na łączną zmianę typów parametru i wyniku. Nazywa się odwrotną strategię kontrawariancja.

Wszystkie nasze przykłady przekonująco demonstrują praktyczną konieczność kowariancji.

[X]. Pojedynczo połączony element listy POŁĄCZALNE musi być powiązany z innym podobnym elementem i instancją BI_LINKABLE- z podobnym. Kowariant będzie musiał zostać zastąpiony, a argument w odłóż poprawnie.

[X]. Każdy podprogram w POŁĄCZONA LISTA z argumentem typu POŁĄCZALNE przy przeprowadzce do TWO_WAY_LIST będzie wymagał argumentu BI_LINKABLE.

[X]. Procedura zestaw_alternatywny akceptuje URZĄDZENIE- kłótnia w klasie URZĄDZENIE I DRUKARKA- argument - w klasie DRUKARKA.

Szczególnie popularna jest redefinicja kowariantna, ponieważ ukrywanie informacji prowadzi do tworzenia procedur formularza


-- Ustaw atrybut na v.

pracować z atrybut typ JAKIŚ_TYP. Takie procedury są oczywiście kowariantne, ponieważ każda klasa, która zmienia typ atrybutu, musi odpowiednio przedefiniować argument. ustaw_atrybut. Chociaż przedstawione przykłady mieszczą się w jednym schemacie, kowariancja jest znacznie bardziej rozpowszechniona. Pomyśl na przykład o procedurze lub funkcji wykonującej łączenie pojedynczo połączonych list ( POŁĄCZONA LISTA). Jej argument musi zostać przedefiniowany jako podwójnie połączona lista ( TWO_WAY_LIST). Uniwersalna operacja dodawania wrostek „+” akceptuje LICZBOWE- kłótnia w klasie LICZBOWE, PRAWDZIWY- w klasie PRAWDZIWY I LICZBA CAŁKOWITA- w klasie LICZBA CAŁKOWITA. W równoległych hierarchiach usług telefonicznych procedura początek w klasie PHONE_SERVICE może być wymagany argument ADRES, reprezentujący adres abonenta (do fakturowania), podczas gdy ta sama procedura w klasie USŁUGI KORPORACYJNE wymagany argument typu CORPORATE_ADDRESS.

Ryż. 17.6. Usługi komunikacyjne

Co można powiedzieć o rozwiązaniu kontrawariantnym? W przykładzie z narciarzem oznaczałoby to, że jeśli przechodzi do klasy RANKED_DZIEWCZYNA, typ wyniku współlokator przedefiniowane jako RANKED_DZIEWCZYNA, a następnie, ze względu na kontrawariancję, typ argumentu udział można przedefiniować na typ DZIEWCZYNA Lub NARCIARZ. Jedynym typem, który nie jest dozwolony w rozwiązaniu kontrawariantnym, jest RANKED_DZIEWCZYNA! Wystarczająco, by wzbudzić najgorsze podejrzenia u rodziców dziewczynek.

Hierarchie równoległe

Aby nie pozostawić kamienia nieodwróconego, rozważ wariant przykładu NARCIARZ z dwiema równoległymi hierarchiami. Pozwoli nam to zasymulować sytuację, z którą spotkaliśmy się już w praktyce: TWO_WAY_LIST > LINKED_LIST I BI_LINKABLE > LINKABLE; lub hierarchia z usługą telefoniczną PHONE_SERVICE.

Niech będzie hierarchia z klasą POKÓJ, którego potomkiem jest POKÓJ_DZIEWCZYNY(Klasa CHŁOPAK pominięty):

Ryż. 17.7. Narciarze i pokoje

Nasze klasy narciarzy w tej równoległej hierarchii zamiast współlokator I udział będzie miał podobne elementy zakwaterowanie (zakwaterowanie) I pomieścić (miejsce):


opis: "Nowy wariant z równoległymi hierarchiami"
pomieścić (r: POKÓJ) to… wymagać… zrobić

Tutaj również potrzebne są nadpisania kowariantne: w klasie DZIEWCZYNA 1 Jak zakwaterowanie i argument podprogramu pomieścić należy zastąpić typem POKÓJ_DZIEWCZYNY, w klasie CHŁOPIEC 1- typ POKÓJ_CHŁOPIEC itp. (Pamiętaj, że nadal pracujemy bez przypinania typu). Podobnie jak w poprzedniej wersji przykładu, kontrawariancja jest tutaj bezużyteczna.

Skrajność polimorfizmu

Czy mało jest przykładów potwierdzających praktyczność kowariancji? Dlaczego ktokolwiek miałby rozważać kontrawariancję, która jest sprzeczna z tym, co jest potrzebne w praktyce (oprócz zachowania niektórych młodych ludzi)? Aby to zrozumieć, rozważ problemy, które pojawiają się podczas łączenia polimorfizmu i strategii kowariancji. Wymyślenie planu sabotażu nie jest trudne i być może sam już go stworzyłeś:


stworzyć b; utwórz g;-- Utwórz obiekty BOY i GIRL.

Wynik ostatniego wywołania, całkiem prawdopodobnie zadowalający młodzież, jest dokładnie tym, czemu staraliśmy się zapobiec zastępując typ. Dzwonić udział prowadzi do tego, że przedmiot CHŁOPAK, znany jako B i dzięki polimorfizmowi otrzymał alias S typ NARCIARZ, staje się sąsiadem obiektu DZIEWCZYNA znany z imienia G. Jednak wezwanie, choć sprzeczne z regulaminem hostelu, jest w tekście programu całkiem poprawne, ponieważ udział-eksportowany składnik w składzie NARCIARZ, A DZIEWCZYNA, typ argumentu G, kompatybilny z NARCIARZ, typ parametru formalnego udział.

Równoległy schemat hierarchii jest równie prosty: zastąp NARCIARZ NA NARCIARZ1, wyzwanie udział- na dyżurze s. zakwaterowanie (gr), Gdzie gr- podmiot typu POKÓJ_DZIEWCZYNY. Wynik jest taki sam.

Przy kontrawariantnym rozwiązaniu tych problemów nie byłoby: specjalizacji celu połączenia (w naszym przykładzie S) wymagałoby uogólnienia argumentacji. W rezultacie kontrawariancja prowadzi do prostszego modelu matematycznego mechanizmu: dziedziczenie - redefinicja - polimorfizm. Fakt ten jest opisany w wielu artykułach teoretycznych proponujących tę strategię. Argument nie jest zbyt przekonujący, ponieważ, jak pokazują nasze przykłady i inne publikacje, kontrawariancja nie ma praktycznego zastosowania.

Dlatego nie próbując naciągać kowariantnego ciała w kontrawariantne ubranie, należy zaakceptować kowariantną rzeczywistość i szukać sposobów na wyeliminowanie niepożądanego efektu.

Ukrywanie się przez potomka

Zanim poszukamy rozwiązania problemu kowariancji, rozważmy inny mechanizm, który może prowadzić do naruszeń typów w warunkach polimorfizmu. Ukrywanie potomków to zdolność klasy do niewyeksportowania komponentu otrzymanego od rodziców.

Ryż. 17.8. Ukrywanie się przez potomka

Typowym przykładem jest komponent add_vertex(dodaj wierzchołek) wyeksportowany przez klasę WIELOKĄT, ale ukryty przez swojego potomka PROSTOKĄT(ze względu na możliwe naruszenie niezmiennika - klasa chce pozostać prostokątem):


Przykład nieprogramistyczny: klasa „Struś” ukrywa metodę „Fly”, otrzymaną od rodzica „Ptak”.

Przyjmijmy na chwilę ten schemat i zapytajmy, czy połączenie dziedziczenia i ukrywania byłoby uzasadnione. Modelująca rola ukrywania się, podobnie jak kowariancja, jest naruszana przez sztuczki, które są możliwe dzięki polimorfizmowi. I tutaj nietrudno zbudować złośliwy przykład, który pozwala mimo ukrycia komponentu wywołać go i dodać wierzchołek do prostokąta:


twórca; -- Utwórz obiekt PROSTOKĄT.
p:=r; -- Przypisanie polimorficzne.

Od obiektu R ukrywając się pod esencją P klasa WIELOKĄT, A add_vertex wyeksportowany składnik WIELOKĄT, a następnie jego wywołanie przez jednostkę P prawidłowy. W wyniku wykonania w prostokącie pojawi się jeszcze jeden wierzchołek, co oznacza, że ​​zostanie utworzony nieprawidłowy obiekt.

Poprawność systemów i klas

Aby omówić problemy kowariancji i ukrywania potomków, potrzebujemy kilku nowych terminów. Zadzwonimy class-correct (klasa-poprawna) system spełniający trzy zasady opisu typów podane na początku wykładu. Przypomnij sobie: każda jednostka ma swój własny typ; rodzaj argumentu rzeczywistego musi być zgodny z typem argumentu formalnego, podobnie sytuacja wygląda z przypisaniem; wywoływany komponent musi być zadeklarowany w swojej klasie i wyeksportowany do klasy zawierającej wywołanie.

System nazywa się poprawne systemowo (poprawne systemowo), jeśli podczas jego wykonywania nie wystąpi żadne naruszenie typu.

W idealnym przypadku obie koncepcje powinny do siebie pasować. Jednak widzieliśmy już, że klasowo poprawny system w warunkach dziedziczenia, kowariancji i ukrywania przez potomka może nie być systemowo poprawny. Nazwijmy ten błąd naruszenie poprawności systemu (błąd ważności systemu).

Aspekt praktyczny

Prostota problemu tworzy swego rodzaju paradoks: dociekliwy początkujący może skonstruować kontrprzykład w ciągu kilku minut, w praktyce błędy poprawności klasowej systemów zdarzają się dzień po dniu, ale naruszenia poprawności systemowej, nawet w dużych, projektów rocznych, występują niezwykle rzadko.

Nie pozwala nam to jednak ich zignorować, dlatego zaczynamy badać trzy możliwe sposoby rozwiązania tego problemu.

Następnie dotkniemy bardzo subtelnych i niezbyt często manifestujących się aspektów podejścia przedmiotowego. Jeśli czytasz tę książkę po raz pierwszy, możesz pominąć pozostałe części tego wykładu. Jeśli jesteś nowicjuszem w technologii OO, lepiej zrozumiesz ten materiał po przestudiowaniu wykładów 1-11 kursu „Podstawy projektowania obiektowego”, poświęconych metodologii dziedziczenia, a w szczególności wykładu 6 kursu „Podstawy projektowania Object-Oriented Design”, poświęcone dziedziczeniu metodologii.

Poprawność systemów: pierwsze przybliżenie

Skupmy się najpierw na problemie kowariancji, ważniejszym z nich. Istnieje obszerna literatura poświęcona temu zagadnieniu, oferująca szereg różnych rozwiązań.

Kontrawariancja i niezmienniczość

Kontrawariancja eliminuje teoretyczne problemy związane z naruszeniem poprawności systemu. Jednak traci to realizm systemu typów, z tego powodu nie ma potrzeby dalszego rozważania tego podejścia.

Oryginalność języka C++ polega na tym, że używa on strategii nowariancja, uniemożliwiając zmianę typu argumentów w nadpisanych podprogramach! Gdyby C++ był językiem silnie typowanym, jego system typów byłby trudny w użyciu. Najprostszym rozwiązaniem problemu w tym języku, a także ominięciem innych ograniczeń C++ (powiedzmy braku ograniczonej uniwersalności), jest zastosowanie rzutowania - rzutowania typów, które pozwala całkowicie zignorować istniejący mechanizm pisania. Rozwiązanie to nie wydaje się atrakcyjne. Należy jednak zauważyć, że wiele omówionych poniżej propozycji będzie opierać się na braku wariancji, której znaczenie zostanie nadane przez wprowadzenie nowych mechanizmów pracy z typami zamiast redefinicji kowariantnej.

Korzystanie z parametrów ogólnych

Uniwersalność leży u podstaw interesującej idei zaproponowanej po raz pierwszy przez Franza Webera. Zadeklarujmy klasę NARCIARZ1, ograniczając generyczność parametrów ogólnych do klasy POKÓJ:


cecha klasy SKIER1
pomieścić (r: G) to ... wymagać ... zrobić akomodację:= r koniec

Potem klasa DZIEWCZYNA 1 będzie spadkobiercą NARCIARZ1 itd. Ta sama technika, jakkolwiek dziwna może się wydawać na pierwszy rzut oka, może być zastosowana w przypadku braku równoległej hierarchii: klasa NARCIARZ.

Takie podejście rozwiązuje problem kowariancji. Każde użycie klasy musi określać rzeczywisty parametr ogólny POKÓJ Lub POKÓJ_DZIEWCZYNY, tak że zła kombinacja staje się po prostu niemożliwa. Język staje się bezwariantowy, a system w pełni zaspokaja potrzeby kowariancji dzięki parametrom generycznym.

Niestety ta technika jest nie do zaakceptowania jako rozwiązanie ogólne, ponieważ prowadzi do rosnącej listy ogólnych parametrów, po jednym dla każdego typu możliwego argumentu kowariantnego. Co gorsza, dodanie podprogramu kowariantnego z argumentem, którego typu nie ma na liście, wymagałoby dodania ogólnego parametru klasy, a zatem zmieniłoby interfejs klasy, powodując zmianę wszystkich klientów klasy, co jest niedopuszczalne.

Wpisz zmienne

Wielu autorów, w tym Kim Bruce, David Shang i Tony Simons, wymyśliło rozwiązanie oparte na zmiennych typu, których wartości są typami. Ich pomysł jest prosty:

[X]. zamiast nadpisań kowariantnych zezwól na deklaracje typu, które używają zmiennych typu;

[X]. rozszerzyć reguły zgodności typów, aby zarządzać takimi zmiennymi;

[X]. zapewniają możliwość przypisania zmiennych typu jako wartości do typów językowych.

Czytelnicy mogą znaleźć szczegółowe przedstawienie tych idei w wielu artykułach na ten temat, a także w publikacjach Cardelli (Cardelli), Castagna (Castagna), Webera (Weber) i innych. Nie zajmiemy się tym problemem, a oto dlaczego.

[X]. Prawidłowo zaimplementowany mechanizm zmiennej typu mieści się w kategorii pozwalającej na użycie typu bez jego pełnego określenia. Ta sama kategoria obejmuje wszechstronność i zakotwiczenie reklam. Mechanizm ten mógłby zastąpić inne mechanizmy z tej kategorii. Na początku można to interpretować na korzyść zmiennych typu, ale wynik może być katastrofalny, ponieważ nie jest jasne, czy ten kompleksowy mechanizm poradzi sobie ze wszystkimi zadaniami z łatwością i prostotą, która jest nieodłączną częścią ogólności i przypinania typu.

[X]. Załóżmy, że opracowano mechanizm zmiennej typu, który może przezwyciężyć problemy łączenia kowariancji i polimorfizmu (nadal ignorując problem ukrywania potomka). Wtedy twórca klasy będzie potrzebował niezwykła intuicja aby z góry zdecydować, które ze składowych będą dostępne do redefinicji typów w klasach pochodnych, a które nie. Poniżej omówimy ten problem, który ma miejsce w praktyce tworzenia programów i, niestety, poddaje w wątpliwość stosowalność wielu schematów teoretycznych.

To zmusza nas do powrotu do rozważanych już mechanizmów: ograniczonej i nieograniczonej uniwersalności, przypinania typów i oczywiście dziedziczenia.

Poleganie na przypinaniu typów

Prawie gotowe rozwiązanie problemu kowariancji znajdziemy patrząc na znany nam mechanizm przypiętych deklaracji.

Podczas opisywania klas NARCIARZ I NARCIARZ1 nie mogłeś się powstrzymać od chęci pozbycia się za pomocą ustalonych zapowiedzi wielu redefinicji. Przypinanie jest typowym mechanizmem kowariantnym. Oto jak wyglądałby nasz przykład (wszystkie zmiany są podkreślone):


udostępnij (inne: jak Bieżący) to… wymagaj… zrób
pomieścić (r: jak zakwaterowanie) to… wymagać… robić

Teraz dzieci mogą opuścić klasę NARCIARZ bez zmian i NARCIARZ1 będą musieli tylko zastąpić atrybut zakwaterowanie. Przypięte elementy: atrybut współlokator i podprogramowe argumenty udział I pomieścić- zmieni się automatycznie. To znacznie upraszcza pracę i potwierdza fakt, że przy braku zakotwiczenia (lub innego podobnego mechanizmu, takiego jak zmienne typu), niemożliwe jest napisanie produktu obiektowego z realistycznym typowaniem.

Ale czy udało Ci się wyeliminować naruszenia poprawności systemu? NIE! Możemy, tak jak poprzednio, przechytrzyć sprawdzanie typu, wykonując przypisania polimorficzne, które powodują naruszenia poprawności systemu.

To prawda, że ​​\u200b\u200boryginalne wersje przykładów zostaną odrzucone. Zostawiać:


utwórz b; utwórz g;-- Utwórz obiekty CHŁOPIEC i DZIEWCZYNA.
s:=b; -- Przypisanie polimorficzne.

Argument G przekazywane udział, jest teraz niepoprawne, ponieważ wymaga obiektu typu lubi i klasa DZIEWCZYNA nie jest zgodny z tym typem, ponieważ zgodnie z regułą typów stałych żaden typ nie jest zgodny lubi chyba dla siebie.

Jednak nie cieszymy się długo. Z drugiej strony, ten przepis tak mówi lubi zgodny z typem S. Tak więc, stosując polimorfizm nie tylko obiektu S, ale także parametr G, możemy ponownie ominąć system sprawdzania typu:


s: NARCIARZ; b:CHŁOPIEC; g: lubi; rzeczywisty_g:DZIEWCZYNA;
stworzyć b; utwórz rzeczywiste_g — Utwórz obiekty CHŁOPIEC i DZIEWCZYNA.
s:= rzeczywista_g; g:= s -- Dołącz g do DZIEWCZYNY przez s.
s:= b — Przypisanie polimorficzne.

W rezultacie nielegalne połączenie zostaje zrealizowane.

Jest wyjście. Jeśli poważnie myślimy o używaniu przypinania deklaracji jako jedynego mechanizmu kowariancji, to możemy pozbyć się naruszeń systemowej poprawności poprzez całkowite zakazanie polimorfizmu przypiętych jednostek. Będzie to wymagało zmiany języka: wprowadzenia nowego słowa kluczowego kotwica(potrzebujemy tej hipotetycznej konstrukcji wyłącznie po to, aby użyć jej w tej dyskusji):


Dopuśćmy deklaracje typu lubi tylko kiedy S opisany jako kotwica. Zmieńmy reguły kompatybilności, aby zapewnić: S i elementy typu lubi mogą być dołączane (w przypisaniach lub przekazywaniu argumentów) tylko do siebie.

Dzięki takiemu podejściu usuwamy z języka możliwość redefinicji typu dowolnych argumentów podprogramowych. Dodatkowo moglibyśmy zabronić ponownego definiowania typu wyniku, ale nie jest to konieczne. Możliwość nadpisania typu atrybutu jest oczywiście zachowana. Wszystko redefinicje typów argumentów będą teraz wykonywane niejawnie poprzez mechanizm przypinania uruchamiany przez kowariancję. Gdzie, przy poprzednim podejściu, klasa D nadpisał odziedziczony komponent jako:


podczas gdy klasa C- rodzic D wyglądało


Gdzie Y korespondował X, teraz redefiniując komponent R będzie wyglądać tak:


Pozostaje w klasie D przesłonić typ twoja_kotwica.

To rozwiązanie problemu kowariancji - polimorfizmu będziemy nazywać podejściem Zakotwiczenie. Dokładniej byłoby powiedzieć: „Kowariancja tylko przez przypinanie”. Właściwości podejścia są atrakcyjne:

[X]. Przypinanie opiera się na idei ścisłej separacji kowariantny i potencjalnie polimorficzne (lub w skrócie polimorficzne) elementy. Wszystkie podmioty zadeklarowane jako kotwica Lub jak jakaś_kotwica kowariant; inne są polimorficzne. W każdej z dwóch kategorii dozwolone są wszelkie załączniki, ale nie ma bytu ani wyrażenia, które naruszałoby granicę. Nie można na przykład przypisać źródła polimorficznego do kowariantnego celu.

[X]. To proste i eleganckie rozwiązanie jest łatwe do wyjaśnienia nawet dla początkujących.

[X]. Całkowicie eliminuje to możliwość naruszenia poprawności systemowej w systemach konstruowanych kowariantnie.

[X]. Zachowuje ramy pojęciowe określone powyżej, w tym koncepcje ograniczonej i nieograniczonej uniwersalności. (W rezultacie to rozwiązanie, moim zdaniem, jest lepsze niż typowanie zmiennych, które zastępują mechanizmy kowariancji i uniwersalności, zaprojektowane do rozwiązywania różnych praktycznych problemów.)

[X]. Wymaga niewielkiej zmiany językowej - dodania pojedynczego słowa kluczowego odzwierciedlonego w regule dopasowania - i nie wiąże się z żadnymi odczuwalnymi trudnościami implementacyjnymi.

[X]. Jest to realistyczne (przynajmniej w teorii): każdy wcześniej możliwy system można przepisać, zastępując nadpisania kowariantne zakotwiczonymi ponownymi deklaracjami. To prawda, że ​​w rezultacie niektóre załączniki staną się nieważne, ale odpowiadają przypadkom, które mogą prowadzić do naruszeń typów, dlatego należy je zastąpić próbami przypisania i zająć się nimi w czasie wykonywania.

Wydawać by się mogło, że na tym dyskusja może się zakończyć. Dlaczego więc podejście kotwiczące nie do końca nam odpowiada? Po pierwsze, nie poruszyliśmy jeszcze kwestii ukrywania się dzieci. Ponadto głównym powodem kontynuowania dyskusji jest problem wyrażony już w krótkiej wzmiance o zmiennych typu. Podział stref wpływów na część polimorficzną i kowariantną jest nieco podobny do wyniku konferencji jałtańskiej. Zakłada ona, że ​​projektant klasy ma niezwykłą intuicję, że jest w stanie dla każdego wprowadzanego przez siebie bytu, w szczególności dla każdego argumentu, wybrać raz na zawsze jedną z dwóch możliwości:

[X]. Jednostka jest potencjalnie polimorficzna: teraz lub później (przez przekazanie parametrów lub przypisanie) może być dołączona do obiektu, którego typ różni się od zadeklarowanego. Oryginalny typ jednostki nie może zostać zmieniony przez żadnego potomka klasy.

[X]. Jednostka jest podmiotem przesłaniania typu, co oznacza, że ​​jest albo zakotwiczona, albo sama jest osią.

Ale jak programista może to wszystko przewidzieć? Cała atrakcyjność metody OO, wyrażona na wiele sposobów w zasadzie Open-Closed, wynika właśnie z możliwości zmian, jakie mamy prawo wprowadzić do wcześniej wykonanej pracy, jak również z faktu, że twórca uniwersalnych rozwiązań Nie musi mieć nieskończoną mądrość, rozumiejąc, jak jego produkt może być dostosowany do ich potrzeb przez potomków.

Przy takim podejściu redefinicja typów i ukrywanie przez potomków jest rodzajem „zaworu bezpieczeństwa”, który umożliwia ponowne wykorzystanie istniejącej klasy, która jest prawie odpowiednia dla naszych celów:

[X]. Odwołując się do redefinicji typu, możemy zmienić deklaracje w klasie pochodnej bez wpływu na oryginał. W takim przypadku rozwiązanie czysto kowariantne będzie wymagało edycji oryginału przez opisane przekształcenia.

[X]. Ukrywanie przez potomka chroni przed wieloma niepowodzeniami podczas tworzenia klasy. Można krytykować projekt, w którym PROSTOKĄT, wykorzystując fakt, że jest potomkiem WIELOKĄT, próbuje dodać wierzchołek. Zamiast tego można by zaproponować strukturę dziedziczenia, w której figury o ustalonej liczbie wierzchołków są oddzielone od wszystkich innych i problem by nie powstał. Jednak podczas projektowania struktur dziedziczenia zawsze lepiej jest mieć te, które ich nie mają wyjątki taksonomiczne. Ale czy można je całkowicie wyeliminować? Omawiając ograniczenia eksportowe w jednym z kolejnych wykładów, przekonamy się, że nie jest to możliwe z dwóch powodów. Pierwszym z nich jest obecność konkurencyjnych kryteriów klasyfikacji. Po drugie, prawdopodobieństwo, że deweloper nie znajdzie idealnego rozwiązania, nawet jeśli takie istnieje.

Analiza globalna

Ta część poświęcona jest opisowi podejścia pośredniego. Główne praktyczne rozwiązania przedstawiono na wykładzie 17 .

Badając opcję przypinania, zauważyliśmy, że jej główną ideą było oddzielenie kowariantnych i polimorficznych zestawów jednostek. Tak więc, jeśli weźmiemy dwie instrukcje formularza


każdy z nich jest przykładem poprawnego zastosowania ważnych mechanizmów OO: pierwszy – polimorfizm, drugi – redefinicja typu. Problemy zaczynają się przy łączeniu ich dla tego samego podmiotu S. Podobnie:


problemy zaczynają się od połączenia dwóch niezależnych i zupełnie niewinnych operatorów.

Błędne wywołania prowadzą do naruszeń typu. W pierwszym przykładzie przypisanie polimorficzne dołącza obiekt CHŁOPAK do esencji S, co on robi G błędny argument udział, ponieważ jest powiązany z obiektem DZIEWCZYNA. W drugim przykładzie do encji R przedmiot jest dołączony PROSTOKĄT, co wyklucza add_vertex z eksportowanych komponentów.

Oto idea nowego rozwiązania: z góry – statycznie, sprawdzając typy przez kompilator lub inne narzędzia – definiujemy skład każdą jednostkę, w tym typy obiektów, z którymi jednostka może być powiązana w czasie wykonywania. Następnie, ponownie statycznie, upewniamy się, że każde wywołanie jest poprawne dla każdego elementu typu docelowego i zestawów argumentów.

W naszych przykładach operator s:=b wskazuje, że klasa CHŁOPAK należy do zbioru typów dla S(ponieważ w wyniku wykonania instrukcji create stworzyć b należy do zbioru typów dla B). DZIEWCZYNA, ze względu na obecność instrukcji stworzyć g, należy do zbioru typów dla G. Ale potem wyzwanie udział będzie nieprawidłowy dla celu S typ CHŁOPAK i argumentacja G typ DZIEWCZYNA. podobnie PROSTOKĄT jest w typie ustawionym dla P, co wynika jednak z przypisania polimorficznego wywołania add_vertex Dla P typ PROSTOKĄT będzie nieważny.

Te obserwacje skłaniają nas do myślenia o stworzeniu światowy podejście oparte na nowej zasadzie pisania:

Reguła poprawności systemu

Dzwonić xf(argument) jest poprawna systemowo wtedy i tylko wtedy, gdy jest poprawna klasowo dla X, I arg, które mają dowolne typy z odpowiednich zestawów typów.

W tej definicji wywołanie jest uważane za poprawne klasowo, jeśli nie narusza reguły wywołania komponentu, która mówi: jeśli C istnieje klasa bazowa, np X, część F należy wyeksportować C i typ arg musi być zgodny z typem parametru formalnego F. (Pamiętaj, dla uproszczenia zakładamy, że każdy podprogram ma tylko jeden parametr, ale rozszerzenie reguły na dowolną liczbę argumentów nie jest trudne).

Poprawność wywołań systemowych sprowadza się do poprawności klasowej, z tym wyjątkiem, że nie jest sprawdzana poszczególne elementy, ale dla dowolnych par ze zbiorów zbiorów. Oto podstawowe zasady tworzenia zestawu typów dla każdej encji:

1 Dla każdej jednostki początkowy zestaw typów jest pusty.

2 Po spełnieniu innej instrukcji formularza utwórz (SOME_TYPE) a, dodać JAKIŚ_TYP do zestawu typów dla A. (Dla uproszczenia założymy, że dowolna instrukcja Stwórz zostanie zastąpiony instrukcją utwórz (ATYP) a, Gdzie TYP- typ encji A.)

3 Napotkanie innego przypisania formy a:=b, dodaj do zestawu typów dla A B.

4 Jeśli A jest parametrem formalnym podprogramu standardowego, a więc po spełnieniu następnego wywołania z parametrem rzeczywistym B, dodaj do zestawu typów dla A wszystkie elementy zestawu typów dla B.

5 Będziemy powtarzać kroki (3) i (4), aż zestawy typów przestaną się zmieniać.

Sformułowanie to nie uwzględnia mechanizmu uniwersalności, jednak w miarę potrzeb możliwe jest rozszerzenie reguły bez szczególnych problemów. Krok (5) jest konieczny ze względu na możliwość powstawania łańcuchów przydziałów i transferów (z B Do A, z C Do B itp.). Łatwo zrozumieć, że po skończonej liczbie kroków proces ten zostanie zatrzymany.

Jak zapewne zauważyłeś, reguła nie uwzględnia sekwencji instrukcji. Gdy


utwórz(TYP1) t; s:=t; utwórz (TYP2) t

do zestawu typów dla S wpisz jako TYP 1, I TYP2, Chociaż S, biorąc pod uwagę sekwencję instrukcji, jest w stanie przyjmować tylko wartości pierwszego typu. Uwzględnienie lokalizacji instrukcji będzie wymagało od kompilatora głębokiej analizy strumienia instrukcji, co doprowadzi do nadmiernego wzrostu poziomu złożoności algorytmu. Zamiast tego obowiązują bardziej pesymistyczne zasady: sekwencja operacji:


zostaną uznane za systemowo nieprawidłowe, mimo że kolejność ich wykonania nie prowadzi do naruszenia typu.

Globalna analiza systemu została (bardziej szczegółowo) przedstawiona w 22. rozdziale monografii. Jednocześnie rozwiązano zarówno problem kowariancji, jak i problem ograniczeń eksportowych podczas dziedziczenia. Jednak to podejście ma niefortunną praktyczną wadę, a mianowicie: ma sprawdzać system jako całość a nie każda klasa osobno. Fatalna okazuje się reguła (4), która przy wywołaniu podprogramu bibliotecznego uwzględni wszystkie możliwe jego wywołania w innych klasach.

Chociaż zaproponowano późniejsze algorytmy do pracy z poszczególnymi klasami w , nie udało się ustalić ich praktycznej wartości. Oznaczało to, że w środowisku programistycznym obsługującym kompilację przyrostową konieczne byłoby zorganizowanie sprawdzenia całego systemu. Pożądane jest wprowadzenie sprawdzania jako elementu (szybkiego) lokalnego przetwarzania zmian dokonywanych przez użytkownika w niektórych klasach. Chociaż znane są przykłady podejścia globalnego, programiści C używają tego narzędzia szarpie znaleźć niespójności w systemie, których kompilator nie wykrywa - wszystko to nie wygląda zbyt atrakcyjnie.

W efekcie, o ile mi wiadomo, nikt nie wdrożył sprawdzania poprawności systemu. (Innym powodem takiego wyniku mogła być złożoność samych zasad walidacji).

Poprawność klas obejmuje walidację ograniczoną do klasy i dlatego jest możliwa przy kompilacji przyrostowej. Poprawność systemu oznacza globalne sprawdzenie całego systemu, co jest w konflikcie z kompilacją przyrostową.

Jednak wbrew swojej nazwie, w rzeczywistości możliwe jest sprawdzenie poprawności systemu za pomocą jedynie przyrostowego sprawdzania klas (w trakcie normalnego kompilatora). Będzie to ostateczny wkład w rozwiązanie problemu.

Uważaj na polimorficzne krzyki!

Reguła Poprawności Systemu jest pesymistyczna: dla uproszczenia odrzuca również całkowicie bezpieczne kombinacje instrukcji. Może się to wydawać paradoksalne, ale ostatnią wersję rozwiązania zbudujemy na podstawie jeszcze bardziej pesymistyczna reguła. Rodzi to oczywiście pytanie, na ile realistyczny będzie nasz wynik.

Powrót do Jałty

Istota rozwiązania Catcall (Catcall), - znaczenie tego pojęcia wyjaśnimy później - wracając do ducha porozumień jałtańskich, dzielących świat na polimorficzny i kowariantny (i towarzysza kowariancji - ukrywających potomków), ale bez potrzeby posiadania nieskończonej mądrości.

Tak jak poprzednio, zawężamy kwestię kowariancji do dwóch operacji. W naszym głównym przykładzie jest to przypisanie polimorficzne: s:=b i wywołanie podprogramu kowariantnego: s.udział(g). Analizując, kto jest prawdziwym winowajcą naruszeń, wykluczamy spór G podejrzanych. Dowolny argument typu NARCIARZ lub wywodzący się z niej, nie odpowiada nam ze względu na polimorfizm S i kowariancja udział. Dlatego jeśli statycznie opisujesz istotę Inny Jak NARCIARZ i dynamicznie dołączyć do obiektu NARCIARZ, potem połączenie s.share (inne) statycznie będzie sprawiać wrażenie idealnego, ale spowoduje naruszenie typu, jeśli zostanie przypisany polimorficznie S oznaczający B.

Podstawowym problemem jest to, że staramy się używać S na dwa niekompatybilne sposoby: jako jednostka polimorficzna i jako cel kowariantnego wywołania podprogramu. (W naszym innym przykładzie problem polega na używaniu P jako byt polimorficzny i jako cel wywołania podprogramu potomnego ukrywającego komponent add_vertex.)

Rozwiązanie Catcall, podobnie jak Pinning, jest radykalne: zabrania używania jednostki jako polimorficznej i kowariantnej jednocześnie. Podobnie jak analizowanie globalne, statycznie określa, które jednostki mogą być polimorficzne, ale nie stara się być zbyt sprytny, szukając zestawów możliwych typów dla jednostek. Zamiast tego każda jednostka polimorficzna jest postrzegana jako na tyle podejrzana, że ​​zabronione jest sprzymierzanie się z kręgiem szanowanych osób, w tym kowariancja i ukrywanie potomków.

Jedna zasada i kilka definicji

Reguła typu dla rozwiązania Catcall ma proste sformułowanie:

Wpisz regułę dla Catcall

Wywołania polimorficzne są nieprawidłowe.

Opiera się na równie prostych definicjach. Przede wszystkim jednostka polimorficzna:

Definicja: jednostka polimorficzna

Istota X typ referencyjny (nierozwinięty) jest polimorficzny, jeśli ma jedną z następujących właściwości:

1 Występuje w zadaniu x:= y, gdzie esencja y ma inny typ lub jest polimorficzny przez rekurencję.

2 Znalezione w instrukcjach tworzenia utwórz (OTHER_TYPE) x, Gdzie OTHER_TYPE nie jest typem określonym w deklaracji X.

3 Jest to formalny argument do podprogramu.

4 Jest funkcją zewnętrzną.

Celem tej definicji jest nadanie statusu polimorficznego („potencjalnie polimorficznego”) dowolnej jednostce, którą można dołączyć do obiektów podczas wykonywania programu. różne rodzaje. Ta definicja ma zastosowanie tylko do typów referencyjnych, ponieważ rozszerzone jednostki nie mogą z natury być polimorficzne.

W naszych przykładach narciarz S i wielokąt P są polimorficzne zgodnie z regułą (1). Pierwszemu przypisuje się obiekt CHŁOPIEC B, drugi - obiekt PROSTOKĄT r.

Jeśli spojrzysz na definicję zestawu typów, zauważysz, o ile bardziej pesymistyczna jest definicja jednostki polimorficznej io ile łatwiej ją przetestować. Nie próbując znaleźć wszystkich możliwych dynamicznych typów bytu, zadowalamy się ogólnym pytaniem: czy dany byt może być polimorficzny, czy nie? Najbardziej zaskakująca jest reguła (3), zgodnie z którą polimorficzny liczy każdy parametr formalny(chyba że jego typ jest rozszerzony, jak ma to miejsce w przypadku liczb całkowitych itp.). Nawet nie zawracamy sobie głowy analizą połączeń. Jeśli podprogram ma argument, to jest on do pełnej dyspozycji klienta, co oznacza, że ​​nie można polegać na typie podanym w deklaracji. Zasada ta jest ściśle powiązana z ponownym użyciem — celem technologii obiektowej — gdzie każda klasa może potencjalnie zostać włączona do biblioteki i wielokrotnie wywoływana przez różnych klientów.

Cechą charakterystyczną tej reguły jest to, że nie wymaga ona żadnych globalnych kontroli. Aby zidentyfikować polimorfizm encji, wystarczy przejrzeć tekst samej klasy. Jeśli dla wszystkich żądań (atrybutów lub funkcji) przechowywana jest informacja o ich statusie polimorficznym, to nawet tekstów przodków nie trzeba badać. W przeciwieństwie do znajdowania zestawów typów, jednostki polimorficzne można odkrywać, sprawdzając klasę po klasie podczas kompilacji przyrostowej.

Wywołania, podobnie jak byty, mogą być polimorficzne:

Definicja: połączenie polimorficzne

Wywołanie jest polimorficzne, jeśli jego cel jest polimorficzny.

Oba wywołania w naszych przykładach są polimorficzne: s.udział(g) ze względu na polimorfizm S, p.add_vertex(...) ze względu na polimorfizm P. Z definicji tylko wywołania kwalifikowane mogą być polimorficzne. (Wykonywanie bezwarunkowego wezwania F(...) rodzaj kwalifikowanego prąd.f(...), nie zmieniamy bowiem istoty sprawy Aktualny, do którego nic nie można przypisać, nie jest obiektem polimorficznym).

Następnie potrzebujemy koncepcji Catcall, opartej na koncepcji CAT. (CAT to skrót oznaczający zmianę dostępności lub typu). Podprogram jest podprogramem CAT, jeśli jakieś przedefiniowanie go przez dziecko skutkuje jednym z dwóch rodzajów zmian, które, jak widzieliśmy, są potencjalnie niebezpieczne: zmianą typu argumentu (kowariantnie) lub ukryciem wcześniej wyeksportowanego komponentu.

Definicja: procedury CAT

Procedura jest nazywana procedurą CAT, jeśli jej przedefiniowanie zmienia status eksportu lub typ któregokolwiek z jej argumentów.

Ta właściwość ponownie umożliwia sprawdzanie przyrostowe: jakakolwiek zmiana definicji typu argumentu lub statusu eksportu powoduje, że procedura lub funkcja staje się podprogramem CAT. W tym miejscu pojawia się koncepcja Catcall: wywołanie podprogramu CAT, który może być błędny.

Definicja: wywołanie

Wywołanie jest nazywane Catcall, jeśli jakieś ponowne zdefiniowanie procedury spowodowałoby niepowodzenie z powodu zmiany statusu eksportu lub typu argumentu.

Stworzona przez nas klasyfikacja pozwala wyróżnić specjalne grupy odgłosów: polimorficzne i gwizdki. Wywołania polimorficzne dają wyrazistą moc podejściu obiektowemu, wywołania typu catcall pozwalają przedefiniować typy i ograniczyć eksport. Korzystając z terminologii wprowadzonej wcześniej w tym rozdziale, możemy powiedzieć, że wywołania polimorficzne rozciągają się przydatność, okrzyki - użyteczność.

Wyzwania udział I add_vertex, rozważane w naszych przykładach, to odgłosy kotów. Pierwszy z nich dokonuje kowariantnej redefinicji swojego argumentu. Drugi jest eksportowany przez klasę PROSTOKĄT, ale ukryte przez klasę WIELOKĄT. Oba odgłosy są również polimorficzne, więc są doskonałymi przykładami polimorficznych odgłosów. Są błędne zgodnie z regułą typu Catcall.

Stopień

Zanim podsumujemy wszystko, czego dowiedzieliśmy się o kowariancji i ukrywaniu dzieci, podsumujmy, że naruszenia poprawności systemów są rzeczywiście rzadkie. Na początku wykładu podsumowano najważniejsze właściwości statycznego typowania OO. Ta imponująca gama mechanizmów pisania wraz z walidacją opartą na klasach toruje drogę bezpiecznej i elastycznej metodzie tworzenia oprogramowania.

Widzieliśmy trzy rozwiązania problemu kowariancji, z których dwa dotyczyły również ograniczeń eksportowych. Który jest prawidłowy?

Nie ma ostatecznej odpowiedzi na to pytanie. Konsekwencje podstępnej interakcji między typowaniem obiektowym a polimorfizmem nie są tak dobrze rozumiane, jak zagadnienia omawiane na poprzednich wykładach. W ostatnich latach ukazały się liczne publikacje na ten temat, do których odnośniki podano w bibliografii na końcu wykładu. Ponadto mam nadzieję, że w tym wykładzie udało mi się przedstawić elementy ostatecznego rozwiązania lub przynajmniej się do niego zbliżyć.

Globalna analiza wydaje się niepraktyczna ze względu na pełne sprawdzenie całego systemu. Pomogło nam to jednak lepiej zrozumieć problem.

Rozwiązanie Pinning jest niezwykle atrakcyjne. Jest prosty, intuicyjny, łatwy do wdrożenia. Tym bardziej należy ubolewać nad niemożnością spełnienia w niej szeregu kluczowych wymagań metody OO, odzwierciedlonych w zasadzie Open-Closed. Gdybyśmy naprawdę mieli wielką intuicję, to przypinanie byłoby świetnym rozwiązaniem, ale który programista odważyłby się to stwierdzić, a tym bardziej przyznać, że autorzy klas bibliotecznych odziedziczonych po jego projekcie mieli taką intuicję?

Jeśli jesteśmy zmuszeni zrezygnować z fiksacji, to rozwiązanie Catcall wydaje się najbardziej odpowiednie, co dość łatwo wytłumaczyć i zastosować w praktyce. Jego pesymizm nie powinien wykluczać użytecznych kombinacji operatorów. W przypadku, gdy polimorficzny krzyk jest generowany przez „uzasadnioną” instrukcję, zawsze można bezpiecznie to przyznać, wprowadzając próbę przypisania. W ten sposób wiele sprawdzeń można przenieść na czas wykonania programu. Liczba takich przypadków powinna być jednak bardzo mała.

W ramach wyjaśnienia powinienem zauważyć, że w momencie pisania tego tekstu rozwiązanie Catcall nie zostało zaimplementowane. Dopóki kompilator nie zostanie dostosowany do sprawdzania reguł typu Catcall i pomyślnie zastosowany do dużych i małych systemów reprezentacyjnych, jest zbyt wcześnie, aby powiedzieć, że zostało powiedziane ostatnie słowo w kwestii pogodzenia typowania statycznego z polimorfizmem, w połączeniu z kowariancją i ukrywaniem potomków.

Pełna zgodność

Kończąc dyskusję na temat kowariancji, warto zrozumieć, w jaki sposób można zastosować ogólną metodę do dość ogólnego problemu. Metoda powstała w wyniku teorii Catcall, ale może być stosowana w ramach podstawowej wersji języka bez wprowadzania nowych reguł.

Niech będą dwie dopasowane listy, gdzie pierwsza określa narciarzy, a druga współlokatora narciarza z pierwszej listy. Chcemy przeprowadzić odpowiednią procedurę umieszczania udział, tylko jeśli zezwalają na to reguły opisu typu, które dopuszczają dziewczyny z dziewczynami, dziewczyny z nagrodami z dziewczynami z nagrodami i tak dalej. Problemy tego typu są powszechne.

Może istnieć proste rozwiązanie oparte na wcześniejszej dyskusji i próbie zadania. Rozważ funkcję uniwersalną wyposażone(zatwierdzić):


dopasowane (inne: OGÓLNE): jak inne jest
-- Bieżący obiekt (Current), jeśli jego typ odpowiada typowi obiektu,
-- dołączone do innych, poza tym nieważne.
if other /= Void, a następnie consid_to (other) then

Funkcjonować wyposażone zwraca bieżący obiekt, ale znany jako jednostka typu dołączonego do argumentu. Jeśli typ bieżącego obiektu nie pasuje do typu obiektu dołączonego do argumentu, funkcja zwraca Próżnia. Zwróć uwagę na rolę próby przypisania. Funkcja używa komponentu zgodny_z z klasy OGÓLNY, która określa zgodność typu pary obiektów.

Wymiana zgodny_z do innego komponentu OGÓLNY Z imieniem taki sam typ daje nam funkcję idealnie dopasowane (pełna zgodność), który powraca Próżnia jeśli typy obu obiektów nie są identyczne.

Funkcjonować wyposażone- daje nam proste rozwiązanie problemu dopasowywania narciarzy bez naruszania zasad opisywania typów. Tak więc w kodzie klasy NARCIARZ możemy wprowadzić nową procedurę i użyć jej zamiast udział, (ta ostatnia może być procedurą ukrytą).


-- Wybierz, jeśli dotyczy, inne jako numer sąsiada.
-- gender_ascertained - płeć przypisana
gender_ascertained_other: jak Current
płeć_ascertained_other:= inna .dopasowana (aktualna)
if gender_ascertained_other /= Puść wtedy
udostępnij(płeć_ascertained_other)
„Wniosek: kolokacja z innymi nie jest możliwa”

Dla Inny dowolny typ NARCIARZ(nie tylko jak prąd) zdefiniuj wersję płeć_potwierdzona_inna, do którego przypisano typ Aktualny. Funkcja pomoże nam zagwarantować tożsamość typów. idealnie dopasowane.

W przypadku dwóch równoległych list narciarzy reprezentujących planowane zakwaterowanie:


mieszkaniec1, mieszkaniec2: LISTA

możliwe jest zorganizowanie pętli poprzez wykonanie wywołania na każdym kroku:


occupant1.item.safe_share(occupant2.item)

pasujące elementy listy wtedy i tylko wtedy, gdy ich typy są w pełni kompatybilne.

Kluczowe idee

[X]. Statyczne pisanie jest kluczem do niezawodności, czytelności i wydajności.

[X]. Aby być realistycznym, typowanie statyczne wymaga kombinacji mechanizmów: asercji, wielokrotnego dziedziczenia, próby przypisania, ograniczonej i nieograniczonej generyczności, zakotwiczonych deklaracji. System typów nie może dopuszczać pułapek (rzutów typu).

[X]. Praktyczne zasady ponownej deklaracji powinny umożliwiać redefinicję kowariantną. Przesłonięte typy wyników i argumentów muszą być zgodne z oryginalnymi.

[X]. Kowariancja, a także zdolność dziecka do ukrycia komponentu wyeksportowanego przez przodka, w połączeniu z polimorfizmem, tworzy rzadki, ale bardzo poważny problem naruszenia typu.

[X]. Tych naruszeń można uniknąć stosując: analizę globalną (co jest niepraktyczne), ograniczenie kowariancji do typów stałych (co jest sprzeczne z zasadą Open-Closed), rozwiązanie Catcall, które uniemożliwia polimorficznemu celowi wywołanie podprogramu z kowariancją lub ukrycie dziecko.

Notatki bibliograficzne

Szereg materiałów z tego wykładu jest prezentowanych w raportach na forach OOPSLA 95 i TOOLS PACIFIC 95, a także publikowanych w . Z artykułu zapożyczono szereg materiałów przeglądowych.

Koncepcja automatycznego wnioskowania o typach została wprowadzona w , gdzie opisano algorytm wnioskowania o typach funkcjonalnego języka ML. Związek między polimorfizmem a sprawdzaniem typu został zbadany w .

Techniki poprawy wydajności kodu w językach dynamicznie typowanych w kontekście języka Self można znaleźć w .

Luca Cardelli i Peter Wegner napisali teoretyczny artykuł o typach w językach programowania, które miały ogromny wpływ na specjalistów. Praca ta, zbudowana na podstawie rachunku lambda (zob.), posłużyła jako podstawa do wielu dalszych badań. Poprzedził ją inny fundamentalny artykuł Cardelliego.

Podręcznik ISE zawiera wprowadzenie do problematyki łącznego zastosowania polimorfizmu, kowariancji i ukrywania potomków. Brak odpowiedniej analizy w pierwszym wydaniu tej książki doprowadził do szeregu krytycznych dyskusji (pierwszą z nich były komentarze Philippe'a Elincka w pracy licencjackiej „De la Conception-Programmation par Objets”, Memoire de licence, Universite Libre de Bruxelles (Belgia), 1988), wyrażone w pracach i . Artykuł Cooka podaje kilka przykładów związanych z problemem kowariancji i próbuje go rozwiązać. Rozwiązanie oparte na parametrach typu dla jednostek kowariantnych w TOOLS EUROPE 1992 zostało zaproponowane przez Franza Webera. Dokładne definicje pojęć poprawności systemowej oraz poprawności klasowej podano w , a także zaproponowano rozwiązanie wykorzystujące pełną analizę systemową. Rozwiązanie Catcall zostało po raz pierwszy zaproponowane w; Zobacz też .

Rozwiązanie Fixing zostało zaprezentowane w moim referacie na seminarium TOOLS EUROPE 1994. Wtedy jednak nie widziałem potrzeby kotwica-ads i powiązane ograniczenia zgodności. Paul Dubois i Amiram Yehudai szybko zwrócili uwagę, że problem kowariancji pozostaje w tych warunkach. Oni, jak również Reinhardt Budde, Karl-Heinz Sylla, Kim Walden i James McKim, poczynili wiele uwag, które miały fundamentalne znaczenie w pracy, która doprowadziła do napisania tego wykładu.

Zagadnieniom kowariancji poświęcono dużą ilość literatury. W i znajdziesz zarówno obszerną bibliografię, jak i przegląd matematycznych aspektów problemu. Aby uzyskać listę linków do materiałów online na temat teorii typów OOP i stron internetowych ich autorów, zobacz stronę Laurenta Dami. Pojęcia kowariancji i kontrawariancji zostały zapożyczone z teorii kategorii. Ich pojawienie się w kontekście pisania programów zawdzięczamy Luca Cardelli, który zaczął używać ich w swoich przemówieniach na początku lat 80., ale w druku sięgał do nich dopiero pod koniec lat 80.

Techniki oparte na zmiennych typu są opisane w , , .

Kontrawariancja została zaimplementowana w języku Sather. Wyjaśnienia podano w.

Ten artykuł zawiera absolutne minimum rzeczy, które musisz wiedzieć o pisaniu na klawiaturze, aby nie nazywać dynamicznego pisania złem, Lispa językiem bez pisania, a C językiem o silnej charakterystyce.

Pełna wersja jest szczegółowy opis wszelkiego rodzaju typowanie, doprawione przykładami kodu, linkami do popularnych języków programowania i poglądowymi obrazkami.

Polecam najpierw przeczytać skróconą wersję artykułu, a następnie, jeśli chcesz, pełną.

Krótka wersja

Typowe języki programowania są zazwyczaj podzielone na dwa duże obozy – typowane i nietypowane (nietypowane). Pierwsza obejmuje C, Python, Scala, PHP i Lua, podczas gdy druga obejmuje asembler, Forth i Brainfuck.

Ponieważ „pisanie bez pisania” jest z natury tak proste jak korek, nie jest dalej dzielone na żadne inne typy. Ale języki pisane są podzielone na kilka bardziej nakładających się kategorii:

  • Typowanie statyczne/dynamiczne. Statyczny jest określany przez fakt, że ostateczne typy zmiennych i funkcji są ustawiane w czasie kompilacji. Te. już kompilator jest w 100% pewien, który typ jest gdzie. W typowaniu dynamicznym wszystkie typy są określane w czasie wykonywania.

    Przykłady:
    Statyczne: C, Java, C#;
    Dynamiczne: Python, JavaScript, Ruby.

  • Mocne/słabe pisanie (czasem nazywane też mocnym/luźnym). Silne typowanie wyróżnia się tym, że język nie pozwala na mieszanie różnych typów w wyrażeniach i nie wykonuje automatycznych konwersji niejawnych, np. nie można odjąć zbioru od ciągu znaków. Słabo typowane języki automatycznie wykonują wiele niejawnych konwersji, nawet jeśli utrata precyzji lub konwersja mogą być niejednoznaczne.

    Przykłady:
    Mocne: Java, Python, Haskell, Lisp;
    Słabe: C, JavaScript, Visual Basic, PHP.

  • Jawne / niejawne pisanie. Języki jawnie typowane różnią się tym, że typ nowych zmiennych/funkcji/ich argumentów musi być jawnie ustawiony. W związku z tym języki z niejawnym pisaniem przenoszą to zadanie na kompilator / tłumacz.

    Przykłady:
    Jawne: C++, D, C#
    Niejawne: PHP, Lua, JavaScript

Należy również zauważyć, że wszystkie te kategorie przecinają się, na przykład język C ma statyczne, słabe jawne pisanie, a język Python ma dynamiczne, silne pisanie niejawne.

Niemniej jednak nie ma języków z jednoczesnym pisaniem statycznym i dynamicznym. Chociaż patrząc w przyszłość, powiem, że leżę tutaj - naprawdę istnieją, ale o tym później.

wersja szczegółowa

Jeśli krótka wersja nie jest dla Ciebie wystarczająca, w porządku. Nic dziwnego, że napisałem szczegółowo? Najważniejsze, że w krótkiej wersji po prostu niemożliwe było zmieszczenie wszystkich przydatnych i interesujących informacji, a szczegółowa byłaby prawdopodobnie zbyt długa, aby każdy mógł ją przeczytać bez wysiłku.

Nietypowane pisanie

W językach programowania bez typów wszystkie jednostki są uważane za sekwencje bitów o różnej długości.

Pisanie bez pisania jest zwykle nieodłącznym elementem języków niskiego poziomu (język asemblera, Forth) i ezoterycznych (Brainfuck, HQ9, Piet). Jednak oprócz swoich wad ma też pewne zalety.

Zalety
  • Umożliwia pisanie na wyjątkowo niskim poziomie, a kompilator/interpreter nie będzie ingerował w żadne kontrole typu. Możesz wykonywać dowolne operacje na dowolnych danych.
  • Wynikowy kod jest zwykle bardziej wydajny.
  • Przejrzystość instrukcji. Znając język, zwykle nie ma wątpliwości, czym jest ten lub inny kod.
Wady
  • Złożoność. Często istnieje potrzeba reprezentowania złożonych wartości, takich jak listy, ciągi znaków lub struktury. Może to powodować niedogodności.
  • Żadnych czeków. Wszelkie bezsensowne działania, takie jak odejmowanie od znaku wskaźnika do tablicy, będą uważane za zupełnie normalne, co jest obarczone subtelnymi błędami.
  • Niski poziom abstrakcji. Praca z dowolnym złożonym typem danych nie różni się niczym od pracy z liczbami, co oczywiście stwarza wiele trudności.
Mocne pisanie bez typów?

Tak, to istnieje. Na przykład w języku asemblera (dla architektury x86 / x86-64, nie znam innych) nie możesz asemblować programu, jeśli próbujesz załadować dane z rejestru rax (64 bity) do rejestru cx (16 bity).

mov cx, eax ; błąd czasu montażu

Okazuje się więc, że asembler nadal ma pisanie? Myślę, że te kontrole nie wystarczą. A twoja opinia zależy oczywiście tylko od ciebie.

Typowanie statyczne i dynamiczne

Najważniejszą rzeczą, która odróżnia pisanie statyczne (statyczne) od dynamicznego (dynamicznego), jest to, że wszystkie sprawdzanie typów jest wykonywane w czasie kompilacji, a nie w czasie wykonywania.

Niektórzy ludzie mogą pomyśleć, że pisanie statyczne jest zbyt restrykcyjne (w rzeczywistości jest, ale już dawno pozbyto się go za pomocą niektórych technik). Dla niektórych języki pisane dynamicznie igrają z ogniem, ale jakie cechy je wyróżniają? Czy oba gatunki mają szansę istnieć? Jeśli nie, to dlaczego istnieje tak wiele języków o typie statycznym i dynamicznym?

Rozwiążmy to.

Korzyści z typowania statycznego
  • Sprawdzanie typu odbywa się tylko raz, w czasie kompilacji. A to oznacza, że ​​nie będziemy musieli ciągle sprawdzać, czy próbujemy podzielić liczbę przez ciąg znaków (i albo zgłosić błąd, albo wykonać konwersję).
  • Szybkość wykonania. Z poprzedniego punktu wynika jasno, że języki o typie statycznym są prawie zawsze szybsze niż języki o typie dynamicznym.
  • Pod pewnymi dodatkowymi warunkami pozwala wykryć potencjalne błędy już na etapie kompilacji.
Korzyści z dynamicznego pisania
  • Łatwość tworzenia uniwersalnych kolekcji - sterty wszystkiego i wszystkiego (rzadko taka potrzeba się pojawia, ale kiedy już się pojawi, z pomocą przychodzi pisanie dynamiczne).
  • Wygoda opisywania uogólnionych algorytmów (np. sortowanie tablicowe, które zadziała nie tylko na liście liczb całkowitych, ale także na liście liczb rzeczywistych, a nawet na liście napisów).
  • Łatwe do nauczenia się — języki o dynamicznym typowaniu są zazwyczaj bardzo dobre do rozpoczęcia programowania.

Programowanie ogólne

Cóż, najważniejszym argumentem przemawiającym za dynamicznym typowaniem jest wygoda opisywania ogólnych algorytmów. Wyobraźmy sobie problem - potrzebujemy funkcji wyszukiwania dla kilku tablic (lub list) - tablicy liczb całkowitych, tablicy liczb rzeczywistych i tablicy znaków.

Jak to rozwiążemy? Rozwiążmy to w 3 różnych językach: jeden z pisaniem dynamicznym i dwa z pisaniem statycznym.

Wezmę jeden z najprostszych algorytmów wyszukiwania - wyliczanie. Funkcja pobierze szukany element, samą tablicę (lub listę) i zwróci indeks elementu lub jeśli element nie zostanie znaleziony - (-1).

Rozwiązanie dynamiczne (Python):

Def find(required_element, list): for (index, element) in enumerate(list): if element == wymagany_element: zwróć indeks return (-1)

Jak widać, wszystko jest proste i nie ma problemu z tym, że lista może zawierać liczby parzyste, nawet listy, mimo że nie ma innych tablic. Bardzo dobry. Idźmy dalej - rozwiąż ten sam problem w C!

Rozwiązanie statyczne (C):

unsigned int find_int(int wymagany_element, int array, unsigned int size) ( for (unsigned int i = 0; i< size; ++i) if (required_element == array[i]) return i; return (-1); } unsigned int find_float(float required_element, float array, unsigned int size) { for (unsigned int i = 0; i < size; ++i) if (required_element == array[i]) return i; return (-1); } unsigned int find_char(char required_element, char array, unsigned int size) { for (unsigned int i = 0; i < size; ++i) if (required_element == array[i]) return i; return (-1); }

Cóż, każda funkcja z osobna jest podobna do wersji Pythona, ale dlaczego są trzy? Czy programowanie statyczne przegrało?

Tak i nie. Istnieje kilka technik programowania, z których jedną omówimy teraz. Nazywa się to programowaniem ogólnym, a język C++ obsługuje je całkiem dobrze. Rzućmy okiem na nową wersję:

Rozwiązanie statyczne (programowanie ogólne, C++):

Szablon unsigned int find(T wymagany_element, std::vector tablica) ( for (unsigned int i = 0; i< array.size(); ++i) if (required_element == array[i]) return i; return (-1); }

Cienki! Nie wygląda na dużo bardziej skomplikowaną niż wersja w Pythonie i nie wymagało dużo pisania. Ponadto otrzymaliśmy implementację dla wszystkich tablic, a nie tylko 3 wymaganych do rozwiązania problemu!

Ta wersja wydaje się być dokładnie tym, czego potrzebujemy - uzyskujemy zarówno zalety pisania statycznego, jak i niektóre zalety dynamicznego.

To wspaniale, że w ogóle jest to możliwe, ale mogłoby być jeszcze lepiej. Po pierwsze, programowanie generyczne może być wygodniejsze i piękniejsze (na przykład w Haskell). Po drugie, oprócz programowania generycznego, można również zastosować polimorfizm (wynik będzie gorszy), przeciążanie funkcji (podobnie) czy makra.

Statyka w dynamice

Należy również wspomnieć, że wiele języków statycznych umożliwia pisanie dynamiczne, na przykład:

  • Język C# obsługuje dynamiczny pseudotyp.
  • Język F# obsługuje cukier składniowy w postaci operatora ?, którego można użyć do naśladowania pisania dynamicznego.
  • Haskell - dynamiczne pisanie zapewnia moduł Data.Dynamic.
  • Delphi - poprzez specjalny typ Wariant.

Ponadto niektóre języki o dynamicznym typowaniu umożliwiają korzystanie z pisania statycznego:

  • Common Lisp - deklaracje typu.
  • Perl - od wersji 5.6 raczej ograniczony.

Mocne i słabe typowanie

Silnie typowane języki nie pozwalają na mieszanie bytów różnych typów w wyrażeniach i nie wykonują żadnych automatycznych konwersji. Nazywa się je również „językami silnie typowanymi”. Angielski termin określający to to mocne pisanie.

Słabo typowane języki wręcz przeciwnie, zachęcają programistę do mieszania różnych typów w jednym wyrażeniu, a kompilator sam przekonwertuje wszystko na jeden typ. Nazywa się je również „językami o słabym typie”. Angielski termin określający to to słabe pisanie.

Słabe pisanie jest często mylone z pisaniem dynamicznym, co jest całkowicie błędne. Dynamicznie typowany język może być zarówno słabo, jak i silnie typowany.

Jednak niewiele osób przywiązuje wagę do ścisłości pisania. Często twierdzi się, że jeśli język jest typowany statycznie, można wychwycić wiele potencjalnych błędów kompilacji. Okłamują cię!

Język musi mieć również mocne pisanie. Rzeczywiście, jeśli kompilator zamiast zgłaszać błąd, po prostu dodaje ciąg znaków do liczby lub, co gorsza, odejmie kolejny z jednej tablicy, to co nam z tego, że wszystkie „sprawdzenia” typów będą na etapie kompilacji? Zgadza się - słabe pisanie statyczne jest jeszcze gorsze niż mocne pisanie dynamiczne! (No cóż, takie jest moje zdanie)

Dlaczego więc słabe pisanie nie ma żadnych zalet? Może się tak wydawać, ale pomimo tego, że jestem zdecydowanym zwolennikiem silnego pisania, muszę zgodzić się, że słabe pisanie ma również zalety.

Chcesz wiedzieć, które?

Korzyści z mocnego pisania
  • Niezawodność — otrzymasz wyjątek lub błąd kompilacji zamiast nieprawidłowego działania.
  • Szybkość – zamiast niejawnych konwersji, które mogą być dość drogie, przy mocnym typowaniu, musisz napisać je jawnie, co przynajmniej uświadamia programiście, że ten fragment kodu może być powolny.
  • Zrozumienie, jak działa program - ponownie, zamiast niejawnego rzutowania typów, programista pisze wszystko sam, co oznacza, że ​​\u200b\u200bw przybliżeniu rozumie, że porównanie ciągu znaków z liczbą nie dzieje się samo z siebie i nie jest magicznie.
  • Pewność - kiedy piszesz przekształcenia ręcznie, dokładnie wiesz, co przekształcasz i na co. Ponadto zawsze zrozumiesz, że takie konwersje mogą prowadzić do utraty precyzji i błędnych wyników.
Korzyści ze słabego pisania
  • Wygoda używania wyrażeń mieszanych (na przykład z liczb całkowitych i liczb rzeczywistych).
  • Abstrahowanie od pisania na klawiaturze i skupienie się na zadaniu.
  • Zwięzłość zapisu.

Dobra, ustaliliśmy, okazuje się, że słabe pisanie ma też zalety! Czy są jakieś sposoby na przeniesienie zalet słabego pisania na mocne pisanie?

Okazuje się, że są nawet dwa.

Niejawne rzutowanie typów, w sytuacjach jednoznacznych i bez utraty danych

Wow… Dość długi akapit. Pozwólcie, że dalej skrócę to do „ograniczona niejawna konwersja”. Co więc oznacza jednoznaczna sytuacja i utrata danych?

Jednoznaczna sytuacja to przekształcenie lub operacja, w której istota jest natychmiast jasna. Na przykład dodanie dwóch liczb jest sytuacją jednoznaczną. Ale zamiana liczby na tablicę nie jest (być może zostanie utworzona tablica jednego elementu, być może tablica o takiej długości będzie domyślnie wypełniona elementami, a być może liczba zostanie zamieniona na łańcuch, a następnie na tablicę znaków).

Utrata danych jest jeszcze łatwiejsza. Jeśli zamienimy liczbę rzeczywistą 3,5 na liczbę całkowitą, stracimy część danych (w rzeczywistości ta operacja jest również niejednoznaczna - jak zostanie wykonane zaokrąglenie? W górę? W dół? Pominięcie części ułamkowej?).

Konwersje w sytuacjach niejednoznacznych i konwersje z utratą danych są bardzo, bardzo złe. W programowaniu nie ma nic gorszego niż to.

Jeśli mi nie wierzysz, naucz się języka PL/I, a nawet poszukaj jego specyfikacji. Ma reguły konwersji między WSZYSTKIMI typami danych! To po prostu piekło!

Dobra, pamiętajmy o ograniczonej konwersji niejawnej. Czy są takie języki? Tak, na przykład w Pascalu możesz zamienić liczbę całkowitą na liczbę rzeczywistą, ale nie odwrotnie. Istnieją również podobne mechanizmy w C#, Groovy i Common Lisp.

Okay, powiedziałem, że jest inny sposób na uzyskanie kilku zalet słabego pisania w mocnym języku. I tak, istnieje i nazywa się polimorfizmem konstruktora.

Wyjaśnię to na przykładzie wspaniałego języka Haskell.

Konstruktory polimorficzne powstały w wyniku obserwacji, że podczas używania literałów liczbowych najczęściej potrzebne są bezpieczne konwersje niejawne.

Na przykład w wyrażeniu pi + 1 nie chcesz pisać pi + 1.0 lub pi + float(1) . Chcę napisać tylko pi + 1!

A odbywa się to w Haskell, dzięki temu, że literał 1 nie ma konkretnego typu. Nie jest ani całość, ani rzeczywista, ani złożona. To tylko liczba!

W rezultacie, pisząc prostą funkcję sum x y , która mnoży wszystkie liczby od x do y (z przyrostem 1), otrzymujemy jednocześnie kilka wersji - suma dla liczb całkowitych, suma dla liczb rzeczywistych, suma dla liczb wymiernych, suma dla zespolonych liczby, a nawet sumę dla wszystkich typów liczbowych, które sam zdefiniowałeś.

Oczywiście ta technika oszczędza tylko przy użyciu wyrażeń mieszanych z literałami numerycznymi, a to tylko wierzchołek góry lodowej.

Można więc powiedzieć, że najlepszym wyjściem będzie balansowanie na granicy między mocnym a słabym pisaniem. Ale jak dotąd żaden język nie ma idealnej równowagi, więc bardziej skłaniam się ku językom silnie typowanym (takim jak Haskell, Java, C#, Python) niż językom słabo typowanym (takim jak C, JavaScript, Lua, PHP) .

Typowanie jawne i niejawne

Język jawnie typowany wymaga od programisty określenia typów wszystkich deklarowanych zmiennych i funkcji. Angielski termin określający to to jawne pisanie.

Z drugiej strony język z niejawnym typowaniem zachęca do zapomnienia o typach i pozostawienia wnioskowania o typie kompilatorowi lub tłumaczowi. Angielski termin określający to to implicit typing.

Na początku można by pomyśleć, że pisanie niejawne jest równoważne pisaniu dynamicznemu, a pisanie jawne jest równoważne pisaniu statycznemu, ale później przekonamy się, że tak nie jest.

Czy każdy rodzaj ma zalety i znowu, czy istnieją ich kombinacje i czy są jakieś języki obsługujące obie metody?

Korzyści z jawnego pisania
  • Posiadanie sygnatury dla każdej funkcji (na przykład int add(int, int)) ułatwia określenie, co robi funkcja.
  • Programista od razu zapisuje, jakiego typu wartości mogą być przechowywane w konkretnej zmiennej, co eliminuje konieczność zapamiętywania tego.
Korzyści z niejawnego pisania
  • Skrót - def add(x, y) jest wyraźnie krótszy niż int add(int x, int y) .
  • Odporność na zmiany. Na przykład, jeśli w funkcji zmienna tymczasowa była tego samego typu co argument wejściowy, to w języku jawnie wpisywanym, gdy zmienia się typ argumentu wejściowego, należy również zmienić typ zmiennej tymczasowej.

Cóż, wygląda na to, że oba podejścia mają zarówno zalety, jak i wady (a kto spodziewał się czegoś innego?), więc poszukajmy sposobów na połączenie tych dwóch podejść!

Jawne pisanie z wyboru

Istnieją języki z domyślnym wpisywaniem niejawnym i możliwością określenia typu wartości w razie potrzeby. Kompilator automatycznie wydedukuje prawdziwy typ wyrażenia. Jednym z takich języków jest Haskell, podam prosty przykład dla zilustrowania:

Brak jawnego dodawania typu (x, y) = x + y -- Jawny dodawanie typu:: (liczba całkowita, liczba całkowita) -> dodawanie liczb całkowitych (x, y) = x + y

Uwaga: Celowo użyłem funkcji uncurried, a także celowo napisałem prywatny podpis zamiast bardziej ogólnego add:: (Num a) -> a -> a -> a , ponieważ Chciałem pokazać pomysł, bez wyjaśniania składni Haskella.

Hm. Jak widać jest bardzo ładnie i krótko. Wpis funkcji zajmuje tylko 18 znaków w linii, wliczając spacje!

Jednak automatyczne wnioskowanie o typie jest dość trudne i nawet w fajnym języku, takim jak Haskell, czasami zawodzi. (przykładem jest ograniczenie monomorfizmu)

Czy istnieją języki z domyślnym pisaniem jawnym i niejawnym z konieczności? Kon
z pewnością.

Niejawne wpisywanie z wyboru

Nowy standard języka C++, nazwany C++11 (wcześniej nazywany C++0x), wprowadził słowo kluczowe auto, które pozwala kompilatorowi wywnioskować typ z kontekstu:

Porównajmy: // Ręczna specyfikacja typu unsigned int a = 5; int bez znaku b = a + 3; // Automatyczne wnioskowanie o typie unsigned int a = 5; auto b = a + 3;

Nie jest zły. Ale rekord niewiele się skurczył. Zobaczmy przykład z iteratorami (jeśli nie rozumiesz, nie bój się, najważniejsze jest to, że rekord jest znacznie zmniejszony z powodu automatycznego wyjścia):

// Ręczny typ std::vector vec = losowyWektor(30); for (std::vector::const_iterator it = vec.cbegin(); ...) ( ... ) // Automatyczne wnioskowanie o typie auto vec = randomVector (trzydzieści); for (auto it = vec.cbegin(); ...) ( ... )

Wow! Oto skrót. No dobra, ale czy da się zrobić coś w duchu Haskella, gdzie typ zwracany będzie zależał od typów argumentów?

Ponownie odpowiedź brzmi tak, dzięki słowu kluczowemu decltype w połączeniu z auto:

// Typ ręczny int dzielenie(int x, int y) ( ... ) // Automatyczne odliczanie typu auto dzielenie (int x, int y) -> decltype(x / y) ( ... )

Ta forma notacji może nie brzmieć najlepiej, ale w połączeniu z rodzajami (szablony/rodzaje), niejawne pisanie lub automatyczne wnioskowanie o typie działa cuda.

Niektóre języki programowania zgodnie z tą klasyfikacją

Podam krótką listę popularnych języków i napiszę, jak są one podzielone na kategorie w ramach każdej kategorii „pisania na klawiaturze”.

JavaScript - Dynamiczny / Słaby / Niejawny Ruby - Dynamiczny / Silny / Niejawny Python - Dynamiczny / Silny / Niejawny Java - Statyczny / Silny / Jawny PHP - Dynamiczny / Słaby / Niejawny C - Statyczny / Słaby / Jawny C++ - Statyczny / Średnio mocny / Jawny Perl - Dynamiczny / Słaby / Niejawny Objective-C - Statyczny / Słaby / Jawny C# - Statyczny / Silny / Jawny Haskell - Statyczny / Silny / Niejawny Common Lisp - Dynamiczny / Silny / Niejawny

Być może gdzieś popełniłem błąd, zwłaszcza z CL, PHP i Obj-C, jeśli masz inne zdanie na temat jakiegoś języka, pisz w komentarzach.

  • Typowanie dynamiczne to technika szeroko stosowana w językach programowania i językach specyfikacji, w której zmienna jest powiązana z typem w momencie przypisania wartości, a nie w momencie deklaracji zmiennej. Tym samym w różnych częściach programu ta sama zmienna może przyjmować wartości różnych typów. Przykładami dynamicznie typowanych języków są Smalltalk, Python, Objective-C, Ruby, PHP, Perl, JavaScript, Lisp, xBase, Erlang, Visual Basic.

    Techniką przeciwną jest pisanie statyczne.

    W niektórych językach ze słabym dynamicznym typowaniem występuje problem z porównywaniem wartości, na przykład PHP ma operatory porównania „==”, „!=" i „===", „!==", gdzie druga para operacji porównuje wartości i typy zmiennych. Operator „===” zwraca wartość true tylko wtedy, gdy pasuje idealnie, w przeciwieństwie do operatora „==”, który uważa następujące wyrażenie za prawdziwe: (1=="1"). Warto zauważyć, że nie jest to ogólnie problem typowania dynamicznego, ale konkretnych języków programowania.

Pojęcia pokrewne

Język programowania to formalny język do pisania programów komputerowych. Język programowania definiuje zestaw reguł leksykalnych, składniowych i semantycznych, które określają wygląd programu i działania, które wykonawca (najczęściej komputer) wykona pod jego kontrolą.

Cukier syntaktyczny w języku programowania to cecha syntaktyczna, której użycie nie wpływa na zachowanie programu, ale sprawia, że ​​korzystanie z języka jest wygodniejsze dla człowieka.

Właściwość to sposób uzyskiwania dostępu do wewnętrznego stanu obiektu, imitujący zmienną pewnego typu. Dostęp do właściwości obiektu wygląda tak samo, jak dostęp do pola struktury (w programowaniu strukturalnym), ale w rzeczywistości jest realizowany poprzez wywołanie funkcji. Podczas próby ustawienia wartości tej właściwości wywoływana jest jedna metoda, a przy próbie uzyskania wartości tej właściwości wywoływana jest inna metoda.

Rozszerzony formularz Backusa – Naura (EBNF) to formalny system definicji składni, w którym niektóre kategorie składniowe są definiowane sekwencyjnie przez inne. Używane do opisywania bezkontekstowych gramatyk formalnych. Sugerowane przez Niklausa Wirtha. Jest to rozbudowana przeróbka form Backusa-Naura, różni się od BNF bardziej „pojemnymi” konstrukcjami, które przy tej samej zdolności wyrazowej pozwalają uprościć…

Programowanie aplikacyjne to rodzaj programowania deklaratywnego, w którym pisanie programu polega na systematycznym stosowaniu jednego obiektu do drugiego. Wynikiem takiej aplikacji jest ponownie obiekt, który może uczestniczyć w aplikacjach zarówno jako funkcja, jak i jako argument i tak dalej. Dzięki temu zapis programu jest matematycznie przejrzysty. Fakt, że funkcja jest oznaczona przez wyrażenie, wskazuje na możliwość użycia funkcji-wartości - funkcyjnych ...

Konkatenacyjny język programowania to język programowania oparty na fakcie, że połączenie dwóch fragmentów kodu wyraża ich kompozycję. W takim języku szeroko stosowana jest niejawna specyfikacja argumentów funkcji (patrz bezsensowne programowanie), nowe funkcje są definiowane jako składanie funkcji, a zamiast aplikacji stosowana jest konkatenacja. Takie podejście jest przeciwieństwem programowania aplikacyjnego.

Zmienna jest atrybutem systemu fizycznego lub abstrakcyjnego, który może zmieniać swoją, zwykle liczbową, wartość. Pojęcie zmiennej jest szeroko stosowane w takich dziedzinach jak matematyka, nauki przyrodnicze, inżynieria i programowanie. Przykładowe zmienne to: temperatura powietrza, parametr funkcji i wiele innych.

Analiza składniowa (lub parsowanie, parsowanie slangowe ← parsowanie angielskie) w językoznawstwie i informatyce to proces porównywania liniowej sekwencji leksemów (słów, tokenów) języka naturalnego lub formalnego z jego gramatyką formalną. Wynikiem jest zwykle drzewo analizy (drzewo składni). Zwykle używany w połączeniu z analizą leksykalną.

Uogólniony algebraiczny typ danych (GADT) to jeden z typów algebraicznych typów danych, który charakteryzuje się tym, że jego konstruktorzy mogą zwracać wartości, które nie są z nim powiązane. Zaprojektowany pod wpływem prac nad rodzinami indukcyjnymi wśród badaczy typów zależnych.

Semantyka w programowaniu to dyscyplina zajmująca się badaniem formalizacji znaczeń konstrukcji języka programowania poprzez konstruowanie ich formalnych modeli matematycznych. Różne narzędzia mogą być używane jako narzędzia do budowania takich modeli, na przykład logika matematyczna, rachunek λ, teoria mnogości, teoria kategorii, teoria modeli, algebra uniwersalna. Formalizacja semantyki języka programowania może być wykorzystana zarówno do opisu języka, jak i do określenia właściwości języka...

Programowanie zorientowane obiektowo (OOP) to metodologia programowania oparta na reprezentowaniu programu jako zestawu obiektów, z których każdy jest instancją określonej klasy, a klasy tworzą hierarchię dziedziczenia.

Zmienna dynamiczna - zmienna w programie, której miejsce w pamięci RAM jest przydzielane podczas wykonywania programu. W rzeczywistości jest to część pamięci przydzielona przez system programowi do określonych celów podczas jego działania. Tym różni się od globalnej zmiennej statycznej - kawałka pamięci przydzielonej przez system programowi do określonych celów przed jego uruchomieniem. Zmienna dynamiczna jest jedną z klas przechowywania zmiennych.

Wszystko jest bardzo proste. To jak różnica między hotelem a prywatnym apartamentem.

W mieszkaniu mieszkają tylko ci, którzy są tam zameldowani. Jeśli, powiedzmy, mieszka w nim rodzina Sidorovów, to rodzina Pupkinów nie będzie mogła tam mieszkać do końca życia. W tym samym czasie Petya Sidorov może mieszkać w tym mieszkaniu, a następnie Grisha Sidorov może się tam przenieść (czasami mogą nawet mieszkać tam w tym samym czasie - to tablica). To jest typowanie statyczne.

Rodzina Sidorovów może przez jakiś czas zamieszkać w hotelu. Nawet nie muszą się tam zapisywać. Potem tam odejdą, a Pupkins się tam przeniosą. A potem Kuzniecow. A potem ktoś inny. To jest pisanie dynamiczne.

Jeśli wrócimy do programowania, to pierwszy przypadek (typowanie statyczne) występuje, powiedzmy, w C, C++, C#, Javie i innych. Zanim przypiszesz po raz pierwszy wartość zmienna, musisz powiedzieć, co będziesz tam przechowywać: liczby całkowite, liczby zmiennoprzecinkowe, ciągi znaków itp. ( Sidorovowie będą mieszkać w tym mieszkaniu). Z drugiej strony pisanie dynamiczne nie jest wymagane. Przypisując wartość, jednocześnie przypisujesz zmiennej jej typ ( W tym pokoju hotelowym mieszka teraz Vasya Pupkin z rodziny Pupkinów). Można to znaleźć w językach takich jak PHP, Python i JavaScript.

Oba podejścia mają swoje zalety i wady. Który z nich jest lepszy, a który gorszy, zależy od zadań do rozwiązania. Więcej szczegółów można znaleźć, powiedzmy, w Wikipedii.

przy statycznym typowaniu znasz dokładnie typ zmiennej w momencie pisania programu i opracowywania algorytmu i bierzesz to pod uwagę. te. jeśli powiedziałeś, że zmienna G jest czterobajtową liczbą całkowitą bez znaku, to w algorytmie zawsze będzie to dokładnie czterobajtowa liczba całkowita bez znaku (jeśli już, to musisz ją jawnie przekonwertować lub wiedzieć, jak tłumacz konwertuje ją w postaci pewien krąg sytuacji, ale w zasadzie jeśli występuje niezgodność typu, jest to błąd algorytmu, a kompilator przynajmniej cię ostrzeże), przy statyce nie możesz umieścić ciągu „Wasya głupek” w liczbie i dodatkowe kontrole przed użyciem zmiennej „czy istnieje liczba” - nie są wymagane, poświęcasz całą poprawność danych w momencie ich wprowadzania do programu lub zgodnie z wymaganiami samego algorytmu.

przy dynamicznym typowaniu typ tej samej zmiennej jest Ci generalnie nieznany i może się zmieniać już w trakcie wykonywania programu, a Ty to bierzesz pod uwagę, nikt Cię nie ostrzeże o potencjalnym błędzie algorytmu z powodu niezgodności typów (kiedy opracowując algorytm założyłeś, że w G jest liczbą całkowitą, a użytkownik wpisał np. liczbę zmiennoprzecinkową lub co gorsza ciąg znaków, albo powiedzmy po wykonaniu operacji arytmetycznej zamiast integer, a w następnym kroku spróbujesz użyć operacje bitowe...), z drugiej strony nie można zaprzątać sobie głowy wieloma drobiazgami.