Principii de bază ale programării: tastare statică și dinamică. Tastarea limbajelor de programare Tipuri de tastare

Acest articol conține minimumul de lucruri pe care trebuie să le știți despre tastare, astfel încât să nu numiți scrierea dinamică rău, Lisp un limbaj netipizat și C un limbaj puternic tastat.

Versiunea completă conține o descriere detaliată a tuturor tipurilor de tastare, asezonată cu exemple de cod, link-uri către limbaje de programare populare și imagini demonstrative.

Vă recomand să citiți mai întâi versiunea scurtă a articolului și apoi, dacă doriți, pe cea integrală.

Versiune scurta

Limbajele de programare de tastare sunt de obicei împărțite în două tabere mari - tastate și neînregistrate (netipizate). Primul include C, Python, Scala, PHP și Lua, în timp ce cel de-al doilea include limbajul de asamblare, Forth și Brainfuck.

Întrucât „dactilografiarea neînregistrată” este în mod inerent la fel de simplă ca o plută, nu mai este împărțită în alte tipuri. Dar limbile tastate sunt împărțite în mai multe categorii care se suprapun:

  • Tastare statică/dinamică. Static este determinat de faptul că tipurile finale de variabile și funcții sunt setate în timpul compilării. Acestea. deja compilatorul este 100% sigur care tip este unde. În tastarea dinamică, toate tipurile sunt determinate în timpul execuției.

    Exemple:
    Static: C, Java, C#;
    Dinamic: Python, JavaScript, Ruby.

  • Tastare puternică / slabă (uneori numită și puternic / liber). Tastarea puternică se distinge prin faptul că limbajul nu permite amestecarea diferitelor tipuri în expresii și nu efectuează conversii implicite automate, de exemplu, nu puteți scădea un set dintr-un șir. Limbile scrise slab realizează automat multe conversii implicite, chiar dacă pierderea preciziei sau conversia poate fi ambiguă.

    Exemple:
    Puternic: Java, Python, Haskell, Lisp;
    Slab: C, JavaScript, Visual Basic, PHP.

  • Tastare explicită / implicită. Limbile scrise explicit diferă prin aceea că tipul de noi variabile/funcții/argumente trebuie să fie setat în mod explicit. În consecință, limbile cu tastare implicită transferă această sarcină la compilator / interpret.

    Exemple:
    Explicit: C++, D, C#
    Implicit: PHP, Lua, JavaScript

De asemenea, trebuie remarcat că toate aceste categorii se intersectează, de exemplu, limbajul C are o tastare statică slabă explicită, iar limbajul Python are o tastare implicită puternică dinamică.

Cu toate acestea, nu există limbi cu tastare statică și dinamică în același timp. Deși privesc înainte, voi spune că zac aici - ele chiar există, dar mai multe despre asta mai târziu.

versiune detaliată

Dacă versiunea scurtă nu este suficientă pentru tine, bine. Nu e de mirare că am scris detaliat? Principalul lucru este că în versiunea scurtă a fost pur și simplu imposibil să se potrivească toate informațiile utile și interesante, iar cea detaliată ar fi probabil prea lungă pentru ca toată lumea să o citească fără a se eforta.

Tastare fără tastare

În limbajele de programare netipizate, toate entitățile sunt considerate a fi doar secvențe de biți de diferite lungimi.

Tastarea netipificată este de obicei inerentă limbajelor de nivel scăzut (limbaj de asamblare, Forth) și ezoterice (Brainfuck, HQ9, Piet). Cu toate acestea, pe lângă dezavantajele sale, are și câteva avantaje.

Avantaje
  • Vă permite să scrieți la un nivel extrem de scăzut, iar compilatorul/interpretul nu va interfera cu nicio verificare de tip. Sunteți liber să efectuați orice operațiune pe orice tip de date.
  • Codul rezultat este de obicei mai eficient.
  • Transparența instrucțiunilor. Cu cunoașterea limbii, de obicei nu există nicio îndoială ce este acest cod sau acela.
Defecte
  • Complexitate. Adesea este nevoie de a reprezenta valori complexe, cum ar fi liste, șiruri de caractere sau structuri. Acest lucru poate cauza neplăceri.
  • Fără verificări. Orice acțiuni fără sens, cum ar fi scăderea unui indicator la o matrice dintr-un caracter, vor fi considerate perfect normale, ceea ce este plin de erori subtile.
  • Nivel scăzut de abstractizare. Lucrul cu orice tip de date complex nu este diferit de lucrul cu numere, ceea ce desigur va crea multe dificultăți.
Tastare puternică fără tip?

Da, asta există. De exemplu, în limbajul de asamblare (pentru arhitectura x86 / x86-64, nu cunosc altele) nu puteți asambla un program dacă încercați să încărcați date din registrul rax (64 de biți) în registrul cx (16). biți).

mov cx, eax ; eroare de timp de asamblare

Deci, se pare că asamblatorul are încă tastarea? Cred că aceste verificări nu sunt suficiente. Și părerea ta, desigur, depinde doar de tine.

Tastare statică și dinamică

Principalul lucru care distinge tastarea statică (statică) de cea dinamică (dinamică) este că toate verificările de tip sunt efectuate în timpul compilării, și nu în timpul rulării.

Unii oameni pot crede că tastarea statică este prea restrictivă (de fapt, este, dar a fost scăpat de mult cu ajutorul unor tehnici). Pentru unii, limbile tastate dinamic se joacă cu focul, dar ce caracteristici le fac să iasă în evidență? Ambele specii au șansa de a exista? Dacă nu, de ce există atât de multe limbi tipizate static și dinamic?

Să ne dăm seama.

Beneficiile tastării statice
  • Verificările de tip au loc o singură dată, în timpul compilării. Și asta înseamnă că nu va trebui să aflăm în mod constant dacă încercăm să împărțim un număr cu un șir (și fie să aruncăm o eroare, fie să facem o conversie).
  • Viteza de executie. Din punctul anterior reiese clar că limbile tipizate static sunt aproape întotdeauna mai rapide decât cele scrise dinamic.
  • În anumite condiții suplimentare, vă permite să detectați erori potențiale deja în etapa de compilare.
Beneficiile tastării dinamice
  • Ușurința de a crea colecții universale - grămezi de tot și de toate (rar apare o astfel de nevoie, dar atunci când apare, tastarea dinamică va ajuta).
  • Comoditatea descrierii algoritmilor generalizați (de exemplu, sortarea matricei, care va funcționa nu numai pe o listă de numere întregi, ci și pe o listă de numere reale și chiar pe o listă de șiruri de caractere).
  • Ușor de învățat - Limbile tastate dinamic sunt de obicei foarte bune pentru a începe programarea.

Programare generică

Ei bine, cel mai important argument pentru tastarea dinamică este comoditatea de a descrie algoritmi generici. Să ne imaginăm o problemă - avem nevoie de o funcție de căutare pentru mai multe matrice (sau liste) - o matrice de numere întregi, o matrice de reali și o matrice de caractere.

Cum o vom rezolva? Să o rezolvăm în 3 limbi diferite: una cu tastare dinamică și două cu tastare statică.

Voi lua unul dintre cei mai simpli algoritmi de căutare - enumerarea. Funcția va primi elementul căutat, matricea (sau lista) în sine și va returna indexul elementului, sau dacă elementul nu este găsit - (-1).

Soluție dinamică (Python):

Def find(required_element, list): for (index, element) in enumerate(list): if element == required_element: return index return (-1)

După cum puteți vedea, totul este simplu și nu există probleme cu faptul că lista poate conține numere pare, chiar liste, chiar dacă nu există alte matrice. Foarte bine. Să mergem mai departe - rezolvă aceeași problemă în C!

Soluție statică (C):

unsigned int find_int(int required_element, int array, unsigned int size) (pentru (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); }

Ei bine, fiecare funcție în mod individual este similară cu versiunea Python, dar de ce sunt trei? S-a pierdut programarea statică?

Da și nu. Există mai multe tehnici de programare, dintre care una o vom lua în considerare acum. Se numește Programare generică, iar limbajul C++ îl suportă destul de bine. Să aruncăm o privire la noua versiune:

Soluție statică (programare generică, C++):

Șablon unsigned int find(T required_element, std::vector matrice) (pentru (unsigned int i = 0; i< array.size(); ++i) if (required_element == array[i]) return i; return (-1); }

Bun! Nu pare mult mai complicat decât versiunea Python și nu a fost nevoie de mult scris. În plus, am primit implementarea pentru toate matricele, nu doar pentru cele 3 necesare pentru a rezolva problema!

Această versiune pare să fie exact ceea ce avem nevoie - obținem atât avantajele tastării statice, cât și unele dintre avantajele dinamicii.

Este grozav că este chiar posibil, dar ar putea fi și mai bine. În primul rând, programarea generică poate fi mai convenabilă și mai frumoasă (de exemplu, în Haskell). În al doilea rând, pe lângă programarea generică, puteți utiliza și polimorfismul (rezultatul va fi mai rău), supraîncărcarea funcțiilor (în mod similar) sau macrocomenzi.

Static în dinamică

De asemenea, trebuie menționat că multe limbaje statice permit tastarea dinamică, de exemplu:

  • C# acceptă pseudo-tipul dinamic.
  • F# acceptă zahărul sintactic sub forma operatorului ?, care poate fi folosit pentru a imita tastarea dinamică.
  • Haskell - tastarea dinamică este asigurată de modulul Data.Dynamic.
  • Delphi - printr-o Varianta de tip special.

De asemenea, unele limbi tastate dinamic vă permit să profitați de tastarea statică:

  • Common Lisp - declarații de tip.
  • Perl - din versiunea 5.6, destul de limitat.

Tastare puternică și slabă

Limbile puternic tastate nu permit amestecarea de entități de diferite tipuri în expresii și nu efectuează conversii automate. Ele sunt numite și „limbi puternic tipizate”. Termenul englezesc pentru acest lucru este scriere puternică.

Limbajele slab tipizate, dimpotrivă, încurajează programatorul să amestece diferite tipuri într-o singură expresie, iar compilatorul însuși va converti totul într-un singur tip. Ele mai sunt numite și „limbi slab tipizate”. Termenul englezesc pentru aceasta este scriere slabă.

Tastarea slabă este adesea confundată cu tastarea dinamică, care este complet greșită. Un limbaj tipizat dinamic poate fi atât slab, cât și puternic tastat.

Cu toate acestea, puțini oameni acordă importanță stricteței tastării. Se pretinde adesea că, dacă o limbă este tastată static, atunci puteți detecta o mulțime de erori potențiale de compilare. Ei te mint!

Limba trebuie să aibă și o tastare puternică. Într-adevăr, dacă compilatorul, în loc să raporteze o eroare, adaugă pur și simplu un șir la un număr sau, și mai rău, scade altul dintr-o matrice, la ce ne ajută că toate „verificările” de tipuri vor fi în stadiul de compilare? Așa este - tastarea statică slabă este chiar mai rea decât tastarea dinamică puternică! (Ei bine, asta e parerea mea)

Deci, de ce tastarea slabă nu are deloc avantaje? Poate părea așa, dar în ciuda faptului că sunt un susținător puternic al tastării puternice, trebuie să fiu de acord că tastarea slabă are și avantaje.

Vrei să știi care dintre ele?

Beneficiile tastării puternice
  • Fiabilitate - Veți primi o excepție sau o eroare de compilare în loc să vă comportați greșit.
  • Viteză - în loc de conversii implicite, care pot fi destul de costisitoare, cu tastare puternică, trebuie să le scrieți explicit, ceea ce îl face pe programator cel puțin conștient că această bucată de cod poate fi lentă.
  • Înțelegerea modului în care funcționează programul - din nou, în loc de turnare implicită de tip, programatorul scrie totul el însuși, ceea ce înseamnă că înțelege aproximativ că compararea unui șir și a unui număr nu se întâmplă de la sine și nu prin magie.
  • Certitudine - atunci când scrii transformări de mână, știi exact ce transformi și în ce. De asemenea, veți înțelege întotdeauna că astfel de conversii pot duce la pierderea preciziei și la rezultate incorecte.
Beneficiile tastării slabe
  • Comoditatea utilizării expresiilor mixte (de exemplu, din numere întregi și numere reale).
  • Abstracția de la tastare și concentrarea asupra sarcinii.
  • Concizia înregistrării.

Bine, ne-am dat seama, se dovedește că tastarea slabă are și avantaje! Există modalități de a transfera avantajele tastării slabe la tastarea puternică?

Se pare că sunt chiar două.

Casting de tip implicit, în situații lipsite de ambiguitate și fără pierderi de date

Wow... Un paragraf destul de lung. Permiteți-mi să o prescurt în continuare la „conversie implicită restricționată” Deci, ce înseamnă situația neechivocă și pierderea de date?

O situație fără ambiguitate este o transformare sau operație în care esența este imediat clară. De exemplu, adăugarea a două numere este o situație clară. Dar conversia unui număr într-o matrice nu este (poate că va fi creată o matrice cu un element, poate o matrice cu o astfel de lungime, umplută cu elemente în mod implicit și poate un număr va fi convertit într-un șir și apoi într-o matrice de personaje).

Pierderea datelor este și mai ușoară. Dacă convertim numărul real 3,5 într-un număr întreg, vom pierde o parte din date (de fapt, această operație este și ea ambiguă - cum se va face rotunjirea? În sus? În jos? Scăderea părții fracționale?).

Conversiile în situații ambigue și conversiile cu pierdere de date sunt foarte, foarte proaste. Nu există nimic mai rău decât asta în programare.

Dacă nu mă credeți, învățați limbajul PL/I sau chiar căutați specificațiile acestuia. Are reguli de conversie între TOATE tipurile de date! E doar iadul!

Bine, să ne amintim despre conversia implicită limitată. Există astfel de limbi? Da, de exemplu în Pascal puteți converti un întreg într-un număr real, dar nu invers. Există și mecanisme similare în C#, Groovy și Common Lisp.

Bine, am spus că există o altă modalitate de a obține câteva avantaje ale tastării slabe într-o limbă puternică. Și da, există și se numește polimorfism constructor.

O voi explica folosind minunatul limbaj Haskell ca exemplu.

Constructorii polimorfi au apărut ca urmare a observației că conversiile implicite sigure sunt cel mai adesea necesare atunci când se folosesc literali numerici.

De exemplu, în expresia pi + 1 , nu doriți să scrieți pi + 1.0 sau pi + float(1) . Vreau să scriu doar pi + 1!

Și acest lucru se face în Haskell, datorită faptului că literalul 1 nu are un tip concret. Nu este nici întreg, nici real, nici complex. Este doar un număr!

Ca urmare, atunci când scriem o funcție simplă sum x y , care înmulțește toate numerele de la x la y (cu un increment de 1), obținem mai multe versiuni deodată - sumă pentru numere întregi, sumă pentru reali, sumă pentru raționale, sumă pentru complex numere și chiar suma pentru toate acele tipuri numerice pe care le-ați definit.

Desigur, această tehnică salvează numai atunci când se utilizează expresii mixte cu literale numerice, iar acesta este doar vârful aisbergului.

Astfel, putem spune că cea mai bună cale de ieșire va fi echilibrarea la limita, între tastarea puternică și slabă. Dar până acum, nicio limbă nu are un echilibru perfect, așa că înclin mai mult spre limbaje puternic tastate (cum ar fi Haskell, Java, C#, Python) decât spre limbaje slab tastate (cum ar fi C, JavaScript, Lua, PHP) .

Tastare explicită și implicită

Un limbaj tipizat explicit necesită ca programatorul să specifice tipurile tuturor variabilelor și funcțiilor pe care le declară. Termenul englezesc pentru aceasta este tastarea explicită.

Un limbaj implicit tipizat, pe de altă parte, vă invită să uitați de tipuri și să lăsați sarcina de inferență a tipurilor pe seama compilatorului sau interpretului. Termenul englezesc pentru aceasta este tastatura implicită.

La început, s-ar putea crede că tastarea implicită este echivalentă cu tastarea dinamică, iar tastarea explicită este echivalentă cu tastarea statică, dar vom vedea mai târziu că nu este cazul.

Există avantaje pentru fiecare fel și, din nou, există combinații ale acestora și există limbi care acceptă ambele metode?

Beneficiile tastării explicite
  • Având o semnătură pentru fiecare funcție (de exemplu, int add(int, int)) face ușor să determinați ce face funcția.
  • Programatorul notează imediat ce tip de valori pot fi stocate într-o anumită variabilă, ceea ce elimină nevoia de a reține acest lucru.
Beneficiile tastării implicite
  • Stenografia - def add(x, y) este clar mai scurtă decât int add(int x, int y) .
  • Rezistenta la schimbare. De exemplu, dacă într-o funcție variabila temporară a fost de același tip cu argumentul de intrare, atunci într-un limbaj tatat explicit, atunci când tipul argumentului de intrare se schimbă, va trebui să se schimbe și tipul variabilei temporare.

Ei bine, se pare că ambele abordări au atât argumente pro, cât și contra (și cine se aștepta la altceva?), așa că haideți să căutăm modalități de a combina aceste două abordări!

Tastare explicită la alegere

Există limbaje cu tastare implicită în mod implicit și posibilitatea de a specifica tipul de valori dacă este necesar. Compilatorul va deduce automat tipul adevărat al expresiei. Un astfel de limbaj este Haskell, permiteți-mi să vă dau un exemplu simplu pentru a ilustra:

Fără tip explicit adăugare (x, y) = x + y -- Tip explicit adăugare:: (Integer, Integer) -> Integer add (x, y) = x + y

Notă: am folosit în mod intenționat o funcție necurriculară și, de asemenea, am scris în mod intenționat o semnătură privată în loc de adăugarea mai generală:: (Num a) -> a -> a -> a , deoarece Am vrut să arăt ideea, fără a explica sintaxa lui Haskell.

Hm. După cum vedem, este foarte frumos și scurt. Intrarea funcției are doar 18 caractere pe linie, inclusiv spațiile!

Cu toate acestea, inferența de tip automat este destul de complicată și chiar și într-un limbaj cool precum Haskell, uneori eșuează. (un exemplu este constrângerea monomorfismului)

Există limbi cu tastare explicită în mod implicit și tastare implicită de necesitate? Kon
desigur.

Tastare implicită la alegere

Noul standard de limbaj C++, numit C++11 (numit anterior C++0x), a introdus cuvântul cheie auto, care permite compilatorului să deducă tipul din context:

Să comparăm: // Specificarea tipului manual unsigned int a = 5; unsigned int b = a + 3; // Inferență automată de tip unsigned int a = 5; auto b = a + 3;

Nu-i rău. Dar recordul nu s-a micșorat prea mult. Să vedem un exemplu cu iteratoare (dacă nu înțelegeți, nu vă fie teamă, principalul lucru de reținut este că înregistrarea este mult redusă datorită ieșirii automate):

// Tip manual std::vector vec = randomVector(30); pentru (std::vector::const_iterator it = vec.cbegin(); ...) ( ... ) // Inferență automată de tip auto vec = randomVector (treizeci); pentru (auto it = vec.cbegin(); ...) ( ... )

Wow! Iată abrevierea. Bine, dar este posibil să faci ceva în spiritul lui Haskell, unde tipul de returnare va depinde de tipurile de argumente?

Din nou, răspunsul este da, datorită cuvântului cheie decltype în combinație cu auto:

// Tip manual int divide(int x, int y) ( ... ) // Deducere automată tip auto divide(int x, int y) -> decltype(x / y) ( ... )

Această formă de notație poate să nu sune grozav, dar atunci când este combinată cu generice (șabloane / generice), tastarea implicită sau inferența de tip automat face minuni.

Unele limbaje de programare conform acestei clasificări

Voi oferi o listă scurtă de limbi populare și voi scrie cum sunt clasificate în fiecare categorie de „dactilografiere”.

JavaScript - Dinamic / Slab / Implicit Ruby - Dinamic / Puternic / Implicit Python - Dinamic / Puternic / Java implicit - Static / Puternic / PHP explicit - Dinamic / Slab / Implicit C - Static / Slab / Explicit C++ - Static / Semi-puternic / Perl explicit - Dinamic / Slab / Implicit Obiectiv-C - Static / Slab / Explicit C# - Static / Puternic / Explicit Haskell - Static / Puternic / Implicit Common Lisp - Dinamic / Puternic / Implicit

Poate am gresit undeva, mai ales cu CL, PHP si Obj-C, daca aveti o alta parere despre vreo limba, scrieti in comentarii.

Simplitatea tastării în abordarea OO este o consecință a simplității modelului de calcul obiect. Omițând detalii, putem spune că în timpul execuției unui sistem OO, are loc un singur tip de eveniment - un apel de caracteristică:


care denotă operația f deasupra obiectului atașat X, cu trecerea argumentului arg(posibil argumente multiple sau deloc). Programatorii Smalltalk vorbesc în acest caz despre „trecerea unui obiect X mesaje f cu un argument arg„, dar aceasta este doar o diferență de terminologie și, prin urmare, nu este semnificativă.

Faptul că totul se bazează pe acest Construct de bază explică o parte a sentimentului de frumusețe în ideile OO.

Din Construcția de bază urmează acele situații anormale care pot apărea în procesul de execuție:

Definiție: încălcarea tipului

O încălcare a tipului de rulare sau doar o încălcare a tipului, pe scurt, are loc în momentul apelului. x.f(arg), Unde X atașat unui obiect OBJîn cazul în care fie:

[X]. nu există nicio componentă potrivită fși aplicabil la OBJ,

[X]. există o astfel de componentă, însă, argumentul arg inacceptabil pentru el.

Problema de tastare este de a evita situații ca aceasta:

Problema de tastare pentru sistemele OO

Când constatăm că poate apărea o încălcare a tipului în execuția unui sistem OO?

Cuvântul cheie este când. Mai devreme sau mai târziu îți vei da seama că există o încălcare de tip. De exemplu, încercarea de a executa componenta „Torpedo Launch” pe un obiect „Angajat” nu va funcționa și execuția va eșua. Cu toate acestea, este posibil să preferați să găsiți erori cât mai devreme posibil decât mai târziu.

Tastare statică și dinamică

Deși sunt posibile opțiuni intermediare, aici sunt prezentate două abordări principale:

[X]. Tastare dinamică: așteptați finalizarea fiecărui apel și apoi luați o decizie.

[X]. Tastare statică: dat un set de reguli, determinați din textul sursă dacă sunt posibile încălcări de tip în timpul execuției. Sistemul este executat dacă regulile garantează că nu există erori.

Acești termeni sunt ușor de explicat: cu tastarea dinamică, verificarea tipului are loc în timpul funcționării sistemului (dinamic), în timp ce cu tastarea statică, verificarea tipului se realizează pe text static (înainte de execuție).

Tastarea statică implică verificarea automată, care este de obicei responsabilitatea compilatorului. Ca urmare, avem o definiție simplă:

Definiție: limbaj tipizat static

Un limbaj OO este tipizat static dacă vine cu un set de reguli consistente, verificate de compilator, care asigură că execuția sistemului nu duce la încălcări de tip.

Termenul " puternic tastare" ( puternic). Ea corespunde naturii ultimatum a definiției, necesitând absența completă a încălcării tipului. Posibil și slab (slab) forme de tastare statică, în care regulile elimină anumite încălcări fără a le elimina în întregime. În acest sens, unele limbi OO sunt tastate slab static. Vom lupta pentru cea mai puternică tastare.

În limbile tipizate dinamic, cunoscute sub numele de limbaje netipizate, nu există declarații de tip și orice valoare poate fi atașată entităților în timpul execuției. Verificarea tipului static nu este posibilă în ele.

Reguli de tastare

Notația noastră OO este tipizată static. Regulile ei tip au fost introduse în prelegerile anterioare și se reduc la trei cerințe simple.

[X]. Când se declară fiecare entitate sau funcție, trebuie specificat tipul acesteia, de exemplu, acc: CONT. Fiecare subrutină are 0 sau mai multe argumente formale, al căror tip trebuie dat, de exemplu: pune(x: G; i: INTEGER).

[X].În orice misiune x:= yşi la orice apel de subrutină în care y este argumentul real pentru argumentul formal X, tip sursă y trebuie să fie compatibil cu tipul țintă X. Definiția compatibilității se bazează pe moștenire: B compatibil cu A, dacă este descendentul acestuia, - completat cu reguli pentru parametrii generici (vezi Lectura 14).

[X]. Apel x.f(arg) cere asta f a fost o componentă a clasei de bază pentru tipul țintă X, și f trebuie exportat în clasa în care apare apelul (vezi 14.3).

Realism

Deși definiția unui limbaj tipizat static este destul de precisă, nu este suficientă - sunt necesare criterii informale la crearea regulilor de tastare. Să luăm în considerare două cazuri extreme.

[X]. Limbajul perfect corect, în care fiecare sistem corect din punct de vedere sintactic este, de asemenea, corect de tip. Regulile de declarare a tipului nu sunt necesare. Există astfel de limbi (gândiți-vă la notația poloneză pentru adăugarea și scăderea numerelor întregi). Din păcate, niciun limbaj universal real nu îndeplinește acest criteriu.

[X]. Limbă complet greșită, care este ușor de creat luând orice limbă existentă și adăugând o regulă de tastare care face orice sistemul este incorect. Prin definiție, acest limbaj este tipat: deoarece nu există sisteme care se conformează regulilor, niciun sistem nu va cauza încălcări de tip.

Putem spune că limbile de primul tip potrivi, dar inutil, acesta din urmă poate fi util, dar nu potrivit.

În practică, este nevoie de un sistem de tip care să fie util și util în același timp: suficient de puternic pentru a îndeplini nevoile de calcul și suficient de convenabil pentru a nu ne obliga să mergem la complicație pentru a satisface regulile de dactilografiere.

Vom spune că limba realist dacă este utilizabil și util în practică. Spre deosebire de definiția tastării statice, care oferă un răspuns peremptoriu la întrebarea: „ X este scris static?„, definiția realismului este parțial subiectivă.

În această prelegere, ne vom asigura că notația pe care o propunem este realistă.

Pesimism

Tastarea statică duce prin natura sa la o politică „pesimistă”. O încercare de a garanta asta toate calculele nu duc la eșecuri, respinge calcule care s-ar putea termina fără erori.

Luați în considerare un limbaj obișnuit, neobiectiv, asemănător Pascal, cu diferite tipuri REALși ÎNTREG. Când descrii n: INTEGER; r: Real operator n:=r va fi respinsă ca încălcare a regulilor. Astfel, compilatorul va respinge toate următoarele afirmații:


Dacă le permitem să ruleze, vom vedea că [A] va funcționa întotdeauna, deoarece orice sistem numeric are o reprezentare exactă a numărului real 0,0, care se traduce fără ambiguitate în 0 numere întregi. [B] va funcționa aproape sigur. Rezultatul acțiunii [C] nu este evident (dorim să obținem rezultatul rotunjind sau eliminând partea fracțională?). [D] va face treaba, la fel ca și operatorul:


ifn^2< 0 then n:= 3.67 end [E]

care include o misiune de neatins ( n^2 este pătratul numărului n). După înlocuire n^2 pe n doar o serie de alergări va da rezultatul corect. Misiune n o valoare reală mare care nu poate fi reprezentată ca număr întreg va avea ca rezultat un eșec.

În limbile tipizate, toate aceste exemple (funcționează, nu funcționează, uneori funcționează) sunt tratate fără milă ca încălcări ale regulilor de descriere a tipurilor și respinse de orice compilator.

Întrebarea nu este vom fie că suntem pesimiști și, de fapt, cât costă ne putem permite să fim pesimiști. Revenind la cerința realismului: dacă regulile de tip sunt atât de pesimiste încât împiedică calculul să fie ușor de scris, le vom respinge. Dar dacă atingerea siguranței de tip se realizează printr-o mică pierdere a puterii expresive, le vom accepta. De exemplu, într-un mediu de dezvoltare care oferă funcții pentru rotunjirea și extragerea unei părți întregi - rundăși trunchia, operator n:=r considerat incorect, pe bună dreptate, pentru că vă obligă să scrieți în mod explicit conversia real-întreg, în loc să utilizați conversiile implicite ambigue.

Tastarea statică: cum și de ce

Deși beneficiile tastării statice sunt evidente, este bine să vorbim din nou despre ele.

Avantaje

Am enumerat motivele pentru utilizarea tastării statice în tehnologia obiectelor la începutul prelegerii. Este fiabilitatea, ușurința de înțelegere și eficiența.

Fiabilitate datorită detectării erorilor care altfel s-ar putea manifesta numai în timpul funcționării și numai în unele cazuri. Prima dintre reguli, care forțează declararea entităților, precum și a funcțiilor, introduce redundanță în textul programului, ceea ce permite compilatorului, folosind celelalte două reguli, să detecteze inconsecvențele între utilizarea intenționată și cea reală a entităților, componentelor și expresii.

Detectarea timpurie a erorilor este, de asemenea, importantă, deoarece cu cât întârziem găsirea lor, cu atât costul corectării va crește. Această proprietate, care este de înțeles intuitiv pentru toți programatorii profesioniști, este confirmată cantitativ de lucrările larg cunoscute ale lui Boehm. Dependența costului corecției de timpul de găsire a erorilor este prezentată într-un grafic construit pe baza unui număr de proiecte industriale mari și experimente efectuate cu un proiect mic controlat:

Orez. 17.1. Costuri comparative de remediere a erorilor (publicat cu permisiune)

Lizibilitate sau Ușurință de înțelegere(lizibilitatea) are avantajele sale. În toate exemplele din această carte, apariția unui tip pe o entitate oferă cititorului informații despre scopul acesteia. Lizibilitatea este extrem de importantă în etapa de întreținere.

In cele din urma, eficienţă poate determina succesul sau eșecul tehnologiei obiectelor în practică. În absența tastării statice la execuție x.f(arg) poate dura orice perioadă de timp. Motivul pentru aceasta este că, în timpul executării, nu se găsește fîn clasa țintă de bază X, căutarea va continua în urmașii săi, iar acesta este un drum sigur către ineficiență. Puteți atenua problema îmbunătățind căutarea unei componente în ierarhie. Autorii limbajului Self au depus multă muncă încercând să genereze cel mai bun cod pentru un limbaj tip dinamic. Dar tastarea statică a permis unui astfel de produs OO să se apropie sau să egaleze în eficiență cu software-ul tradițional.

Cheia pentru tastarea statică este ideea deja declarată că compilatorul care generează codul pentru construct x.f(arg), știe tipul X. Din cauza polimorfismului, nu este posibil să se determine în mod unic versiunea adecvată a componentei f. Dar declarația restrânge setul de tipuri posibile, permițând compilatorului să construiască un tabel care oferă acces la f la costuri minime, cu constantă limitată dificultate de acces. Optimizări suplimentare efectuate legarea staticăși înlocuiri (inline)- sunt facilitate si de tastarea statica, eliminand complet overhead acolo unde este cazul.

Argumente pentru tastarea dinamică

Cu toate acestea, tastarea dinamică nu își pierde adepții, în special printre programatorii Smalltalk. Argumentele lor se bazează în primul rând pe realismul discutat mai sus. Ei cred că tastarea statică este prea restrictivă, împiedicându-i să-și exprime liber ideile creative, numind uneori „centa de castitate”.

Se poate fi de acord cu această argumentare, dar numai pentru limbile tipizate static care nu acceptă o serie de caracteristici. Este de remarcat faptul că toate conceptele asociate conceptului de tip și introduse în prelegerile anterioare sunt necesare - respingerea oricăreia dintre ele este plină de restricții serioase, iar introducerea lor, dimpotrivă, oferă flexibilitate acțiunilor noastre și oferă ne oferă oportunitatea de a ne bucura pe deplin de caracterul practic al unei tastări statice.

Tipizare: componente ale succesului

Care sunt mecanismele pentru tastarea statică realistă? Toate au fost introduse în prelegerile anterioare și, prin urmare, ne rămâne doar să le amintim pe scurt. Enumerarea lor comună arată coerența și puterea asocierii lor.

Sistemul nostru de tip se bazează în întregime pe concept clasă. Clasele sunt chiar tipuri de bază ca ÎNTREGși, prin urmare, nu avem nevoie de reguli speciale pentru descrierea tipurilor predefinite. (Acesta este locul în care notația noastră diferă de limbajele „hibride” precum Object Pascal, Java și C++, unde sistemul de tip al limbilor vechi este combinat cu tehnologia obiectelor bazată pe clasă.)

Tipuri extinse oferiți-ne mai multă flexibilitate, permițând tipuri ale căror valori denotă obiecte, precum și tipuri ale căror valori denotă referințe.

Cuvântul decisiv în crearea unui sistem de tip flexibil îi aparține moştenireși conceptul aferent compatibilitate. Acest lucru depășește principala limitare a limbajelor tipizate clasice, de exemplu, Pascal și Ada, în care operatorul x:= y cere ca tipul Xși y a fost la fel. Această regulă este prea strictă: interzice utilizarea entităților care pot denota obiecte de tipuri înrudite ( CONT DE ECONOMIIși VERIFICAREA CONTULUI). În moștenire, avem nevoie doar de compatibilitate de tip y cu tip X, de exemplu, X are tip CONT, y- CONT DE ECONOMII, iar a doua clasă este succesorul primei.

În practică, un limbaj tipizat static are nevoie de sprijin moștenire multiplă. Acuzații fundamentale cunoscute de tastare statică că nu oferă posibilitatea de a interpreta obiectele în moduri diferite. Da, obiectul DOCUMENT(documentul) poate fi transmis prin rețea și, prin urmare, necesită prezența componentelor asociate tipului MESAJ(mesaj). Dar această critică este valabilă numai pentru limbile care sunt limitate la moștenirea singulară.

Orez. 17.2. Moștenirea multiplă

Versatilitate necesar, de exemplu, pentru a descrie structurile de date ale containerelor flexibile, dar sigure (de exemplu clasa LIST[G]...). Fără acest mecanism, tastarea statică ar necesita declararea unor clase diferite pentru liste cu diferite tipuri de elemente.

În unele cazuri, este necesară versatilitatea restrânge, care vă permite să utilizați operațiuni care se aplică numai entităților de tip generic. Dacă clasa generică SORTABLE_LIST acceptă sortarea, necesită entități de tip G, Unde G- un parametru generic, prezența unei operațiuni de comparare. Acest lucru se realizează prin conectarea la G clasa care definește constrângerea generică - COMPARABIL:


clasa SORTABLE_LIST...

Orice parametru generic real SORTABLE_LIST trebuie să fie un copil al unei clase COMPARABIL, care are componenta necesară.

Un alt mecanism esențial este încercare de atribuire- organizează accesul la acele obiecte, tipul cărora software-ul nu le controlează. În cazul în care un y este un obiect de bază de date sau un obiect obținut prin intermediul rețelei, apoi instrucțiunea x ?= y atribui X sens y, dacă y este de tip compatibil sau, dacă nu este, va da X sens Vidul.

Declarații, asociată ca parte a ideii Design by Contract cu clase și componentele acestora sub formă de precondiții, postcondiții și invarianți de clasă, fac posibilă descrierea constrângerilor semantice care nu sunt acoperite de o specificație de tip. Limbi precum Pascal și Ada au tipuri de intervale care pot limita valoarea unei entități între 10 și 20, de exemplu, dar nu le puteți folosi pentru a forța valoarea i a fost negativ, întotdeauna de două ori j. Invarianții de clasă vin în ajutor, proiectați să reflecte cu exactitate restricțiile introduse, indiferent cât de complexe ar fi acestea.

Anunțuri fixate sunt necesare pentru a evita duplicarea codului în practică. anunţând y: ca x, ai garanția că y se va modifica în urma oricăror declarații repetate de tip X la un descendent. În absența acestui mecanism, dezvoltatorii ar re-declara neîncetat, încercând să păstreze diferitele tipuri consistente.

Declarațiile lipicioase sunt un caz special al ultimului mecanism lingvistic de care avem nevoie - covarianta, despre care vom discuta mai detaliat ulterior.

La dezvoltarea sistemelor software, de fapt, este nevoie de încă o proprietate care este inerentă mediului de dezvoltare în sine - recompilare incrementală rapidă. Când scrieți sau modificați un sistem, doriți să vedeți efectul modificărilor cât mai curând posibil. Cu tastarea statică, trebuie să acordați timp compilatorului să verifice din nou tipurile. Rutinele tradiționale de compilare necesită recompilarea întregului sistem (și adunarea ei), iar acest proces poate fi dureros de lung, mai ales cu trecerea la sisteme la scară largă. Acest fenomen a devenit un argument în favoarea interpretarea sisteme, cum ar fi mediile incipiente Lisp sau Smalltalk, care rulau sistemul fără procesare sau deloc, fără verificare de tip. Acum acest argument este uitat. Un compilator modern bun detectează modul în care codul s-a schimbat de la ultima compilare și procesează doar modificările pe care le găsește.

„Copilul este tipizat”?

Scopul nostru - strict tastare statică. De aceea trebuie să evităm orice lacune în „jocul nostru după reguli”, cel puțin să le identificăm cu exactitate dacă există.

Cea mai comună lacună în limbile tipizate static este existența conversiilor care schimbă tipul unei entități. În C și în limbajele sale derivate, ele sunt numite „turnare de tip” sau turnare. Înregistrare (OTHER_TYPE) x indică faptul că valoarea X este perceput de compilator ca având tipul OTHER_TYPE, sub rezerva anumitor restricții privind tipurile posibile.

Mecanisme ca acesta ocolesc limitările verificării tipului. Castingul este obișnuit în programarea C, inclusiv în dialectul ANSI C. Chiar și în C++, castingul de tip, deși mai puțin obișnuit, rămâne obișnuit și poate necesar.

Respectarea regulilor de tastare statică nu este atât de ușoară, dacă în orice moment pot fi ocolite prin turnare.

Tastarea și legarea

Deși, în calitate de cititor al acestei cărți, veți distinge cu siguranță tastarea statică de cea statică legare Ei bine, sunt oameni care nu pot face asta. Acest lucru se poate datora parțial influenței limbajului Smalltalk, care pledează pentru o abordare dinamică a ambelor probleme și poate duce la concepția greșită că au aceeași soluție. (În această carte susținem că este de dorit să combinați tastarea statică și legăturile dinamice pentru a crea programe robuste și flexibile.)

Atât tastarea, cât și legarea se ocupă de semantica Constructului de bază x.f(arg) dar răspunde la două întrebări diferite:

Tastarea și legarea

[X]. O întrebare despre tastare: când trebuie să știm cu siguranță că o operațiune corespunzătoare f Aplicabil obiectului atașat entității X(cu parametru arg)?

[X]. Întrebare de legătură: Când trebuie să știm ce operație inițiază un anumit apel?

Tastarea răspunde la întrebarea de disponibilitate cel puțin unul operațiuni, legarea este responsabilă de selectare necesar.

În cadrul abordării obiectului:

[X]. Problema cu tastarea este legată de polimorfism: pentru că Xîn timpul rulării poate desemna obiecte de mai multe tipuri diferite, trebuie să fim siguri că operația reprezentând f, disponibilîn fiecare dintre aceste cazuri;

[X]. problema legată cauzată anunţuri repetate: deoarece o clasă poate modifica componentele moștenite, pot exista două sau mai multe operații care pretind că le reprezintă fîn acest apel.

Ambele probleme pot fi rezolvate atât dinamic, cât și static. Limbile existente oferă toate cele patru soluții.

[X]. O serie de limbaje non-obiective, cum ar fi Pascal și Ada, implementează atât tastarea statică, cât și legarea statică. Fiecare entitate reprezintă obiecte de un singur tip definit static. Acest lucru asigură fiabilitatea soluției, prețul pentru care este flexibilitatea acesteia.

[X]. Smalltalk și alte limbi OO conțin legături dinamice și tastare dinamică. Se preferă flexibilitatea în detrimentul fiabilității limbii.

[X]. Unele limbi non-obiective acceptă tastarea dinamică și legarea statică. Printre acestea se numără limbaje de asamblare și o serie de limbaje de scripting.

[X]. Ideile de tastare statică și de legare dinamică sunt întruchipate în notația propusă în această carte.

Rețineți particularitatea limbajului C ++, care acceptă tastarea statică, deși nu este strictă din cauza prezenței tipului de turnare, legarea statică (în mod implicit), legarea dinamică când este virtuală ( virtual) reclame.

Motivul pentru care alegeți tastarea statică și legarea dinamică este evident. Prima întrebare este: „Când vom ști despre existența componentelor?” - sugerează un răspuns static: " Cu cât mai devreme, cu atât mai bine", ceea ce înseamnă: la momentul compilării. A doua întrebare, "Ce componentă să utilizați?" sugerează un răspuns dinamic: " cel de care ai nevoie", corespunzătoare tipului dinamic al obiectului determinat în timpul rulării. Aceasta este singura soluție acceptabilă dacă legarea statică și dinamică produc rezultate diferite.

Următorul exemplu de ierarhie de moștenire va ajuta la clarificarea acestor concepte:

Orez. 17.3. Tipuri de aeronave

Luați în considerare apelul:


aeronava_mea.trenul_de_aterizare inferior

O întrebare despre tastare: când să vă asigurați că o componentă va fi aici trenul_de_aterizare inferior("eliberarea trenului de aterizare"), aplicabil unui obiect (pentru COPTER nu va fi deloc) Problema legării: care dintre mai multe versiuni posibile să o alegem.

Legarea statică ar însemna că ignorăm tipul obiectului atașat și ne bazăm pe declarația entității. Ca urmare, atunci când avem de-a face cu un Boeing 747-400, am solicita o versiune concepută pentru avioanele convenționale din seria 747, și nu pentru modificarea lor 747-400. Legarea dinamică aplică operația cerută de obiect și aceasta este abordarea corectă.

Cu tastarea statică, compilatorul nu va respinge un apel dacă se poate garanta că atunci când execută programul către entitate aeronava_mea va fi atașat obiectul furnizat cu componenta corespunzătoare trenul_de_aterizare inferior. Tehnica de bază pentru obținerea garanțiilor este simplă: cu declarație obligatorie aeronava_mea clasa de bază a tipului său este necesară pentru a include o astfel de componentă. De aceea aeronava_mea nu poate fi declarat ca AERONAVE, întrucât acesta din urmă nu are trenul_de_aterizare inferior la acest nivel; elicopterele, cel puțin în exemplul nostru, nu știu cum să elibereze trenul de aterizare. Dacă declarăm o entitate ca AVION, - clasa care conține componenta necesară - totul va fi bine.

Tastarea dinamică în stilul Smalltalk necesită să așteptați apelul și, în momentul executării acestuia, să verificați prezența componentei dorite. Acest comportament este posibil pentru prototipuri și dezvoltări experimentale, dar inacceptabil pentru sistemele industriale - la momentul zborului este prea târziu să întrebi dacă ai un tren de aterizare.

Covarianța și ascunderea copiilor

Dacă lumea ar fi simplă, atunci conversația despre tastare ar putea fi încheiată. Am identificat obiectivele și beneficiile tastării statice, am examinat constrângerile pe care trebuie să le îndeplinească sistemele de tip realiste și am verificat că metodele de tastare propuse îndeplinesc criteriile noastre.

Dar lumea nu este simplă. Combinarea tastării statice cu unele cerințe de inginerie software creează probleme mai complexe decât se vede. Problemele sunt cauzate de două mecanisme: covarianta- modificarea tipurilor de parametri la redefinire, descendent ascunzându-se- capacitatea unei clase descendente de a restricționa statutul de export al componentelor moștenite.

covarianta

Ce se întâmplă cu argumentele unei componente atunci când tipul acesteia este redefinit? Aceasta este o problemă majoră și am văzut deja o serie de exemple în acest sens: dispozitive și imprimante, liste conectate individual și dublu și așa mai departe (vezi Secțiunile 16.6, 16.7).

Iată un alt exemplu pentru a ajuta la clarificarea naturii problemei. Și deși este departe de realitate și metaforică, apropierea sa de schemele de programe este evidentă. În plus, atunci când îl analizăm, vom reveni adesea la problemele din practică.

Imaginează-ți o echipă de schi universitară care se pregătește pentru un campionat. Clasă FATĂ include schiori care fac parte din echipa feminină, BĂIAT- schiori. Un număr de participanți ai ambelor echipe sunt clasați, arătând rezultate bune în competițiile anterioare. Acest lucru este important pentru ei, pentru că acum vor alerga primii, câștigând un avantaj față de ceilalți. (Această regulă, care îi privilegiază pe cei deja privilegiați, este poate ceea ce face slalomul și schiul de fond atât de atractive în ochii multor oameni, fiind o bună metaforă a vieții însăși.) Deci avem două clase noi: RANKED_GIRLși RANKED_BOY.

Orez. 17.4. Clasificarea schiorilor

Au fost rezervate o serie de camere pentru cazarea sportivilor: doar pentru bărbați, doar pentru fete, doar pentru femei câștigătoare. Pentru a afișa acest lucru, folosim o ierarhie de clasă paralelă: CAMERĂ, CAMERA_FATĂși RANKED_GIRL_ROOM.

Iată schița clasei SCHIOR:


- Colega de cameră.
... Alte componente posibile omise în această clasă și în clasele ulterioare...

Ne interesează două componente: atributul coleg de cameră si procedura acțiune, care „plasează” acest schior în aceeași cameră cu schiorul actual:


La declararea unei entitati alte puteți renunța la tipul SCHIORîn favoarea tipului fix ca colegul de cameră(sau precum Current pentru coleg de camerăși alte simultan). Dar să uităm pentru un moment de fixarea tipului (vom reveni la ele mai târziu) și să privim problema covarianței în forma sa originală.

Cum se introduce suprascrierea tipului? Regulile necesită reședința separată a băieților și fetelor, a câștigătorilor și a altor participanți. Pentru a rezolva această problemă, la redefinire, vom schimba tipul componentei coleg de cameră, după cum se arată mai jos (în continuare, elementele suprascrise sunt subliniate).


- Colega de cameră.

Redefiniți, respectiv, argumentul procedurii acțiune. O versiune mai completă a clasei arată acum astfel:


- Colega de cameră.
-- Selectați altul ca vecin.

În mod similar, ar trebui să modificați toate cele generate de SCHIOR clase (nu folosim tip fixing acum). Ca urmare, avem o ierarhie:

Orez. 17.5. Ierarhia membrilor și redefiniții

Deoarece moștenirea este o specializare, regulile de tip impun ca atunci când se suprascrie rezultatul unei componente, în acest caz coleg de cameră, noul tip a fost un copil al originalului. Același lucru este valabil și pentru redefinirea tipului de argument. alte rutine acțiune. Această strategie, după cum știm, se numește covarianță, unde prefixul „ko” indică o modificare comună a tipurilor de parametru și rezultat. Strategia opusă se numește contravarianta.

Toate exemplele noastre demonstrează în mod convingător necesitatea practică a covarianței.

[X]. Element de listă legat individual LINKABLE trebuie asociat cu un alt element similar, iar instanța BI_LINKABLE- cu unul asemanator. Covariantul va trebui să fie suprascris și argumentul în pus corect.

[X]. Fiecare subrutină în LINKED_LIST cu argument tip LINKABLE la mutarea la TWO_WAY_LIST va necesita un argument BI_LINKABLE.

[X]. Procedură set_alternate acceptă DISPOZITIV-argumentare in clasa DISPOZITIVși IMPRIMANTA-argument - în clasă IMPRIMANTA.

Redefinirea covariantă este deosebit de populară deoarece ascunderea informațiilor duce la crearea de proceduri ale formei


-- Setați atributul la v.

a lucra cu atribut tip SOME_TYPE. Astfel de proceduri sunt, desigur, covariante, deoarece orice clasă care schimbă tipul unui atribut trebuie să redefinească argumentul în consecință. set_attrib. Deși exemplele prezentate se încadrează într-o singură schemă, covarianța este mult mai răspândită. Gândiți-vă, de exemplu, la o procedură sau la o funcție care realizează concatenarea listelor legate individual ( LINKED_LIST). Argumentul său trebuie redefinit ca o listă dublu legată ( TWO_WAY_LIST). Operație de adăugare universală infix "+" acceptă NUMERIC-argumentare in clasa NUMERIC, REAL- in clasa REALși ÎNTREG- in clasa ÎNTREG. În ierarhii de servicii telefonice paralele, procedură start in clasa PHONE_SERVICE poate fi necesar un argument ABORDARE, reprezentând adresa abonatului, (pentru facturare), în timp ce aceeași procedură în clasă CORPORATE_SERVICE este necesar un argument de tip CORPORATE_ADDRESS.

Orez. 17.6. Servicii de comunicare

Ce se poate spune despre soluția contravariantă? În exemplul schiorului, ar însemna că dacă, trecerea la clasă RANKED_GIRL, tip de rezultat coleg de cameră redefinit ca RANKED_GIRL, apoi, din cauza contravariantei, tipul argumentului acțiune poate fi redefinit la tip FATĂ sau SCHIOR. Singurul tip care nu este permis în cadrul soluției contravariante este RANKED_GIRL! Suficient cât să trezească cele mai rele suspiciuni în părinții fetelor.

Ierarhii paralele

Pentru a nu lăsa piatra neîntorsă, luați în considerare o variantă a exemplului SCHIOR cu două ierarhii paralele. Acest lucru ne va permite să simulăm o situație care a fost deja întâlnită în practică: TWO_WAY_LIST > LINKED_LISTși BI_LINKABLE > LINKABLE; sau ierarhie cu serviciul telefonic PHONE_SERVICE.

Să existe o ierarhie cu o clasă CAMERĂ, al cărui descendent este CAMERA_FATĂ(Clasă BĂIAT omis):

Orez. 17.7. Schiori și camere

Clasele noastre de schiori în această ierarhie paralelă în loc de coleg de camerăși acțiune va avea componente similare cazare (cazare) și găzdui (loc):


descriere: „O nouă variantă cu ierarhii paralele”
acomoda (r: CAMERA) este ... necesita ... face

De asemenea, aici sunt necesare suprascrieri covariante: în clasă FATA1 Cum cazare, și argumentul subrutinei găzdui trebuie înlocuit cu tipul CAMERA_FATĂ, in clasa BĂIAT1- tip BOY_ROOM etc. (Nu uitați, încă lucrăm fără fixarea tipului.) Ca și în versiunea anterioară a exemplului, contravarianța este inutilă aici.

Capacitatea polimorfismului

Nu există suficiente exemple care confirmă caracterul practic al covarianței? De ce ar lua cineva în considerare contravarianța care intră în conflict cu ceea ce este necesar în practică (în afară de comportamentul unor tineri)? Pentru a înțelege acest lucru, luați în considerare problemele care apar atunci când combinați polimorfismul și strategia de covarianță. Elaborarea unei scheme de sabotaj nu este dificilă și este posibil să fi creat-o deja singur:


creați b; creați g;-- Creați obiecte BOY și GIRL.

Rezultatul ultimului apel, foarte posibil pe placul tinerilor, este exact ceea ce încercam să prevenim prin tip overriding. Apel acțiune duce la faptul că obiectul BĂIAT, cunoscut ca bși datorită polimorfismului a primit un alias s tip SCHIOR, devine un vecin al obiectului FATĂ cunoscută după nume g. Cu toate acestea, apelul, deși contrar regulilor hostelului, este destul de corect în textul programului, deoarece acțiune-componenta exportata in compozitie SCHIOR, A FATĂ, tip argument g, compatibil cu SCHIOR, tipul parametrului formal acțiune.

Schema de ierarhie paralelă este la fel de simplă: înlocuiți SCHIOR pe SCHIOR1, provocare acțiune- in asteptare s.acomoda (gr), Unde gr- entitate de tip CAMERA_FATĂ. Rezultatul este același.

Cu o soluție contravariantă la aceste probleme, nu ar exista: specializarea țintei apelului (în exemplul nostru s) ar necesita o generalizare a argumentului. Ca urmare, contravarianța duce la un model matematic mai simplu al mecanismului: moștenire - redefinire - polimorfism. Acest fapt este descris într-un număr de articole teoretice care propun această strategie. Argumentul nu este foarte convingător, deoarece, după cum arată exemplele noastre și alte publicații, contravarianța nu are nicio utilitate practică.

Prin urmare, fără a încerca să trageți hainele contravariante pe corpul covariant, ar trebui să acceptați realitatea covariantă și să căutați modalități de a elimina efectul nedorit.

Ascuns de descendent

Înainte de a căuta o soluție la problema covarianței, luați în considerare un alt mecanism care poate duce la încălcări de tip în condiții de polimorfism. Ascunderea descendenților este capacitatea unei clase de a nu exporta o componentă primită de la părinții săi.

Orez. 17.8. Ascuns de descendent

Un exemplu tipic este componenta add_vertex(adăugați vârf) exportate de clasă POLIGON, dar ascuns de descendentul său DREPTUNGHI(având în vedere posibila încălcare a invariantului - clasa vrea să rămână dreptunghi):


Exemplu de non-programare: clasa „Struț” ascunde metoda „Zboară”, primită de la părintele „Pasare”.

Să luăm această schemă așa cum este pentru un moment și să ne întrebăm dacă o combinație de moștenire și ascundere ar fi legitimă. Rolul de modelare al ascunzării, ca și covarianța, este încălcat de trucuri care sunt posibile datorită polimorfismului. Și aici nu este dificil să construiești un exemplu rău intenționat care să permită, în ciuda ascunderii componentei, să o numești și să adaugi un vârf la dreptunghi:


creator; -- Creați un obiect RECTANGLE.
p:=r; -- Atribuire polimorfă.

Din moment ce obiectul r ascunzându-se sub esenţă p clasă POLIGON, A add_vertex componenta exportata POLIGON, apoi apelul său de către entitate p corect. Ca rezultat al execuției, va apărea încă un vârf în dreptunghi, ceea ce înseamnă că va fi creat un obiect nevalid.

Corectitudinea sistemelor și claselor

Pentru a discuta problemele de covarianță și ascunderea descendenților, avem nevoie de câțiva termeni noi. Vom suna clasa-corectă (clasa-validă) un sistem care satisface cele trei reguli de descriere a tipurilor date la începutul prelegerii. Amintiți-le: fiecare entitate are propriul ei tip; tipul argumentului propriu-zis trebuie să fie compatibil cu tipul argumentului formal, situația este similară cu atribuirea; componenta apelată trebuie să fie declarată în clasa sa și exportată în clasa care conține apelul.

Sistemul este numit corect de sistem (valid de sistem), dacă nu are loc nicio încălcare a tipului în timpul executării acestuia.

În mod ideal, ambele concepte ar trebui să se potrivească. Cu toate acestea, am văzut deja că un sistem corect din punct de vedere al clasei în condițiile moștenirii, covarianței și ascunsării de către un descendent poate să nu fie corect din punct de vedere al sistemului. Să numim această eroare eroare de valabilitate a sistemului.

Aspect practic

Simplitatea problemei creează un fel de paradox: un începător curios poate construi un contraexemplu în câteva minute, în practică reală, erorile de corectitudine de clasă a sistemelor apar zi de zi, dar încălcările corectitudinii sistemului, chiar și în cazuri mari, multiple. proiecte de an, apar extrem de rar.

Cu toate acestea, acest lucru nu ne permite să le ignorăm și, prin urmare, începem să studiem trei modalități posibile de a rezolva această problemă.

În continuare, vom atinge aspecte foarte subtile și nu atât de des manifeste ale abordării obiectului. Dacă citiți această carte pentru prima dată, poate doriți să săriți peste secțiunile rămase ale acestei prelegeri. Dacă sunteți nou în tehnologia OO, veți înțelege mai bine acest material după ce ați studiat cursurile 1-11 ale cursului „Fundamentals of Object-Oriented Design”, dedicat metodologiei moștenirii și, în special, prelegerea 6 a cursului „Fundamentals of Object-Oriented Design”, dedicată moștenirii metodologiei.

Corectitudinea sistemelor: prima aproximare

Să ne concentrăm mai întâi pe problema covarianței, cea mai importantă dintre cele două. Există o literatură extinsă dedicată acestui subiect, oferind o serie de soluții diferite.

Contravarianță și invarianță

Contravarianța elimină problemele teoretice asociate cu încălcarea corectitudinii sistemului. Cu toate acestea, acest lucru pierde realismul sistemului de tip, din acest motiv, nu este nevoie să luăm în considerare această abordare în continuare.

Originalitatea limbajului C++ este că folosește strategia novarie, împiedicându-vă să schimbați tipul de argumente în subrutinele suprascrise! Dacă C++ ar fi un limbaj puternic tipizat, sistemul său de tipări ar fi dificil de utilizat. Cea mai simplă soluție la problema în acest limbaj, precum și ocolirea altor limitări ale C ++ (să zicem, lipsa universalității limitate), este să folosiți turnare - tip turnare, care vă permite să ignorați complet mecanismul de tastare existent. Această soluție nu pare atractivă. Rețineți, totuși, că o serie de propuneri discutate mai jos se vor baza pe lipsa de varianță, al cărei sens va fi dat de introducerea de noi mecanisme de lucru cu tipuri în loc de redefinirea covariantă.

Utilizarea parametrilor generici

Universalitatea se află în centrul unei idei interesante propuse pentru prima dată de Franz Weber. Să declarăm o clasă SCHIOR1, restricționând genericizarea parametrilor generici la clasă CAMERĂ:


caracteristica clasa SKIER1
acomoda (r: G) este ... necesită ... do acomodare:= r sfârşit

Apoi clasa FATA1 va fi mostenitorul SCHIOR1 etc. Aceeași tehnică, oricât de ciudată ar părea la prima vedere, poate fi folosită în absența unei ierarhii paralele: clasa SCHIOR.

Această abordare rezolvă problema covarianței. Orice utilizare a clasei trebuie să specifice parametrul generic real CAMERĂ sau CAMERA_FATĂ, astfel încât combinația greșită devine pur și simplu imposibilă. Limbajul devine fără variante, iar sistemul răspunde pe deplin nevoilor de covarianță datorită parametrilor generici.

Din păcate, această tehnică este inacceptabilă ca soluție generală, deoarece duce la o listă tot mai mare de parametri generici, câte unul pentru fiecare tip de posibil argument covariant. Mai rău, adăugarea unei subrutine covariante cu un argument al cărui tip nu este în listă ar necesita adăugarea unui parametru generic de clasă și, prin urmare, ar schimba interfața clasei, provocând schimbarea tuturor clienților clasei, ceea ce este inacceptabil.

Variabile de tip

O serie de autori, printre care Kim Bruce, David Shang și Tony Simons, au venit cu o soluție bazată pe variabile de tip ale căror valori sunt tipuri. Ideea lor este simplă:

[X].în loc de suprascrieri de covariantă, permiteți declarațiile de tip care folosesc variabile de tip;

[X]. extindeți regulile de compatibilitate de tip pentru a gestiona astfel de variabile;

[X]. oferă posibilitatea de a atribui variabile de tip ca valori tipurilor de limbă.

Cititorii pot găsi o prezentare detaliată a acestor idei într-o serie de articole pe această temă, precum și în publicațiile lui Cardelli (Cardelli), Castagna (Castagna), Weber (Weber) și alții. Nu ne vom ocupa de această problemă și iată de ce.

[X]. Un mecanism de variabilă de tip implementat corespunzător se încadrează în categoria de a permite unui tip să fie utilizat fără a-l specifica complet. Aceeași categorie include versatilitatea și ancorarea reclamelor. Acest mecanism ar putea înlocui alte mecanisme din această categorie. La început, acest lucru poate fi interpretat în favoarea variabilelor de tip, dar rezultatul poate fi dezastruos, deoarece nu este clar dacă acest mecanism cuprinzător poate gestiona toate sarcinile cu ușurința și simplitatea care sunt inerente în generalitatea și fixarea tipului.

[X]. Să presupunem că este dezvoltat un mecanism de tip variabilă care poate depăși problemele combinării covarianței și polimorfismului (ignorând încă problema ascunderii descendenților). Apoi, dezvoltatorul clasei va avea nevoie intuiție extraordinară pentru a decide în prealabil care dintre componente va fi disponibilă pentru redefinirea tipurilor din clasele derivate și care nu. Mai jos vom discuta această problemă, care are loc în practica creării de programe și, din păcate, pune la îndoială aplicabilitatea multor scheme teoretice.

Acest lucru ne obligă să revenim la mecanismele deja luate în considerare: universalitatea limitată și nerestricționată, fixarea tipului și, bineînțeles, moștenirea.

Bazându-se pe fixarea tipului

Vom găsi o soluție aproape gata făcută la problema covarianței analizând mecanismul declarațiilor fixate cunoscut de noi.

Când descriu clasele SCHIORși SCHIOR1 nu puteai să nu fii vizitat de dorința, folosind anunțurile fixe, de a scăpa de multe redefiniri. Fixarea este un mecanism covariant tipic. Iată cum ar arăta exemplul nostru (toate modificările sunt subliniate):


share (altul: ca Current) este ... cere ... face
acomoda (r: ca cazarea) este ... solicita ... face

Acum copiii pot părăsi clasa SCHIOR neschimbat, și SCHIOR1 vor trebui doar să suprascrie atributul cazare. Entități fixate: atribut coleg de camerăși argumentele subrutinei acțiuneși găzdui- se va schimba automat. Acest lucru simplifică foarte mult munca și confirmă faptul că în absența ancorării (sau a altui mecanism similar, cum ar fi variabilele de tip), este imposibil să scrieți un produs OO cu tastare realistă.

Dar ați reușit să eliminați încălcările corectitudinii sistemului? Nu! Putem, ca și înainte, să depășim verificarea tipului, făcând atribuiri polimorfe care cauzează încălcări ale corectitudinii sistemului.

Adevărat, versiunile originale ale exemplelor vor fi respinse. Lăsa:


creați b;creați g;-- Creați obiecte BOY și GIRL.
s:=b; -- Atribuire polimorfă.

Argument g transmise acțiune, este acum incorectă deoarece necesită un obiect de tip îi place, și clasa FATĂ nu este compatibil cu acest tip, deoarece prin regula tipurilor fixe niciun tip nu este compatibil cu îi place cu excepția lui însuși.

Cu toate acestea, nu ne bucurăm mult. Pe de altă parte, această regulă spune că îi place compatibil cu tipul s. Deci, folosind polimorfismul nu numai al obiectului s, dar și parametrul g, putem ocoli din nou sistemul de verificare a tipului:


s: SCHIOR; b: BĂIAT; g: like-uri; actual_g:FATĂ;
creați b; create actual_g -- Creează obiecte BOY și GIRL.
s:= actual_g; g:= s -- Adăugați g la GIRL prin s.
s:= b -- Atribuire polimorfă.

Drept urmare, apelul ilegal trece.

Există o cale de ieșire. Dacă suntem serioși să folosim fixarea declarației ca singur mecanism de covarianță, atunci putem scăpa de încălcările corectitudinii sistemice interzicând complet polimorfismul entității fixate. Acest lucru va necesita o schimbare a limbii: introduceți un nou cuvânt cheie ancoră(avem nevoie de această construcție ipotetică doar pentru a o folosi în această discuție):


Să permitem declarații de genul îi place Doar cand s descris ca ancoră. Să modificăm regulile de compatibilitate pentru a ne asigura: sși elemente de tip îi place pot fi atașate doar (în sarcini sau în trecerea argumentelor) unul altuia.

Cu această abordare, eliminăm din limbaj posibilitatea de a redefini tipul oricăror argumente de subrutină. În plus, am putea interzice redefinirea tipului de rezultat, dar acest lucru nu este necesar. Abilitatea de a suprascrie tipul de atribut este, desigur, păstrată. Toate redefinirile tipurilor de argument se vor face acum implicit prin mecanismul de fixare declansat de covarianta. Unde, cu abordarea anterioară, clasa D a suprascris componenta moștenită ca:


în timp ce clasa C- părinte D Arăta


Unde Y a corespuns X, acum redefinind componenta r va arata asa:


Rămâne în clasă D tip de anulare ancora_ta.

Această soluție la problema covarianței - polimorfism va fi numită abordare Ancorare. Ar fi mai corect să spunem: „Covarianță numai prin fixare”. Proprietățile abordării sunt atractive:

[X]. Fixarea se bazează pe ideea de separare strictă covariantăși elemente potențial polimorfe (sau, pe scurt, polimorfe). Toate entitățile declarate ca ancoră sau ca o_ancoră covariantă; altele sunt polimorfe. În fiecare dintre cele două categorii sunt permise orice atașamente, dar nu există nicio entitate sau expresie care să încalce limita. Nu puteți, de exemplu, să atribuiți o sursă polimorfă unei ținte covariante.

[X]. Această soluție simplă și elegantă este ușor de explicat chiar și începătorilor.

[X]. Elimină complet posibilitatea încălcării corectitudinii sistemului în sistemele construite covariant.

[X]. Ea păstrează cadrul conceptual stabilit mai sus, inclusiv conceptele de universalitate limitată și nelimitată. (Ca urmare, această soluție, în opinia mea, este de preferat variabilelor de tip care înlocuiesc mecanismele de covarianță și universalitate, menite să rezolve diverse probleme practice.)

[X]. Necesită o schimbare minoră a limbii - adăugarea unui singur cuvânt cheie reflectat în regula de potrivire - și nu implică dificultăți apreciabile de implementare.

[X]. Este realist (cel puțin în teorie): orice sistem posibil anterior poate fi rescris prin înlocuirea înlocuirilor covariante cu redeclarări ancorate. Este adevărat că unele atașamente vor fi invalide ca urmare, dar corespund cazurilor care pot duce la încălcări de tip și, prin urmare, ar trebui înlocuite cu încercări de atribuire și tratate în timpul execuției.

S-ar părea că discuția se poate termina aici. Deci, de ce abordarea Anchoring nu este pe deplin pe placul nostru? În primul rând, nu am atins încă problema ascunderii copiilor. În plus, motivul principal pentru continuarea discuției este problema deja exprimată în mențiunea scurtă a variabilelor de tip. Împărțirea sferelor de influență într-o parte polimorfă și covariantă este oarecum similară cu rezultatul Conferinței de la Yalta. Se presupune că proiectantul clasei are o intuiție extraordinară, că este capabil, pentru fiecare entitate pe care o introduce, în special pentru fiecare argument, să aleagă una dintre cele două posibilități odată pentru totdeauna:

[X]. O entitate este potențial polimorfă: acum sau mai târziu (prin trecerea de parametri sau prin atribuire) poate fi atașată unui obiect al cărui tip diferă de cel declarat. Tipul de entitate original nu poate fi modificat de niciun descendent al clasei.

[X]. O entitate este subiectul suprascrierii tipului, adică fie este ancorată, fie este ea însăși un pivot.

Dar cum poate un dezvoltator să prevadă toate acestea? Toată atractivitatea metodei OO, exprimată în multe feluri în principiul Deschis-Închis, se datorează tocmai posibilității de modificări pe care avem dreptul să le facem lucrărilor efectuate anterior, precum și faptului că dezvoltatorul de soluții universale nu trebuie să aibă o înțelepciune infinită, înțelegând cum produsul său poate fi adaptat nevoilor lor de către descendenți.

Cu această abordare, redefinirea tipurilor și ascunderea de către descendenți este un fel de „supapă de siguranță” care face posibilă reutilizarea unei clase existente care este aproape potrivită pentru scopurile noastre:

[X]. Apelând la redefinirea tipului, putem modifica declarațiile din clasa derivată fără a afecta originalul. În acest caz, o soluție pur covariantă va necesita editarea originalului prin transformările descrise.

[X]. Ascunderea de către un descendent protejează împotriva multor eșecuri la crearea unei clase. Este posibil să criticăm un proiect în care DREPTUNGHI, folosind faptul că este un descendent POLIGON, încearcă să adauge un vârf. În schimb, s-ar putea propune o structură de moștenire în care figurile cu un număr fix de vârfuri sunt separate de toate celelalte, iar problema nu ar apărea. Cu toate acestea, atunci când se proiectează structuri de moștenire, este întotdeauna de preferat să aibă cele care nu au excepții taxonomice. Dar pot fi ele eliminate complet? Când discutăm despre restricțiile de export într-una dintre prelegerile următoare, vom vedea că acest lucru nu este posibil din două motive. Prima este prezența criteriilor de clasificare concurente. În al doilea rând, probabilitatea ca dezvoltatorul să nu găsească soluția ideală, chiar dacă aceasta există.

Analiza globală

Această secțiune este dedicată descrierii abordării intermediare. Principalele soluții practice sunt prezentate în prelegerea 17 .

În timp ce am explorat opțiunea de fixare, am observat că ideea sa principală a fost de a separa seturile de entități covariante și polimorfe. Deci, dacă luăm două instrucțiuni din formular


fiecare dintre ele este un exemplu de aplicare corectă a mecanismelor OO importante: primul - polimorfism, al doilea - redefinirea tipului. Problemele încep când le combinăm pentru aceeași entitate s. În mod similar:


problemele încep cu unirea a doi operatori independenți și complet nevinovați.

Apelurile eronate duc la încălcări de tip. În primul exemplu, atribuirea polimorfă atașează un obiect BĂIAT la esență s, ce face g argument invalid acțiune, deoarece este asociat cu un obiect FATĂ. În al doilea exemplu către entitate r obiectul este atașat DREPTUNGHI, care exclude add_vertex din componentele exportate.

Iată ideea unei noi soluții: în prealabil - static, atunci când verificăm tipuri de un compilator sau alte instrumente - definim tipărit fiecare entitate, inclusiv tipurile de obiecte cu care entitatea poate fi asociată în timpul execuției. Apoi, din nou static, ne asigurăm că fiecare apel este corect pentru fiecare element din tipul și seturile de argumente ale țintei.

În exemplele noastre, operatorul s:=b indică faptul că clasa BĂIAT aparține setului de tipuri pt s(deoarece ca rezultat al executării instrucțiunii create creați b apartine setului de tipuri pt b). FATĂ, datorită prezenței instrucțiunilor creați g, aparține setului de tipuri pt g. Dar apoi provocarea acțiune va fi invalid pentru țintă s tip BĂIAT si argument g tip FATĂ. În mod similar DREPTUNGHI este de tipul setat pentru p, care se datorează atribuirii polimorfe, totuși, apelul add_vertex pentru p tip DREPTUNGHI va fi invalid.

Aceste observații ne duc să ne gândim la creație global abordare bazată pe noua regulă de tastare:

Regula de corectitudine a sistemului

Apel x.f(arg) este corect de sistem dacă și numai dacă este corect de clasă pentru X, și arg, care au orice tipuri din seturile de tipuri respective.

În această definiție, un apel este considerat corect de clasă dacă nu încalcă regula de invocare a componentelor, care spune: dacă C există o clasă de bază ca X, componentă f ar trebui să fie exportate C, și tipul arg trebuie să fie compatibil cu tipul parametrului formal f. (Rețineți, pentru simplitate, presupunem că fiecare subrutină are un singur parametru, dar nu este greu să extindeți regula la un număr arbitrar de argumente.)

Corectitudinea sistemului a unui apel este redusă la corectitudinea clasei, cu excepția faptului că este verificată nu pentru elemente individuale, ci pentru orice perechi din seturi de seturi. Iată regulile de bază pentru crearea unui set de tipuri pentru fiecare entitate:

1 Pentru fiecare entitate, setul inițial de tipuri este gol.

2 După ce am întâlnit o altă instrucțiune a formularului creați(SOME_TYPE) a, adăuga SOME_TYPE la setul de tipuri pt A. (Pentru simplitate, vom presupune că orice instrucțiune creeaza o va fi înlocuit de instrucțiune creați (ATYPE) a, Unde UN FEL- tip de entitate A.)

3 Întâmpinarea unei alte sarcini a formularului a:=b, adăugați la setul de tipuri pentru A b.

4 În cazul în care un A este un parametru formal al subrutinei, apoi, după ce a întâlnit următorul apel cu parametrul actual b, adăugați la setul de tipuri pentru A toate elementele setului de tipuri pt b.

5 Vom repeta pașii (3) și (4) până când seturile de tipuri încetează să se schimbe.

Această formulare nu ține cont de mecanismul universalității, cu toate acestea, este posibil să se extindă regula după cum este necesar, fără probleme speciale. Pasul (5) este necesar datorită posibilității lanțurilor de atribuiri și transferuri (de la b la A, din c la b etc.). Este ușor de înțeles că după un număr finit de pași acest proces se va opri.

După cum probabil ați observat, regula nu ține cont de secvențele de instrucțiuni. Când


creați(TIP1) t; s:=t; crea (TYPE2) t

la setul de tipuri pt s intra ca TIPUL 1, și TIP 2, cu toate că s, având în vedere secvența de instrucțiuni, este capabil să ia numai valori de primul tip. Luând în considerare locația instrucțiunilor, compilatorul va necesita să analizeze în profunzime fluxul de instrucțiuni, ceea ce va duce la o creștere excesivă a nivelului de complexitate a algoritmului. În schimb, se aplică reguli mai pesimiste: succesiunea operațiilor:


vor fi declarate incorecte de sistem, în ciuda faptului că succesiunea executării lor nu duce la o încălcare a tipului.

Analiza globală a sistemului a fost prezentată (mai detaliat) în capitolul 22 al monografiei. În același timp, s-a rezolvat atât problema covarianței, cât și problema restricțiilor la export în timpul moștenirii. Cu toate acestea, această abordare are un defect practic nefericit și anume: se presupune că trebuie să verifice sistem în ansamblu mai degrabă decât fiecare clasă separat. Regula (4) se dovedește a fi fatală, care, la apelarea unei subrutine de bibliotecă, va ține cont de toate apelurile sale posibile în alte clase.

Deși au fost propuși algoritmi mai târziu pentru lucrul cu clase individuale în , valoarea lor practică nu a putut fi stabilită. Aceasta însemna că într-un mediu de programare care a suportat compilarea incrementală, ar fi necesar să se organizeze o verificare a întregului sistem. Este de dorit să se introducă verificarea ca element de procesare locală (rapidă) a modificărilor făcute de utilizator la unele clase. Deși sunt cunoscute exemple de abordare globală, de exemplu, programatorii C folosesc instrumentul puf pentru a găsi inconsecvențe în sistem care nu sunt detectate de compilator - toate acestea nu arată foarte atractiv.

Ca urmare, din câte știu, verificarea corectitudinii sistemului nu a fost implementată de nimeni. (Un alt motiv pentru acest rezultat poate să fi fost complexitatea regulilor de validare în sine.)

Corectitudinea clasei implică validarea limitată la clasă și, prin urmare, este posibilă cu compilarea incrementală. Corectitudinea sistemului implică o verificare globală a întregului sistem, care este în conflict cu compilarea incrementală.

Cu toate acestea, în ciuda numelui său, este de fapt posibil să se verifice corectitudinea sistemului folosind doar verificarea incrementală a clasei (în timpul unui compilator normal). Aceasta va fi contribuția finală la rezolvarea problemei.

Atenție la zgomote polimorfe!

Regula de corectitudine a sistemului este pesimistă: de dragul simplității, respinge și combinațiile de instrucțiuni complet sigure. Oricât de paradoxal ar părea, însă vom construi ultima versiune a soluției pe baza regulă şi mai pesimistă. Desigur, acest lucru ridică întrebarea cât de realist va fi rezultatul nostru.

Înapoi la Yalta

Esența soluției Catcall (Catcall), - sensul acestui concept îl vom explica mai târziu - într-o întoarcere la spiritul acordurilor de la Yalta, împărțind lumea în polimorfă și covariantă (și un însoțitor al covarianței - ascunderea descendenților), dar fără a fi nevoie să posede o înțelepciune infinită.

Ca și înainte, restrângem problema covarianței la două operații. În exemplul nostru principal, aceasta este o atribuire polimorfă: s:=b, și apelând subrutinei covariante: s.share(g). Analizând cine este adevăratul vinovat de încălcări, excludem argumentul g a suspecţilor. Orice argument de tip SCHIOR sau derivat din ea, nu ne convine din cauza polimorfismului s si covarianta acțiune. Prin urmare, dacă descrii static esența alte Cum SCHIORși se atașează dinamic la obiect SCHIOR, apoi apelul s.share (altul) va da static impresia de a fi ideal, dar va avea ca rezultat încălcări de tip dacă este atribuit polimorf s sens b.

Problema fundamentală este că încercăm să folosim sîn două moduri incompatibile: ca entitate polimorfă și ca țintă a unui apel de subrutină covariantă. (În celălalt exemplu al nostru, problema este utilizarea p ca entitate polimorfă și ca țintă a apelării unei subrutine a unui copil care ascunde componenta add_vertex.)

Soluția lui Catcall, ca și Pinning, este radicală: interzice utilizarea unei entități ca polimorfă și covariantă în același timp. La fel ca analiza globală, determină static ce entități pot fi polimorfe, dar nu încearcă să fie prea inteligent căutând seturi de tipuri posibile pentru entități. În schimb, orice entitate polimorfă este percepută ca suficient de suspectă încât este interzis să se alieze cu un cerc de persoane respectabile, inclusiv covarianța și ascunderea descendenților.

O regulă și mai multe definiții

Regula de tip pentru soluția Catcall are o formulare simplă:

Tip Rule pentru Catcall

Apelurile polimorfe sunt incorecte.

Se bazează pe definiții la fel de simple. În primul rând, o entitate polimorfă:

Definiție: entitate polimorfă

Esență X tipul de referință (neextins) este polimorf dacă are una dintre următoarele proprietăți:

1 Apare în sarcină x:= y, unde esența y are alt tip sau este polimorfă prin recursivitate.

2 Găsit în instrucțiunile de creare creați(OTHER_TYPE) x, Unde OTHER_TYPE nu este tipul specificat în declarație X.

3 Este un argument formal pentru o subrutină.

4 Este o funcție externă.

Scopul acestei definiții este de a da statutul de polimorf („potențial polimorf”) oricărei entități care poate fi atașată la obiecte de diferite tipuri în timpul execuției programului. Această definiție se aplică doar tipurilor de referință, deoarece entitățile extinse nu pot fi prin natura lor polimorfe.

În exemplele noastre, schiorul sși poligon p sunt polimorfe conform regulii (1). Primului i se atribuie un obiect BĂIAT b, al doilea - obiectul DREPTANGUL r.

Dacă te-ai uitat la definiția unui set de tipuri, vei observa cât de pesimistă este definiția unei entități polimorfe și cât de ușor este de testat. Fără a încerca să găsim toate tipurile dinamice posibile ale unei entități, ne mulțumim cu întrebarea generală: poate o entitate dată să fie polimorfă sau nu? Cea mai surprinzătoare este regula (3), conform căreia polimorfă conteaza fiecare parametru formal(cu excepția cazului în care tipul său este extins, așa cum este cazul numerelor întregi etc.). Nici măcar nu ne deranjam cu analiza apelurilor. Dacă subrutina are un argument, atunci este la dispoziția clientului, ceea ce înseamnă că nu te poți baza pe tipul specificat în declarație. Această regulă este strâns legată de reutilizare - scopul tehnologiei obiectelor - unde orice clasă poate fi inclusă într-o bibliotecă și poate fi apelată de mai multe ori de către diferiți clienți.

O proprietate caracteristică a acestei reguli este că nu necesită verificări globale. Pentru a identifica polimorfismul unei entități, este suficient să vizualizați textul clasei în sine. Dacă pentru toate solicitările (atribute sau funcții) sunt stocate informații despre starea lor polimorfă, atunci nici textele strămoșilor nu trebuie să fie studiate. Spre deosebire de găsirea de seturi de tipuri, puteți descoperi entități polimorfe verificând clasă cu clasă în timpul compilării incrementale.

Apelurile, ca și entitățile, pot fi polimorfe:

Definiție: apel polimorf

Un apel este polimorf dacă ținta sa este polimorfă.

Ambele apeluri din exemplele noastre sunt polimorfe: s.share(g) din cauza polimorfismului s, p.add_vertex(...) din cauza polimorfismului p. Prin definiție, numai apelurile calificate pot fi polimorfe. (Apel necalificat f(...) un fel de calificat Curent.f(...), nu schimbam esenta materiei, din moment ce Actual, căruia nu i se poate atribui nimic, nu este un obiect polimorf.)

În continuare, avem nevoie de conceptul de Catcall, bazat pe conceptul de CAT. (CAT este o abreviere pentru schimbarea disponibilității sau a tipului). O subrutină este o subrutină CAT dacă o redefinire a acesteia de către un copil are ca rezultat unul dintre cele două tipuri de modificări pe care le-am văzut că sunt potențial periculoase: schimbarea tipului argumentului (covariant) sau ascunderea unei componente exportate anterior.

Definiție: rutine CAT

O rutină se numește o rutină CAT dacă o redefinire a acesteia schimbă starea de export sau tipul oricăruia dintre argumentele sale.

Această proprietate permite din nou verificarea incrementală: orice redefinire a tipului de argument sau a stării exportului face ca procedura sau funcția să fie o subrutină CAT. Aici intervine noțiunea lui Catcall: apelarea unei subrutine CAT care poate fi eronată.

Definiție: Catcall

Un apel este numit Catcall dacă o redefinire a rutinei l-ar face să eșueze din cauza unei modificări a stării exportului sau a tipului de argument.

Clasificarea pe care am creat-o ne permite să distingem grupuri speciale de apeluri: polimorfe și catcalls. Apelurile polimorfe dau putere expresivă abordării obiectelor, apelurile vă permit să redefiniți tipurile și să restricționați exporturile. Folosind terminologia introdusă mai devreme în acest capitol, putem spune că apelurile polimorfe se extind utilitate, strigări - utilizabilitate.

Provocări acțiuneși add_vertex, luate în considerare în exemplele noastre, sunt chemarea pisicilor. Primul efectuează o redefinire covariantă a argumentului său. Al doilea este exportat de clasă DREPTUNGHI, dar ascuns de clasă POLIGON. Ambele apeluri sunt, de asemenea, polimorfe, deci sunt exemple excelente de apeluri polimorfe. Sunt eronate conform regulii de tip Catcall.

Nota

Înainte de a încheia tot ce am învățat despre covarianță și ascunderea copiilor, să recapitulăm că încălcările corectitudinii sistemelor sunt într-adevăr rare. Cele mai importante proprietăți ale tastării statice OO au fost rezumate la începutul prelegerii. Această gamă impresionantă de mecanisme de tastare, împreună cu validarea bazată pe clasă, deschide calea pentru o metodă sigură și flexibilă de construire a software-ului.

Am văzut trei soluții la problema covarianței, dintre care două au abordat și restricțiile la export. Care este corect?

Nu există un răspuns definitiv la această întrebare. Consecințele interacțiunii insidioase dintre tiparea OO și polimorfism nu sunt la fel de bine înțelese ca problemele discutate în prelegerile anterioare. În ultimii ani au apărut numeroase publicații pe această temă, referiri la care sunt date în bibliografia de la finalul prelegerii. În plus, sper că în această prelegere am putut să prezint elementele soluției finale, sau măcar să mă apropii de ea.

O analiză globală pare nepractică din cauza unei verificări complete a întregului sistem. Cu toate acestea, ne-a ajutat să înțelegem mai bine problema.

Soluția Pinning este extrem de atractivă. Este simplu, intuitiv, ușor de implementat. Cu atât mai mult ar trebui să regretăm imposibilitatea de a susține în ea o serie de cerințe cheie ale metodei OO, reflectate în principiul Deschis-Închis. Dacă am avea într-adevăr o mare intuiție, atunci fixarea ar fi o soluție grozavă, dar ce dezvoltator ar îndrăzni să afirme acest lucru sau, cu atât mai mult, să admită că autorii orelor de bibliotecă moștenite în proiectul său au avut o asemenea intuiție?

Dacă suntem forțați să renunțăm la fixare, atunci soluția Catcall pare a fi cea mai potrivită, ceea ce este destul de ușor de explicat și aplicabil în practică. Pesimismul lui nu ar trebui să excludă combinații utile de operatori. În cazul în care un apel polimorf este generat de o declarație „legitimă”, este întotdeauna sigur să o admitem prin introducerea unei încercări de atribuire. Astfel, un număr de verificări pot fi transferate la timpul de execuție a programului. Cu toate acestea, numărul acestor cazuri ar trebui să fie extrem de mic.

Ca o precizare, ar trebui să remarc că la momentul scrierii acestui articol, soluția Catcall nu a fost implementată. Până când compilatorul este adaptat la verificarea regulilor de tip Catcall și aplicat cu succes sistemelor reprezentaționale mari și mici, este prea devreme să spunem că ultimul cuvânt a fost spus despre problema reconcilierii tipăririi statice cu polimorfismul, combinată cu covarianța și ascunderea descendenților. .

Deplină concordanță

Încheind discuția despre covarianță, este util să înțelegem cum poate fi aplicată o metodă generală la o problemă destul de generală. Metoda a apărut ca rezultat al teoriei Catcall, dar poate fi utilizată în cadrul versiunii de bază a limbajului fără a introduce reguli noi.

Să fie două liste potrivite, în care prima specifică schiorii și a doua specifică colegul de cameră pentru schiorul din prima listă. Dorim să efectuăm procedura de plasare corespunzătoare acțiune, numai dacă este permis de regulile de descriere a tipului care permit fete cu fete, fete premiate cu fete premiate și așa mai departe. Problemele de acest gen sunt frecvente.

Poate exista o soluție simplă bazată pe discuția anterioară și încercarea de atribuire. Luați în considerare funcția universală montate(a aproba):


montat (altul: GENERAL): ca altul este
-- Obiectul curent (Actual), dacă tipul său se potrivește cu tipul obiectului,
-- atașat la altul, altfel nul.
if other /= Void and then conforms_to (altul) atunci

Funcţie montate returnează obiectul curent, dar cunoscut ca o entitate de tipul atașat argumentului. Dacă tipul obiectului curent nu se potrivește cu tipul obiectului atașat argumentului, atunci revine Vidul. Observați rolul încercării de atribuire. Funcția folosește componenta se conformeaza la din clasa GENERAL, care determină compatibilitatea de tip a unei perechi de obiecte.

Înlocuire se conformeaza la la o altă componentă GENERAL Cu nume acelasi tip ne oferă o funcție perfect_potrivit (deplină concordanță) care revine Vidul dacă tipurile ambelor obiecte nu sunt identice.

Funcţie montate- ne oferă o soluție simplă la problema potrivirii schiorilor fără a încălca regulile de descriere a tipurilor. Deci, în codul clasei SCHIOR putem introduce o nouă procedură și o putem folosi în loc de acțiune, (acesta din urmă poate fi făcută o procedură ascunsă).


-- Selectați, dacă este cazul, altul ca număr vecin.
-- gender_acertained - sex atribuit
gender_acertained_other: ca Current
gender_acertained_other:= other .fitted(Current)
if gender_acertained_other /= Void atunci
share(gender_acertained_other)
„Concluzie: Colocarea cu alții nu este posibilă”

Pentru alte tip arbitrar SCHIOR(nu numai precum Current) definiți versiunea sex_determinat_altul, care are un tip atribuit Actual. Funcția ne va ajuta să garantăm identitatea tipurilor perfect_potrivit.

Dacă există două liste paralele de schiori reprezentând cazarea planificată:


ocupant1, ocupant2: LIST

este posibil să se organizeze o buclă executând un apel la fiecare pas:


ocupant1.item.safe_share(occupant2.item)

elementele listei potrivire dacă și numai dacă tipurile lor sunt pe deplin compatibile.

Concepte cheie

[X]. Tastarea statică este cheia fiabilității, lizibilității și eficienței.

[X]. Pentru a fi realist, tastarea statică necesită o combinație de mecanisme: aserțiuni, moștenire multiplă, încercare de atribuire, genericitate mărginită și nemărginită, declarații ancorate. Sistemul de tip nu trebuie să permită capcane (tip aruncări).

[X]. Regulile generale pentru redeclarare ar trebui să permită redefinirea covariantă. Tipurile de rezultat și argument înlocuite trebuie să fie compatibile cu cele originale.

[X]. Covarianța, precum și capacitatea unui copil de a ascunde o componentă exportată de un strămoș, combinată cu polimorfismul, creează o problemă de încălcare de tip rară, dar foarte gravă.

[X]. Aceste încălcări pot fi evitate prin utilizarea: analizei globale (care este nepractică), restricționarea covarianței la tipuri fixe (ceea ce este contrar principiului Open-Closed), soluția Catcall, care împiedică o țintă polimorfă să apeleze o subrutină cu covarianță sau să ascundă un copil.

Note bibliografice

O serie de materiale din această prelegere sunt prezentate în rapoartele de la forumurile OOPSLA 95 și TOOLS PACIFIC 95 și, de asemenea, publicate în . O serie de materiale de recenzie sunt împrumutate din articol.

Conceptul de inferență automată de tip a fost introdus în , unde este descris algoritmul de inferență de tip al limbajului funcțional ML. Relația dintre polimorfism și verificarea tipului a fost explorată în .

Tehnicile de îmbunătățire a eficienței codului în limbi tipizate dinamic în contextul limbajului Self pot fi găsite în .

Luca Cardelli și Peter Wegner au scris un articol teoretic despre tipurile din limbaje de programare care au avut o mare influență asupra specialiștilor. Această lucrare, construită pe baza calculului lambda (vezi), a servit drept bază pentru multe studii ulterioare. A fost precedată de o altă lucrare fundamentală a lui Cardelli.

Manualul ISE include o introducere în problemele aplicării în comun a polimorfismului, covarianței și ascunderii descendenților. Lipsa unei analize adecvate în prima ediție a acestei cărți a dus la o serie de discuții critice (prima dintre care au fost comentariile lui Philippe Elinck în lucrarea de licență „De la Conception-Programmation par Objets”, Memoire de licence, Universite Libre de Bruxelles (Belgia), 1988), exprimată în lucrările și . Articolul lui Cook oferă câteva exemple legate de problema covarianței și încearcă să o rezolve. O soluție bazată pe parametrii de tip pentru entitățile covariante la TOOLS EUROPE 1992 a fost propusă de Franz Weber. Definiții precise ale conceptelor de corectitudine a sistemului, precum și corectitudinea clasei, sunt date în , și acolo este, de asemenea, propusă o soluție folosind o analiză completă a sistemului. Soluția Catcall a fost propusă pentru prima dată în ; Vezi si .

Soluția de fixare a fost prezentată în discursul meu de la seminarul TOOLS EUROPE 1994. La acel moment, însă, nu vedeam nevoia ancoră-reclame și restricții de compatibilitate aferente. Paul Dubois și Amiram Yehudai s-au grăbit să sublinieze că problema covarianței rămâne în aceste condiții. Ei, ca și Reinhardt Budde, Karl-Heinz Sylla, Kim Walden și James McKim, au făcut multe comentarii care au avut o importanță fundamentală în munca care a condus la scrierea acestei prelegeri.

O mare cantitate de literatură este dedicată problemelor de covarianță. În și veți găsi atât o bibliografie extinsă, cât și o prezentare generală a aspectelor matematice ale problemei. Pentru o listă de link-uri către materiale online despre teoria tipului POO și paginile Web ale autorilor acestora, consultați pagina lui Laurent Dami. Conceptele de covarianță și contravarianță sunt împrumutate din teoria categoriilor. Apariția lor în contextul tastării programelor îi datorăm lui Luca Cardelli, care a început să le folosească în discursurile sale la începutul anilor '80, dar nu a recurs la ele în tipărire până la sfârșitul anilor '80.

Tehnicile bazate pe variabile de tip sunt descrise în , , .

Contravarianța a fost implementată în limba Sather. Explicațiile sunt date în .

  • Tastarea dinamică este o tehnică utilizată pe scară largă în limbajele de programare și limbajele de specificație, în care o variabilă este asociată cu un tip în momentul atribuirii valorii, și nu în momentul în care variabila este declarată. Astfel, în diferite părți ale programului, aceeași variabilă poate lua valori de diferite tipuri. Exemple de limbaje tipizate dinamic sunt Smalltalk, Python, Objective-C, Ruby, PHP, Perl, JavaScript, Lisp, xBase, Erlang, Visual Basic.

    Tehnica opusă este tastarea statică.

    În unele limbi cu tastare dinamică slabă, există o problemă de comparare a valorilor, de exemplu, PHP are operatorii de comparare "==", "!=" și "===", "!==", unde a doua pereche de operații compară valori și tipuri de variabile. Operatorul „===" evaluează drept adevărat numai dacă se potrivește perfect, spre deosebire de „==" care consideră că următoarea expresie este adevărată: (1=="1"). Este de remarcat faptul că aceasta nu este o problemă de tastare dinamică în general, ci de limbaje de programare specifice.

Concepte înrudite

Un limbaj de programare este un limbaj formal pentru scrierea programelor de calculator. Un limbaj de programare definește un set de reguli lexicale, sintactice și semantice care determină aspectul programului și acțiunile pe care executantul (de obicei un computer) le va efectua sub controlul său.

Zahărul sintactic într-un limbaj de programare este o caracteristică sintactică, a cărei utilizare nu afectează comportamentul programului, dar face ca utilizarea limbajului să fie mai convenabilă pentru oameni.

O proprietate este o modalitate de a accesa starea internă a unui obiect, imitând o variabilă de un anumit tip. Accesarea unei proprietăți a unui obiect arată la fel ca accesarea unui câmp struct (în programarea structurată), dar este implementată de fapt printr-un apel de funcție. Când încercați să setați valoarea acestei proprietăți, este apelată o metodă, iar când încercați să obțineți valoarea acestei proprietăți, este apelată o altă metodă.

Extended Backus–Naur Form (EBNF) este un sistem formal de definire a sintaxelor în care unele categorii sintactice sunt definite secvenţial prin altele. Folosit pentru a descrie gramatici formale fără context. Sugerat de Niklaus Wirth. Este o reluare extinsă a formelor Backus-Naur, se deosebește de BNF prin construcții mai „capabile”, care, cu aceeași capacitate expresivă, fac posibilă simplificarea...

Programarea aplicativă este un tip de programare declarativă în care scrierea unui program constă în aplicarea sistematică a unui obiect la altul. Rezultatul unei astfel de aplicații este din nou un obiect care poate participa la aplicații atât ca funcție, cât și ca argument și așa mai departe. Acest lucru face ca înregistrarea programului să fie clară din punct de vedere matematic. Faptul că o funcție este notă printr-o expresie indică posibilitatea utilizării funcțiilor-valoare - funcționale ...

Un limbaj de programare concatenativ este un limbaj de programare bazat pe faptul că concatenarea a două bucăți de cod exprimă compoziția lor. Într-un astfel de limbaj, specificarea implicită a argumentelor funcției este utilizată pe scară largă (vezi programarea inutilă), noile funcții sunt definite ca compoziție de funcții și concatenarea este folosită în loc de aplicare. Această abordare este opusă programării aplicative.

O variabilă este un atribut al unui sistem fizic sau abstract care își poate schimba valoarea, de obicei numerică. Conceptul de variabilă este utilizat pe scară largă în domenii precum matematică, științe naturale, inginerie și programare. Exemple de variabile sunt: ​​temperatura aerului, parametrul funcției și multe altele.

Analiza sintactică (sau parsing, slang parsing ← English parsing) în lingvistică și informatică este procesul de comparare a unei secvențe liniare de lexeme (cuvinte, simboluri) a unei limbi naturale sau formale cu gramatica sa formală. Rezultatul este de obicei un arbore de analiză (arborele de sintaxă). Folosit de obicei împreună cu analiza lexicală.

Un tip de date algebrice generalizate (GADT) este unul dintre tipurile de tipuri de date algebrice, care se caracterizează prin faptul că constructorii săi pot returna valori care nu sunt de tipul lor asociate. Proiectat sub influența lucrărilor asupra familiilor inductive în rândul cercetătorilor de tipuri dependente.

Semantica în programare este o disciplină care studiază formalizarea semnificațiilor constructelor limbajului de programare prin construirea modelelor lor matematice formale. Diferite instrumente pot fi folosite ca instrumente pentru construirea unor astfel de modele, de exemplu, logica matematică, λ-calcul, teoria mulțimilor, teoria categoriilor, teoria modelelor, algebră universală. Formalizarea semanticii unui limbaj de programare poate fi folosită atât pentru a descrie limbajul, cât și pentru a determina proprietățile limbajului...

Programarea orientată pe obiecte (OOP) este o metodologie de programare bazată pe reprezentarea unui program ca un set de obiecte, fiecare dintre acestea fiind o instanță a unei anumite clase, iar clasele formează o ierarhie de moștenire.

Variabilă dinamică - o variabilă în program, un loc în RAM pentru care este alocat în timpul execuției programului. De fapt, este o bucată de memorie alocată de sistem unui program în scopuri specifice în timp ce programul rulează. Prin aceasta, diferă de o variabilă statică globală - o bucată de memorie alocată de sistem unui program în scopuri specifice înainte de începerea programului. O variabilă dinamică este una dintre clasele de stocare a variabilelor.

Pentru a explica cât mai simplu două tehnologii complet diferite, să începem de la început. Primul lucru pe care îl întâlnește un programator când scrie cod este declararea variabilelor. Puteți observa că, de exemplu, în limbajul de programare C++, trebuie să specificați tipul unei variabile. Adică, dacă declarați o variabilă x, atunci trebuie să adăugați int - pentru a stoca date întregi, float - pentru a stoca date în virgulă mobilă, char - pentru datele de caractere și alte tipuri disponibile. Prin urmare, C++ folosește tastarea statică, la fel ca predecesorul său, C.

Cum funcționează tastarea statică?

În momentul declarării unei variabile, compilatorul trebuie să știe ce funcții și parametri poate folosi în legătură cu aceasta și care nu. Prin urmare, programatorul trebuie să indice imediat în mod clar tipul variabilei. De asemenea, rețineți că tipul unei variabile nu poate fi schimbat în timp ce codul rulează. Dar vă puteți crea propriul tip de date și îl puteți utiliza în viitor.

Să luăm în considerare un mic exemplu. La inițializarea variabilei x (int x;), specificăm identificatorul int - aceasta este o abreviere pentru care stochează numai numere întregi în intervalul de la - 2 147 483 648 la 2 147 483 647. Astfel, compilatorul înțelege că poate efectua valori matematice ale acestei variabile - sumă, diferență, înmulțire și împărțire. Dar, de exemplu, funcția strcat(), care conectează două valori char, nu poate fi aplicată la x. La urma urmei, dacă eliminați restricțiile și încercați să conectați două valori int folosind o metodă simbolică, atunci va apărea o eroare.

De ce avem nevoie de limbi cu tastare dinamică?

În ciuda unor limitări, tastarea statică are o serie de avantaje și nu aduce prea mult disconfort la scrierea algoritmilor. Cu toate acestea, în diverse scopuri, pot fi necesare mai multe „reguli libere” cu privire la tipurile de date.

Un bun exemplu de dat este JavaScript. Acest limbaj de programare este de obicei folosit pentru încorporarea într-un cadru pentru a obține acces funcțional la obiecte. Datorită acestei caracteristici, a câștigat o mare popularitate în tehnologiile web, unde tastarea dinamică este ideală. Uneori, scrierea de scripturi și macrocomenzi mici este simplificată. Și, de asemenea, există un avantaj în reutilizarea variabilelor. Dar această posibilitate este folosită destul de rar, din cauza posibilelor confuzii și erori.

Ce tip de tastare este cel mai bun?

Dezbaterea conform căreia tastarea dinamică este mai bună decât tastarea strictă continuă până în prezent. De obicei, ele apar la programatori foarte specializați. Desigur, dezvoltatorii web folosesc zilnic toate avantajele tastării dinamice pentru a crea cod de înaltă calitate și produsul software final. În același timp, programatorii de sistem care dezvoltă algoritmi complecși în limbaje de programare de nivel scăzut, de obicei, nu au nevoie de astfel de capacități, așa că tastarea statică este suficientă pentru ei. Există, desigur, și excepții de la regulă. De exemplu, tastarea dinamică este complet implementată în Python.

Prin urmare, este necesar să se determine conducerea unei anumite tehnologii pe baza doar a parametrilor de intrare. Tastarea dinamică este mai bună pentru dezvoltarea cadrelor ușoare și flexibile, în timp ce tastarea puternică este mai bună pentru crearea unei arhitecturi masive și complexe.

Separarea în tastare „puternică” și „slabă”.

Printre materialele de programare atât în ​​limba rusă, cât și în limba engleză, se poate întâlni expresia - tastare „puternică”. Acesta nu este un concept separat, sau mai degrabă, un astfel de concept nu există deloc în lexicul profesional. Deși mulți încearcă să o interpreteze diferit. De fapt, tastarea „puternică” trebuie înțeleasă ca fiind cea care îți este convenabilă și cu care este cel mai confortabil să lucrezi. Un sistem „slab” este un sistem incomod și ineficient pentru tine.

Caracteristica dinamică

Trebuie să fi observat că în etapa de scriere a codului, compilatorul analizează constructele scrise și generează o eroare dacă tipurile de date nu se potrivesc. Dar nu JavaScript. Unicitatea sa constă în faptul că va efectua operația în orice caz. Iată un exemplu ușor - vrem să adăugăm un caracter și un număr, care nu are sens: „x” + 1.

În limbajele statice, în funcție de limba în sine, această operațiune poate avea consecințe diferite. Dar, în cele mai multe cazuri, nici măcar nu va fi permisă compilarea, deoarece compilatorul va da o eroare imediat după ce ați scris o astfel de construcție. Pur și simplu va considera că este incorect și va avea complet dreptate.

În limbajele dinamice, această operație poate fi efectuată, dar în cele mai multe cazuri va urma o eroare deja în etapa de execuție a codului, deoarece compilatorul nu analizează tipurile de date în timp real și nu poate decide asupra erorilor în acest domeniu. JavaScript este unic prin faptul că va efectua o astfel de operație și va ajunge cu un set de caractere care nu pot fi citite. Spre deosebire de alte limbi care doar vor termina programul.

Sunt posibile arhitecturile adiacente?

În prezent, nu există nicio tehnologie înrudită care ar putea suporta simultan tastarea statică și dinamică în limbaje de programare. Și putem spune cu încredere că nu va apărea. Deoarece arhitecturile diferă unele de altele în termeni fundamentali și nu pot fi utilizate simultan.

Dar, cu toate acestea, în unele limbi, puteți schimba tastarea cu ajutorul cadrelor suplimentare.

  • În limbajul de programare Delphi, subsistemul Variant.
  • În limbajul de programare AliceML - pachete suplimentare.
  • În limbajul de programare Haskell, biblioteca Data.Dynamic.

Când este cu adevărat mai bună tastarea puternică decât tastarea dinamică?

Puteți aproba fără echivoc avantajul tastării puternice față de dinamică numai dacă sunteți un programator începător. Absolut toți specialiștii IT sunt de acord cu acest lucru. Când predați abilități fundamentale și de bază de programare, este mai bine să folosiți tastarea puternică pentru a câștiga o disciplină atunci când lucrați cu variabile. Apoi, dacă este necesar, puteți trece la dinamică, dar abilitățile dobândite cu o tastare puternică vor juca un rol important. Veți învăța cum să verificați cu atenție variabilele și să luați în considerare tipurile lor atunci când proiectați și scrieți cod.

Beneficiile tastării dinamice

  • Minimizează numărul de caractere și linii de cod datorită predeclarării inutile a variabilelor și specificării tipului acestora. Tipul va fi determinat automat după atribuirea valorii.
  • În blocuri mici de cod, percepția vizuală și logică a construcțiilor este simplificată datorită absenței liniilor de declarație „extra”.
  • Dinamica are un efect pozitiv asupra vitezei compilatorului, deoarece nu ia în considerare tipurile și nu le verifică pentru conformitate.
  • Mărește flexibilitatea și vă permite să creați modele versatile. De exemplu, atunci când creați o metodă care trebuie să interacționeze cu o matrice de date, nu trebuie să creați funcții separate pentru a lucra cu numere, text și alte tipuri de matrice. Este suficient să scrieți o singură metodă și va funcționa cu orice tip.
  • Simplifica ieșirea datelor din sistemele de gestionare a bazelor de date, astfel încât tastarea dinamică este utilizată în mod activ în dezvoltarea aplicațiilor web.

Aflați mai multe despre limbajele de programare cu tastare statică

  • C++ este cel mai utilizat limbaj de programare cu scop general. Astăzi are mai multe ediții majore și o mare armată de utilizatori. A devenit popular datorită flexibilității sale, posibilității de extindere nelimitată și suportului pentru diverse paradigme de programare.

  • Java este un limbaj de programare care folosește o abordare orientată pe obiecte. A câștigat popularitate datorită platformei multiple. Când este compilat, codul este interpretat în bytecode care poate fi executat pe orice sistem de operare. Java și tastarea dinamică sunt incompatibile deoarece limbajul este puternic tastat.

  • Haskell este, de asemenea, una dintre limbile populare al cărui cod poate fi integrat și interacționat cu alte limbi. Dar, în ciuda acestei flexibilități, are o tastare puternică. Echipat cu un set mare încorporat de tipuri și posibilitatea de a crea propriile tipuri.

Aflați mai multe despre limbaje de programare cu tastare dinamică

  • Python este un limbaj de programare care a fost creat în primul rând pentru a facilita munca unui programator. Are o serie de îmbunătățiri funcționale, datorită cărora crește lizibilitatea codului și scrierea acestuia. În multe feluri, acest lucru a fost realizat datorită tastării dinamice.

  • PHP este un limbaj de scripting. Utilizat pe scară largă în dezvoltarea web, oferind interacțiune cu bazele de date pentru a crea pagini web dinamice interactive. Datorită tastării dinamice, lucrul cu bazele de date este mult facilitat.

  • JavaScript este limbajul de programare deja menționat mai sus, care și-a găsit aplicație în tehnologiile web pentru crearea de scripturi web care rulează pe partea clientului. Tastarea dinamică este folosită pentru a face codul mai ușor de scris, deoarece este de obicei împărțit în blocuri mici.

Tip dinamic de tastare - Dezavantaje

  • Dacă a fost făcută o greșeală sau o greșeală la utilizarea sau declararea variabilelor, compilatorul nu o va afișa. Și vor apărea probleme în timpul execuției programului.
  • Când utilizați tastarea statică, toate declarațiile de variabile și funcții sunt de obicei plasate într-un fișier separat, ceea ce face ușoară crearea documentației în viitor sau chiar utilizarea fișierului în sine ca documentație. În consecință, tastarea dinamică nu permite utilizarea unei astfel de caracteristici.

Rezuma

Tastarea statică și dinamică sunt folosite în scopuri complet diferite. În unele cazuri, dezvoltatorii urmăresc beneficii funcționale, iar în altele motive pur personale. În orice caz, pentru a determina singur tipul de tastare, trebuie să le studiați cu atenție în practică. În viitor, atunci când creați un nou proiect și alegeți o tastare pentru acesta, aceasta va juca un rol important și va oferi o înțelegere a alegerii eficiente.

Când studiați limbaje de programare, auziți adesea expresii precum „tastat static” sau „tastat dinamic” în conversații. Aceste concepte descriu procesul de verificare a tipului și atât verificarea statică a tipului, cât și verificarea dinamică a tipului se referă la diferite sisteme de tip. Un sistem de tipuri este un set de reguli care atribuie o proprietate numită „tip” diferitelor entități dintr-un program: variabile, expresii, funcții sau module - cu scopul final de a reduce erorile, asigurându-se că datele sunt afișate corect.

Nu vă faceți griji, știu că toate acestea sună confuz, așa că vom începe cu elementele de bază. Ce este „verificarea tipului” și ce este un tip în general?

Tip de

Codul care trece verificarea dinamică a tipului este, în general, mai puțin optimizat; în plus, există posibilitatea unor erori de rulare și, ca urmare, necesitatea verificării înainte de fiecare rulare. Cu toate acestea, tastarea dinamică deschide ușa către alte tehnici de programare puternice, cum ar fi metaprogramarea.

Concepții greșite comune

Mitul 1: Tastare statică/dinamică == tastare puternică/slabă

O concepție greșită comună este aceea că toate limbile tipizate static sunt puternic tastate și toate limbile tastate dinamic sunt slab tastate. Acest lucru este greșit și iată de ce.

Un limbaj puternic tipizat este unul în care variabilele sunt legate de anumite tipuri de date și care va genera o eroare de tip dacă tipurile așteptate și reale nu se potrivesc, ori de câte ori este efectuată verificarea. Cel mai ușor este să te gândești la o limbă puternic tipizată ca la o limbă foarte sigură pentru tipare. De exemplu, în fragmentul de cod deja folosit mai sus, un limbaj puternic tastat va produce o eroare explicită de tip care va anula programul:

X = 1 + "2"

Adesea asociem limbaje tipizate static precum Java și C# cu limbaje puternic tipizate (care sunt acestea) deoarece tipul de date este setat în mod explicit atunci când variabila este inițializată - ca în acest exemplu Java:

String foo = new String("bună lume");

Cu toate acestea, Ruby, Python și JavaScript (toate sunt tastate dinamic) sunt de asemenea puternic tastate, deși dezvoltatorul nu trebuie să specifice tipul variabilei atunci când o declară. Luați în considerare același exemplu, dar scris în Ruby:

Foo = „bună lume”

Ambele limbi sunt puternic tastate, dar folosesc metode diferite de verificare a tipului. Limbi precum Ruby, Python și JavaScript nu necesită definiții explicite ale tipului datorită inferenței de tip, a capacității de a deduce în mod programatic tipul corect al unei variabile pe baza valorii acesteia. Inferența de tip este o proprietate separată a limbajului și nu se aplică sistemelor de tip.

O limbă cu tastare vag este o limbă în care variabilele nu sunt legate de un anumit tip de date; au încă un tip, dar restricțiile de siguranță de tip sunt mult mai slabe. Luați în considerare următorul exemplu de cod PHP:

$foo = "x"; $foo = $foo + 2; // nu este o eroare echo $foo; // 2

Deoarece PHP este scris slab, nu există nicio eroare în acest cod. Similar cu sugestia anterioară, nu toate limbile tastate slab sunt tastate dinamic: PHP este un limbaj tastat dinamic, dar C, de asemenea, un limbaj tastat slab, este cu adevărat tastat static.

Mitul este rupt.

Deși sistemele de tip static/dinamic și puternic/slab sunt diferite, ambele sunt legate de siguranța tipului. Cel mai simplu mod de a o pune este că primul sistem vă spune când este verificată siguranța tipului, iar al doilea vă spune cum.

Mitul 2: Tastarea statică/dinamică == limbaje compilate/interpretate

Este adevărat să spunem că majoritatea limbilor tipizate static sunt de obicei compilate și sunt interpretate dinamic, dar această afirmație nu poate fi generalizată și există un exemplu simplu în acest sens.

Când vorbim despre scrierea limbii, vorbim despre limba ca întreg. De exemplu, nu contează ce versiune de Java utilizați - va fi întotdeauna scrisă static. Acest lucru este diferit de cazul în care limbajul este compilat sau interpretat, deoarece în acest caz vorbim despre o implementare specifică a limbajului. În teorie, orice limbaj poate fi atât compilat, cât și interpretat. Cea mai populară implementare a limbajului Java folosește compilarea la bytecode, care este interpretată de JVM - dar există și alte implementări ale acestui limbaj care se compilează direct în codul mașinii sau sunt interpretate ca atare.

Dacă acest lucru încă nu este clar, vă sfătuiesc să citiți această serie.

Concluzie

Știu că au fost multe informații în acest articol - dar cred că ai înțeles bine. Aș dori să mut informații despre tastarea puternică / slabă într-un articol separat, dar acesta nu este un subiect atât de important; mai mult, trebuia să se arate că acest tip de tastare nu avea nicio legătură cu verificarea tipului.

Nu există un singur răspuns la întrebarea „care tastare este mai bună?” - fiecare are propriile sale avantaje și dezavantaje. Unele limbi - cum ar fi Perl și C# - vă permit chiar să alegeți singur între sistemele de verificare a tipurilor statice și dinamice. Înțelegerea acestor sisteme vă va permite să înțelegeți mai bine natura erorilor care apar, precum și să vă faceți mai ușor de tratat.



Se încarcă...
Top