Program educațional despre tastarea în limbaje de programare. Principii de bază de programare: tastare statică și dinamică Tastare puternică și slabă

Simplitatea tastării în abordarea OO este o consecință a simplității modelului de calcul obiect. Omițând detaliile, putem spune că un singur tip de evenimente are loc atunci când un sistem OO este executat - un apel de caracteristică:


semnificând o operație f peste un obiect atașat de X, trecând argumentul arg(sunt posibile mai multe argumente sau deloc). Programatorii Smalltalk vorbesc în acest caz despre „trecerea la un obiect X mesaje f cu argument arg„, dar aceasta este doar o diferență de terminologie și, prin urmare, este nesemnificativă.

Faptul că totul se bazează pe acest Construct de bază explică în parte sensul frumuseții ideilor OO.

Din Construcția de bază decurg situații anormale care pot apărea în timpul execuției:

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 obiectului OBJîn cazul în care fie:

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

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

Problema cu tastarea este evitarea situațiilor ca aceasta:

Problema tastării sistemelor OO

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

Cuvântul cheie este cand... Mai devreme sau mai târziu îți vei da seama că există o încălcare de tip. De exemplu, o încercare de a executa componenta Torpedo Launcher pentru un obiect Employee nu va funcționa și 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ă versus dinamică

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

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

[X]. Tastare statică: Pe baza unui 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 execuției (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 lăsată în seama compilatorului. Ca rezultat, avem o definiție simplă:

Definiție: limbaj tipizat static

Un limbaj OO este tipizat static dacă vine cu un set de reguli consecvente verificate de compilator pentru a se asigura că execuția sistemului nu duce la încălcarea tipului.

În literatură, termenul „ puternic tastare "( puternic). Se conformează naturii ultimatum a definiției, care nu necesită deloc încălcarea tipului. Posibil și slab (slab) forme de dactilografiere statică, în care regulile elimină anumite încălcări, fără a le elimina în totalitate. În acest sens, unele limbi OO sunt tastate slab static. Vom lupta pentru cea mai puternică tastare.

Limbile tipizate dinamic, cunoscute ca limbaje netipizate, nu au declarații de tip și orice valoare poate fi atașată entităților în timpul rulării. Verificarea tipului static nu este posibilă în ele.

Reguli de tastare

Notația noastră OO este tipizată static. Regulile sale de 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 specificat, de exemplu: pune (x: G; i: INTEGER).

[X].În orice misiune x: = y iar pentru 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 un descendent al 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. Luați în considerare două cazuri extreme.

[X]. Limbajul perfect corect, în care fiecare sistem corect din punct de vedere sintactic este corect în ceea ce privește tipurile. Regulile de declarare a tipului nu sunt necesare. Astfel de limbi există (imaginați-vă notația poloneză pentru o expresie cu adunare și scădere de numere întregi). Din păcate, niciun limbaj universal real nu îndeplinește acest criteriu.

[X]. Limbă complet incorectă 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 tastat: deoarece nu există sisteme care se potrivesc cu regulile, niciun sistem nu va cauza încălcarea tipului.

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

În practică, avem nevoie de un sistem de tip care să fie în același timp potrivit și util: suficient de puternic pentru a răspunde nevoilor de calcul și suficient de convenabil pentru a nu ne obliga să complicăm lucrurile pentru a satisface regulile de tastare.

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

În această prelegere, vom verifica dacă notația pe care o propunem este realistă.

Pesimism

Tastarea statică duce prin natura sa la politici „pesimiste”. O încercare de a garanta asta toate calculele nu duc la eșecuri, respinge calcule care s-ar fi putut termina fără eroare.

Luați în considerare un limbaj obișnuit, non-obiect, 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 activăm, vom vedea că [A] va funcționa întotdeauna, deoarece orice sistem numeric are o reprezentare exactă a numărului real 0,0, care poate fi tradus fără ambiguitate în 0 numere întregi. [B] va funcționa aproape sigur și el. Rezultatul acțiunii [C] nu este evident (dorim să obținem totalul rotunjind sau eliminând partea fracțională?). [D] își va face treaba, la fel ca operatorul:


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

care include misiunea de neatins ( n ^ 2 este pătratul numărului n). După înlocuire n ^ 2 pe n doar o serie de porniri vor da rezultatul corect. Misiune n o valoare mare în virgulă mobilă care nu poate fi reprezentată printr-un număr întreg va duce la o eroare.

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

Întrebarea nu este vom suntem pesimiști și în asta, cât costă ne putem permite să fim pesimiști. Să ne întoarcem 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 vine cu o mică pierdere a puterii expresive, le vom accepta. De exemplu, într-un mediu de dezvoltare care oferă funcții de rotunjire și evidențiere a întregului - rundăși trunchia, operator n: = r este considerat nevalid, deoarece vă obligă să scrieți în mod explicit conversia real-întreg în loc să utilizați conversiile ambigue implicite.

Tastarea statică: cum și de ce

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

Avantaje

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

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

Detectarea din timp a erorilor este, de asemenea, importantă, deoarece cu cât întârziem găsirea lor, cu atât costul remedierii lor va crește. Această proprietate, înțeleasă intuitiv de către toți programatorii profesioniști, este confirmată cantitativ de binecunoscutele lucrări ale lui Boehm. Dependența costului remedierii de timpul de găsire a erorilor este prezentată în grafic, construit în funcție de datele unui număr de proiecte industriale mari și experimente efectuate cu un proiect mic de gestionat:

Orez. 17.1. Costuri comparative ale remedierii erorilor (, publicate 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 faza de întreținere.

In cele din urma, eficienţă poate determina succesul sau eșecul tehnologiei obiectelor în practică. În absența tastării statice, a executa x.f (arg) poate dura cât vrei. Motivul pentru aceasta este că, în timpul execuției, nu se găsește fîn clasa de bază a țintei X, căutarea va continua printre urmașii ei, iar acesta este un drum sigur către ineficiență. Puteți atenua problema îmbunătățind căutarea unei componente în ierarhie. Autorii cărții Self au făcut o treabă grozavă încercând să genereze cel mai bun cod pentru limbajul tip dinamic. Dar tastarea statică a permis unui astfel de produs OO să se apropie sau să fie egal cu eficiența software-ului tradițional.

Cheia pentru tastarea statică este ideea deja exprimată că compilatorul generează codul pentru construct x.f (arg), știe tipul X... Din cauza polimorfismului, nu există nicio modalitate de a determina fără ambiguitate versiunea adecvată a componentei f... Dar declarația restrânge numeroasele tipuri posibile, permițând compilatorului să construiască un tabel care oferă acces la f cu costuri minime, - constantă mărginită complexitatea accesului. Optimizări suplimentare efectuate legarea staticăși inliniere- sunt facilitate si de tastarea statica, eliminand complet overhead acolo unde este cazul.

Argumente pentru tastarea dinamică

Cu toate acestea, tastarea dinamică nu și-a pierdut 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ă îi limitează prea mult, împiedicându-i să-și exprime liber ideile creative, numind uneori „centa de castitate”.

Se poate fi de acord cu acest raționament, 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ă acțiunilor noastre flexibilitate și oferă ne oferă oportunitatea de a ne bucura pe deplin de caracterul practic al tastării statice.

Tastarea: termenii succesului

Care sunt mecanismele de tastare statică realistă? Toate au fost introduse în prelegerile anterioare și, prin urmare, trebuie doar să le amintim pe scurt. Listarea lor împreună arată consistența și puterea combinării lor.

Sistemul nostru de tip se bazează în întregime pe concept clasă... Chiar și tipuri de bază, cum ar fi Î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 mai vechi este combinat cu tehnologia obiectelor bazată pe clasă.)

Tipuri extinse oferă-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 este de tip CONT, y - CONT DE ECONOMII, iar clasa a doua este moștenitorul primei.

În practică, un limbaj tipizat static are nevoie de sprijin moștenire multiplă... Există acuzații fundamentale de tastare statică că nu oferă capacitatea de a interpreta obiectele în mod diferit. Deci, obiectul DOCUMENT(documentul) poate fi transmis prin rețea și, prin urmare, are nevoie de componente asociate tipului MESAJ(mesaj). Dar această critică este valabilă numai pentru limbile limitate la moștenire unică.

Orez. 17.2. Moștenirea multiplă

Versatilitate este 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 limită, care vă permite să utilizați operațiuni care sunt aplicabile numai entităților de tip generic. Dacă clasa generică SORTABLE_LIST acceptă sortarea, necesită entități de tipul G, Unde G- un parametru generic, prezența unei operațiuni de comparare. Acest lucru se realizează prin legătura cu G clasa care definește constrângerea generică - COMPARABIL:


clasa SORTABLE_LIST...

Orice parametru generic real SORTABLE_LIST trebuie să fie un descendent al clasei COMPARABIL care are componenta necesară.

Un alt mecanism necesar este încercare de atribuire- organizează accesul la acele obiecte, tipul cărora software-ul nu le controlează. Dacă y este un obiect de bază de date sau un obiect primit prin rețea, apoi operator x? = y va atribui X sens y, dacă y este de tip compatibil sau, dacă nu este, va da X sens Vidul.

Afirmații asociate 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. În limbi precum Pascal și Ada, există tipuri de intervale care pot limita valorile unei entități, de exemplu, la intervalul de la 10 la 20, cu toate acestea, folosindu-le, nu veți putea atinge valoarea i a fost negativ, întotdeauna de două ori j... Invarianții de clasă vin în ajutor, proiectați să reflecte cu acuratețe constrângerile impuse, indiferent cât de complexe ar fi acestea.

Anunțuri fixate sunt necesare pentru a evita duplicarea codului de avalanșă în practică. Anunțând y: ca x, ai garanția că y se va schimba în urma oricăror declarații repetate precum X descendentul. Fără acest mecanism, dezvoltatorii ar fi neîncetat ocupați cu re-declarații, încercând să mențină diferitele tipuri consistente.

Declarațiile lipicioase sunt un caz special al ultimului motor de limbă de care avem nevoie - covarianta, despre care vom discuta în detaliu mai târziu.

La dezvoltarea sistemelor software, de fapt, este necesară încă o proprietate, care este inerentă mediului de dezvoltare însuși - recompilare incrementală rapidă... Când scrieți sau modificați un sistem, doriți să vedeți efectul schimbării cât mai curând posibil. Cu tastarea statică, trebuie să acordați timp compilatorului să verifice tipul. Rutinele tradiționale de compilare necesită recompilarea întregului sistem (și ansamblurile sale), iar acest proces poate fi chinuitor de lung, mai ales odată 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 au pornit sistemul cu procesare redusă sau deloc, fără verificare de tip. Acest argument este acum uitat. Un compilator modern bun detectează modul în care s-a schimbat codul de la ultima compilare și procesează doar modificările pe care le găsește.

— Copilul este tastat?

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

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

Astfel de mecanisme ocolesc limitările verificării tipului. Castingul este larg răspândit în programarea C, inclusiv în dialectul ANSI C. Chiar și în C ++, castingul, deși nu este la fel de frecvent, rămâne obișnuit și poate necesar.

Respectarea regulilor de tastare statică nu este ușor dacă acestea pot fi ocolite în orice moment prin casting.

Tastarea și legarea

Deși, în calitate de cititor al acestei cărți, cu siguranță vei distinge între scrierea statică și cea statică. legare, sunt oameni care nu pot face asta. Acest lucru se poate datora în parte influenței Smalltalk, care susține o abordare dinamică a ambelor probleme și poate induce în eroare oamenii că au aceeași soluție. (În cartea noastră susținem că este de dorit să combinați tastarea statică și conectarea dinamică pentru a crea programe robuste și flexibile.)

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

Tastarea și legarea

[X]. Întrebarea de tastare: când trebuie să știm cu siguranță că la runtime va exista o operație corespunzătoare f aplicabil unui obiect atașat unei entități X(cu parametru arg)?

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

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

În cadrul abordării obiectului:

[X]. problema cu tastarea este cu polimorfism: în măsura în care 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 de legare este cauzată de anunţuri repetate: deoarece o clasă poate modifica componentele moștenite, pot exista două sau mai multe operațiuni care pretind că le reprezintă fîn acest apel.

Ambele sarcini pot fi rezolvate atât dinamic, cât și static. Toate cele patru soluții sunt prezentate în limbile existente.

[X]. O serie de limbaje non-obiect, cum ar fi Pascal și Ada, implementează atât tastarea statică, cât și legarea statică. Fiecare entitate reprezintă obiecte de un singur tip, specificate 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 facilități de tastare dinamică. În același timp, se acordă preferință flexibilității în detrimentul fiabilității limbii.

[X]. Anumite limbi non-obiect acceptă tastarea dinamică și legarea statică. Acestea includ limbaje de asamblare și o serie de limbaje de scripting.

[X]. Ideile de tastare statică și de legare dinamică sunt încorporate în notația furnizată î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ă atunci când se specifică în mod explicit virtual ( virtual) reclame.

Motivul alegerii tastării statice și a legăturii dinamice este evident. Prima întrebare este: „Când vom ști despre existența componentelor?” - sugerează un răspuns static: " Cu cat mai repede cu atat mai bine", ceea ce înseamnă: la momentul compilării. A doua întrebare," Ce componentă ar trebui să folosesc?" sugerează un răspuns dinamic:" cel de care ai nevoie", - corespunzătoare tipului de obiect dinamic care este determinat în timpul execuției. Aceasta este singura soluție acceptabilă dacă legătura statică și dinamică produce 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

Întrebarea de tastare: când să vă asigurați că va exista o componentă trenul_de_aterizare inferior("extinde trenul de aterizare"), aplicabil obiectului (pentru COPTER nu va fi deloc) Problema legării: pe care dintre mai multe variante posibile să o alegem.

Legătura 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 numi versiunea dezvoltată pentru avioanele convenționale din seria 747, și nu pentru modificarea acestora 747-400. Legătura dinamică aplică operația cerută de obiect și aceasta este abordarea corectă.

Cu tastarea statică, compilatorul nu va respinge apelul dacă se poate garanta că atunci când execută programul către entitate aeronava_mea 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 entitatea ca AVION, - clasa care conține componenta necesară - totul va fi bine.

Tastarea dinamică în stil Smalltalk necesită așteptarea apelului și verificarea prezenței componentei necesare în momentul executării acesteia. Acest comportament este posibil pentru prototipuri și proiecte 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 ușoară. Combinarea tastării statice cu unele dintre cerințele ingineriei software creează probleme mai complexe decât se vede. Există două mecanisme care provoacă probleme: covarianta- modificarea tipurilor de parametri la suprascriere, ascuns descendent- capacitatea unei clase descendente de a restricționa statutul de export al componentelor moștenite.

Covarianta

Ce se întâmplă cu argumentele unei componente când se suprascrie tipul acesteia? Aceasta este o problemă majoră și am văzut deja o serie de exemple de manifestare a acesteia: dispozitive și imprimante, liste cu una și două legături etc. (vezi Secțiunile 16.6, 16.7).

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

Imaginează-ți o echipă de schi universitară care se pregătește pentru campionat. Clasă FATĂ include schioare de sex feminin, BĂIAT- schiori. Un număr de participanți din ambele echipe sunt clasați, avâ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 restul. (Această regulă, care acordă privilegii celor 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 câștigători. Pentru a afișa acest lucru, folosim o ierarhie de clasă paralelă: CAMERĂ, CAMERA_FATĂși RANKED_GIRL_ROOM.

Iată o schiță a clasei SCHIOR:


- Vecin după număr.
... Alte componente posibile omise în această clasă și în clasele ulterioare...

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


La declararea unei entităţi alte poti refuza 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 despre fixarea tipurilor (vom reveni la ele mai târziu) și să privim problema covarianței în forma sa originală.

Cum se introduc suprascrieri de tip? Regulile necesită cazare separată pentru băieți și fete, câștigători de premii și alți participanți. Pentru a rezolva această problemă, la anulare, vom schimba tipul componentei coleg de cameră după cum se arată mai jos (în continuare, elementele suprascrise sunt subliniate).


- Vecin după număr.

Să redefinim, în consecință, argumentul procedurii acțiune... O versiune mai completă a clasei arată acum astfel:


- Vecin după număr.
- Selectați pe altul ca vecin.

În mod similar, toate generate din SCHIOR clase (nu folosim tip fixing acum). Ca rezultat, avem o ierarhie:

Orez. 17.5. Ierarhia membrilor și redefiniri

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 era un descendent al originalului. Același lucru este valabil și pentru suprascrierea tipului de argument alte subrutine acțiune... Această strategie, după cum știm, se numește covarianță, unde prefixul „ko” indică o modificare comună a tipurilor de parametri și rezultate. Strategia opusă se numește contravarianta.

Toate exemplele noastre sunt dovezi convingătoare ale necesității practice de covarianță.

[X]. Element din listă conectat individual LINKABLE trebuie să fie asociat cu un alt element similar cu el și cu instanța BI_LINKABLE- cu cineva ca tine. Covariantly va trebui să fie suprascris și argumentul în pus corect.

[X]. Orice subrutină din compoziție LINKED_LIST cu un argument de genul LINKABLE când mergi la TWO_WAY_LIST va necesita un argument BI_LINKABLE.

[X]. Procedură set_alternate ia DISPOZITIV-argument în clasă DISPOZITIVși IMPRIMANTA-argument - în clasă IMPRIMANTA.

Supracrierile covariante sunt deosebit de populare, deoarece ascunderea informațiilor duce la crearea de proceduri ale formularului


- Setați atributul la v.

a lucra cu atribut tip SOME_TYPE... Astfel de proceduri sunt în mod natural covariante, deoarece orice clasă care schimbă tipul unui atribut trebuie să suprascrie 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ă ( LISTA_DOUĂ_DĂRI). Operație de adăugare universală infix "+" ia NUMERIC-argument în clasă NUMERIC, REAL- in clasa REALși ÎNTREG- in clasa ÎNTREG... În paralel ierarhii de serviciu telefonic la o procedură start in clasa PHONE_SERVICE poate fi necesar un argument ABORDARE reprezentand adresa abonatului (pentru facturare), in timp ce aceeasi procedura in clasa CORPORATE_SERVICE ar avea nevoie de un argument de genul CORPORATE_ADDRESS.

Orez. 17.6. Servicii de comunicare

Dar o soluție contravariantă? În exemplul cu schiori, ar însemna că dacă, mergi la clasă RANKED_GIRL, tip de rezultat coleg de cameră redefinit ca RANKED_GIRL, apoi, din cauza contravariantei, tipul argumentului acțiune poate fi suprascris pentru a tasta FATĂ sau SCHIOR... Singurul tip care nu este permis într-o soluție contravariantă este RANKED_GIRL! Suficient cât să trezească cele mai rele suspiciuni părinților fetelor.

Ierarhii paralele

Pentru a nu lăsa o piatră 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 deja întâlnită în practică: TWO_ WAY_LIST> LINKED_LISTși BI_LINKABLE> LINKABLE; sau ierarhie cu serviciul telefonic PHONE_SERVICE.

Să avem 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 sunt î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 iar argumentul subrutinei găzdui ar trebui înlocuit cu tip CAMERA_FATĂ, in clasa BĂIAT1- tip BOY_ROOM etc. (Amintiți-vă, încă lucrăm fără fixare.) Ca și în exemplul anterior, contravarianța este inutilă aici.

Voința 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ă (dacă nu să ținem cont 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ță. Este ușor să veniți cu o schemă de sabotaj și este posibil să fi creat deja una singur:


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

Rezultatul ultimului apel, foarte posibil plăcut bărbaților tineri, este exact ceea ce am încercat să evităm prin suprascrieri de tip. Apel acțiune duce la faptul că obiectul BĂIAT, cunoscut ca b iar datorită polimorfismului, pseudonimul primit s tip SCHIOR, devine un vecin al obiectului FATĂ cunoscut ca g... Cu toate acestea, apelul, deși contrazice regulile hostelului, este destul de corect în textul programului, deoarece acțiune-componenta exportata ca parte a 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, apel acțiune- a apela s.acomoda (gr), Unde gr- tip de entitate CAMERA_FATĂ... Rezultatul este același.

Cu o soluție contravariantă a acestor probleme nu ar apărea următoarele: 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 - suprascrie - polimorfism. Acest fapt este descris într-un număr de articole teoretice care sugerează această strategie. Argumentarea nu este foarte convingătoare, 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 peste un corp covariant, ar trebui să acceptați realitatea covariantă și să căutați modalități de a elimina efectul nedorit.

Ascuns de un copil

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

Orez. 17.8. Ascuns de un copil

Un exemplu tipic este componenta add_vertex(adăugați vârf) exportate după clasă POLIGON dar ascuns de descendentul ei DREPTUNGHI(din cauza unei posibile încălcări a invariantului - clasa vrea să rămână dreptunghi):


Exemplu non-programator: clasa Ostrich ascunde metoda Fly pe care a primit-o de la părintele Bird.

Să luăm această schemă așa cum este pentru un moment și să ne punem întrebarea dacă o combinație de moștenire și ascundere ar fi legitimă. Rolul de modelare al ascunzării, ca și covarianța, este afectat de trucurile pe care le poate provoca polimorfismul. Și aici nu este dificil să construiți un exemplu rău intenționat care să permită, în ciuda ascunderii componentei, să o apelați și să adăugați un vârf dreptunghiului:


creați r; - Crearea obiectului 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 provocarea acesteia de către entitate p corect. Ca urmare a execuției, un alt vârf va apărea în dreptunghi, ceea ce înseamnă că va fi creat un obiect nevalid.

Corectitudinea sistemelor și claselor

Avem nevoie de câțiva termeni noi pentru a discuta problemele covarianței și a ascunderii copiilor. Vom suna clasă valabilă un sistem care satisface cele trei reguli de descriere a tipurilor date la începutul prelegerii. Să le reamintim: fiecare entitate are propriul ei tip; tipul argumentului propriu-zis trebuie să fie compatibil cu tipul formalului, aceeași situație este și cu atribuirea; componenta apelată trebuie să fie declarată în clasa sa și exportată în clasa care conține apelul.

Sistemul este numit sistem-valid dacă nu există încălcare de tip atunci când este executat.

În mod ideal, ambele concepte ar trebui să fie aceleași. Cu toate acestea, am văzut deja că un sistem corect de clasă în condiții de moștenire, covarianță și ascundere 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 va construi un contraexemplu în câteva minute; în practică reală, erorile de corectitudine ale sistemelor apar în fiecare zi, dar încălcări ale corectitudinii sistemului chiar și în proiecte mari, de mai mulți ani. sunt extrem de rare.

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 care se fac simțite ale abordării obiectului. Când citiți această carte pentru prima dată, puteți sări peste secțiunile rămase ale acestei prelegeri. Dacă tocmai ați început recent tehnologia OO, atunci ar trebui să stăpâniți mai bine acest material după ce ați studiat cursurile 1-11 ale cursului „Fundamentals of Object-Oriented Design” despre metodologia moștenirii, și în special cursul 6 din cursul „Fundamentals of Object-Oriented Design” privind moștenirea metodologiei.

Corectitudinea sistemelor: prima aproximare

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

Contravarianță și nonvarianță

Contravarianța elimină problemele teoretice asociate cu încălcările 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 viitor.

Originalitatea limbajului C++ este că folosește strategia novarie fără a vă permite să schimbați tipul de argumente în subrutinele suprascrise! Dacă C++ ar fi un limbaj puternic tipizat, tipurile sale de sistem 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ă utilizaț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 varianță, al cărei sens va fi dat de introducerea de noi mecanisme de lucru cu tipuri în loc de suprascrieri covariante.

Utilizarea parametrilor generici

Versatilitatea se află în centrul unei idei interesante lansate de Franz Weber. Să declarăm o clasă SCHIOR1 prin limitarea universalizării parametrilor generici la clasă CAMERĂ:


caracteristica clasa SKIER1
acomoda (r: G) este ... necesita ... face acomodare: = r final

Apoi clasa FATA1 va mostenitor SCHIOR1și așa mai departe. 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 necesită specificarea parametrului generic real CAMERĂ sau CAMERA_FATĂ, așa că combinația greșită devine pur și simplu imposibilă. Limbajul devine nonvariant, 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 proliferare de parametri generici, câte unul pentru fiecare tip de posibil argument covariant. Mai rău, adăugarea unei subrutine covariante cu un argument de tip care nu este în listă va necesita adăugarea unui parametru de clasă generică și, prin urmare, va schimba interfața clasei, provocând modificări tuturor clienților clasei, ceea ce este inacceptabil.

Variabile tipice

O serie de autori, printre care Kim Bruce, David Shang și Tony Simons, au propus 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 folosind variabile de tip;

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

[X]. oferă posibilitatea de a atribui tipuri de limbă ca valori variabilelor de tip.

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, Castagna, Weber și alții.Puteți începe să studiați problema din sursele indicate în notele bibliografice pentru această prelegere. . Nu ne vom ocupa de această problemă și iată de ce.

[X]. Un mecanism variabil de tip implementat corespunzător se încadrează în categoria de a permite unui tip să fie utilizat fără specificația sa completă. Această categorie include versatilitatea și fixarea 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 general poate face față tuturor sarcinilor cu ușurința și simplitatea care sunt inerente universalității și fixării tipului.

[X]. Să presupunem că ați dezvoltat un mecanism variabil generic care poate depăși problemele combinării covarianței și polimorfismului (în timp ce încă ignorați problema ascunderii copiilor). Apoi, dezvoltatorul clasei va fi obligat să intuiție extraordinară pentru a decide în prealabil care dintre componente va fi disponibilă pentru suprascrierea tipurilor din clasele derivate și care nu. Mai jos vom discuta despre această problemă care apare în practica creării de programe și, din păcate, care pune la îndoială aplicabilitatea multor scheme teoretice.

Acest lucru ne obligă să revenim la mecanismele deja discutate: universalitatea mărginită și nemărginită, 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 îndeaproape mecanismul declarațiilor fixate pe care îl cunoaștem.

Când descriu clasele SCHIORși SCHIOR1 nu ai putut să nu vizitezi dorința, folosind declarațiile fixate, să scapi de multe depășiri. Ancorarea este un mecanism tipic covariant. Iată cum va arăta exemplul nostru (toate modificările sunt subliniate):


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

Acum descendenții pot părăsi clasa SCHIOR neschimbat, iar în SCHIOR1 trebuie doar să suprascrie atributul cazare... Entități lipicioase: atribut coleg de camerăși argumente la subrutine acțiuneși găzdui- se va schimba automat. Acest lucru simplifică foarte mult munca și confirmă faptul că, în absența legării (sau a unui alt mecanism similar, de exemplu, variabile de tip), este imposibil să scrieți un produs software OO cu tastare realistă.

Dar ați reușit să eliminați încălcările corectitudinii sistemului? Nu! Ca și înainte, putem depăși verificarea tipului prin efectuarea de atribuiri polimorfe care încalcă corectitudinea sistemului.

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


create b; create g; - Crearea de obiecte BOY și GIRL.
s: = b; - Atribuire polimorfă.

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

Cu toate acestea, nu vom fi fericiți pentru mult timp. În cealaltă direcție, această regulă spune că îi place compatibil cu tipul s... Deci, folosind polimorfismul nu numai al unui obiect s, dar și parametrul g, putem ocoli din nou sistemul de verificare a tipului:


s: SCHIOR; b: BĂIAT; g: ca s; actual_g: FATA;
creați b; create actual_g - Creează obiecte BOY și GIRL.
s: = actual_g; g: = s - Folosește s pentru a adăuga g la FATĂ.
s: = b - Atribuire polimorfă.

Drept urmare, apelul ilegal trece.

Există o cale de ieșire. Dacă suntem gata să folosim fixarea declarațiilor ca singur mecanism de covarianță, atunci putem scăpa de încălcările corectitudinii sistemului prin dezactivarea completă a polimorfismului entităților fixate. Acest lucru va necesita o schimbare a limbii: vom introduce un nou cuvânt cheie ancoră(avem nevoie de această construcție ipotetică doar pentru a o folosi în această discuție):


Permite declarații de tip îi place Doar cand s descris ca ancoră... Să modificăm regulile de compatibilitate pentru a ne asigura: sși elemente precum îi place pot fi concatenate doar (în teme sau prin transmiterea unui argument) unul altuia.

Cu această abordare, eliminăm din limbaj capacitatea de a suprascrie tipul oricăror argumente la o subrutină. În plus, am putea interzice suprascrierea tipului de rezultat, dar acest lucru nu este necesar. Abilitatea de a redefini tipul de atribut este bineînțeles păstrată. Tot suprascrierile tipului de argument vor fi acum efectuate implicit prin mecanismul de postare declanșat de covarianță. Unde, cu abordarea anterioară, clasa D a suprascris componenta moștenită ca:


pe când clasa C- părinte D Arăta


Unde Y a corespuns X apoi acum suprascriind componenta r va arata asa:


Rămâne doar î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 Legare”. Proprietățile abordării sunt atractive:

[X]. Ancorarea 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ă sunt covariante; altele sunt polimorfe. În fiecare dintre cele două categorii, orice alăturare este permisă, 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 pentru începători.

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

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

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

[X]. Este realist (cel puțin în teorie): orice sistem posibil anterior poate fi rescris prin înlocuirea înlocuirilor covariante cu redeclarări fixate. Adevărat, unele dintre îmbinări 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, iar situația în timpul execuției ar trebui rezolvată.

S-ar părea că discuția se poate termina aici. Deci, de ce nu este complet satisfăcătoare abordarea Ancorării? În primul rând, nu am atins încă problema ascunderii copiilor. În plus, motivul principal pentru continuarea discuției este problema deja exprimată atunci când se menționează pe scurt variabilele de tip. Divizarea sferelor de influență pe partea polimorfă și covariantă este oarecum similară cu rezultatul conferinței de la Yalta. El presupune că dezvoltatorul clasei are o intuiție extraordinară că este capabil, pentru fiecare entitate pe care o introduce, în special, pentru fiecare argument, odată pentru totdeauna, să aleagă una dintre cele două posibilități:

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

[X]. O entitate este supusă înlocuirilor de tip, adică este fie fixată, fie ea însăși un pivot.

Dar cum poate un dezvoltator să anticipeze toate acestea? Toată atractivitatea metodei OO, exprimată în multe privințe în principiul Deschis-Închis, este legată tocmai de posibilitatea unor modificări pe care avem dreptul să le facem lucrării efectuate anterior, precum și de faptul că dezvoltatorul de solutii 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, anularea și ascunderea este un fel de „supapă de siguranță” care vă permite să reutilizați o clasă existentă, aproape potrivită pentru scopurile noastre:

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

[X]. Ascunderea de către un copil protejează împotriva multor eșecuri la crearea unei clase. Poți critica un proiect în care DREPTUNGHI, folosindu-se de faptul că el este un descendent POLIGON, încearcă să adauge un vârf. În schimb, s-ar putea propune o structură de moștenire în care formele 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 dezvoltă structuri de moștenire, este întotdeauna de preferat să avem acelea în care nu există scutiri taxonomice... Dar pot fi eliminate complet? Pe măsură ce discutăm restricțiile la export într-o prelegere ulterioară, 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 perfectă, chiar dacă există una.

Analiza globală

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

În timp ce am studiat 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 servește ca exemplu de aplicare corectă a mecanismelor OO importante: primul este polimorfismul, al doilea este suprascrierile de tip. Problemele încep când le combini pentru aceeași entitate s... De asemenea:


problema începe cu unificarea a doi operatori independenți și complet inocenți.

Apelurile eronate duc la încălcarea tipului. În primul exemplu, atribuirea polimorfă atașează un obiect BĂIAT la esență s, ce face g argument invalid acțiuneîntrucât este asociat cu obiectul FATĂ... În al doilea exemplu, către entitate r atașează obiect DREPTUNGHI care exclude add_vertex dintre componentele exportate.

Iată ideea unei noi soluții: în prealabil - static, la verificarea tipurilor de către compilator sau alte instrumente - definim tipărit ale fiecărei entități, 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 setul de tipuri și argumente țintă.

În exemplele noastre, operatorul s: = b indică faptul că clasa BĂIAT aparține setului de tipuri pt s(deoarece ca urmare a executării instrucțiunii de creare creați b aparţine tipografiei pt b). FATĂ, având în vedere prezența instrucțiunilor creați g, aparține setului de tipuri pt g... Dar apoi apelul acțiune va fi invalid în acest scop s tip BĂIATși argumentare g tip FATĂ... De asemenea DREPTUNGHI este în tipărirea pentru p, care se datorează atribuirii polimorfe, însă, apelul add_vertex pentru p tip DREPTUNGHI se dovedește a fi invalid.

Aceste observații ne conduc la ideea de a crea global abordare bazată pe noua regulă de tastare:

Regula corectitudinii sistemului

Apel x.f (arg) este corect de sistem dacă și numai dacă este corect de clasă pentru X, și arg de orice tip din setul respectiv de tipuri.

În această definiție, un apel este considerat corect de clasă dacă nu încalcă regula de apelare 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, cu toate acestea, nu este dificil să extindeți regula la un număr 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 au îndeplinit următoarea instrucțiune a formularului creați (SOME_TYPE) a, adăuga SOME_TYPEîntr-un set de tipuri pentru A... (Pentru simplitate, vom presupune că orice instrucțiune creeaza o va fi înlocuit cu o instrucțiune creați (ATYPE) a, Unde UN FEL- tip de entitate A.)

3 După ce au îndeplinit următoarea sarcină a formularului a: = b, adăugați la setul de tipuri pentru A b.

4 Dacă A există un parametru formal al unei subrutine, apoi, la întâlnirea unui alt apel cu un parametru real 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. Pasul (5) este necesar datorită posibilității lanțurilor de atribuire și transfer (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 ignoră succesiunea instrucțiunilor. Cand


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

într-un set de tipuri pentru s va intra ca TIPUL 1și TIP 2, cu toate că s, având în vedere secvența de instrucțiuni, este capabil să accepte valori doar 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 din punct de vedere sistemic, chiar dacă succesiunea executării lor nu are ca rezultat o încălcare de tip.

Analiza globală a sistemului a fost prezentată (mai detaliat) în capitolul 22 al monografiei. Aceasta a rezolvat atât problema covarianței, cât și problema restricțiilor la export în timpul moștenirii. Totuși, această abordare are un defect practic enervant și anume: se presupune că trebuie să verifice sistem în ansamblu, și nu fiecare clasă separat. Regula (4) se dovedește a fi mortală, care, atunci când apelează o rutină de bibliotecă, va ține cont de toate apelurile sale posibile în alte clase.

Deși atunci au fost propuși algoritmi pentru lucrul cu clase individuale, valoarea lor practică nu a putut fi stabilită. Acest lucru însemna că într-un mediu de programare care suporta compilarea incrementală, întregul sistem ar trebui verificat. Este de dorit să se introducă validarea ca element de procesare locală (rapidă) a modificărilor făcute de utilizator la unele clase. Deși sunt cunoscute exemple de aplicare a abordării globale, 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 a rămas neimplementată de nimeni. (Un alt motiv pentru acest rezultat poate fi complexitatea regulilor de validare în sine.)

Corectitudinea clasei presupune verificarea legată de clasă și, prin urmare, este posibilă cu compilarea incrementală. Corectitudinea sistemului implică o verificare globală a întregului sistem, care intră î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 timp ce rulează un 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. Paradoxal, vom construi ultima soluție pe baza regulă şi mai pesimistă... Desigur, acest lucru va ridica întrebarea cât de realist va fi rezultatul nostru.

Înapoi la Yalta

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

Ca și mai înainte, vom restrânge 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 dintre suspecţi. Orice argument de tip SCHIOR sau generat din ea, nu ne convine din cauza polimorfismului s si covarianta acțiune... Prin urmare, dacă descrii static entitatea alte Cum SCHIORși se atașează dinamic la obiect SCHIOR apoi sună s.share (altul) static va da impresia de a fi ideal, dar va duce la încălcarea tipului 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 la o subrutină covariantă. (În celălalt exemplu al nostru, problema este utilizarea p ca entitate polimorfă și ca țintă a apelării unei subrutine a copilului care ascunde componenta add_vertex.)

Soluția lui Catcall, precum Binding, este de natură 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, cu toate acestea, nu încearcă să fie prea inteligent în căutarea unui set de tipuri posibile pentru entități. În schimb, orice entitate polimorfă este percepută ca suficient de suspectă și este interzis să se alieze cu un cerc de persoane respectabile, inclusiv covarianța și ascunderea de către urmași.

O regulă și mai multe definiții

Regula de tip pentru soluția lui Catcall este simplă:

Regula de tip a lui Catcall

Sumele polimorfe sunt incorecte.

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

Definiție: entitate polimorfă

Esenta X un tip de referință (nu extins) este polimorf dacă are una dintre următoarele proprietăți:

1 Apare în sarcină x: = y unde este esența y este de 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ă numai tipurilor de referință, deoarece entitățile extinse nu pot fi de natură polimorfă.

Î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ă sunteți familiarizat cu formularea conceptului de set de tipuri, veți observa cât de pesimistă arată definiția unei entități polimorfe și cât de ușor este să o testați. Fără a încerca să găsim tot felul de tipuri de entități dinamice, 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 obosim să analizăm apelurile. Dacă subrutina are un argument, atunci este la dispoziția completă a 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 va fi apelată de mai multe ori de către diferiți clienți.

Proprietatea caracteristică a acestei reguli este că nu necesită verificări globale. Pentru a identifica polimorfismul unei entități, este suficient să ne uităm la textul clasei în sine. Dacă salvăm informații despre starea lor de polimorfism pentru toate solicitările (atribute sau funcții), atunci nici măcar nu trebuie să studiem textele strămoșilor. Spre deosebire de căutarea unor seturi de tipuri, puteți descoperi entități polimorfe verificând clasă cu clasă în compilarea incrementală.

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. (Prin efectuarea unui apel necalificat f (...) un fel de calificat Current.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 un concept Catcall bazat pe conceptul CAT. (CAT înseamnă schimbarea disponibilității sau a tipului). O subrutină este o subrutină CAT dacă o redefinire a acestuia are ca rezultat unul dintre cele două tipuri de modificări care, după cum am văzut, sunt potențial periculoase: schimbarea tipului de argument (covariant) sau ascunderea unei componente exportate anterior.

Definiție: rutine CAT

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

Această proprietate permite din nou validarea incrementală: orice înlocuire a tipului de argument sau a stării de export face ca procedura sau funcția să fie o subrutină CAT. Aici urmează conceptul Catcall: un apel de subrutină CAT care poate fi eronat.

Definiție: Catcall

Apelul se numește Catcall dacă o redefinire a subrutinei ar face ca acesta să fie eronat din cauza unei modificări a stării de export sau a tipului argumentului.

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

Provocări acțiuneși add_vertexîn exemplele noastre sunt apeluri de pisică. 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, ceea ce le face exemple perfecte de apeluri polimorfe. Sunt eronate conform regulii de tip Catcall.

Nota

Înainte de a rezuma ceea ce am învățat despre covarianță și ascunderea copiilor, să reiterăm că încălcările corectitudinii sistemului sunt într-adevăr rare. Cele mai importante proprietăți ale tastării statice OO au fost rezumate la începutul prelegerii. Acest set impresionant de mecanisme pentru lucrul cu tipuri, cuplat cu verificarea corectitudinii clasei, deschide calea pentru un mod sigur și flexibil de a construi software.

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

Nu există un răspuns definitiv la această întrebare. Implicațiile interacțiunii insidioase ale tipăririi OO și polimorfismului nu sunt la fel de bine înțelese ca întrebările puse în prelegerile anterioare. În ultimii ani au apărut numeroase publicații pe această temă, link-uri către 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.

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

Soluția de legare este extrem de atractivă. Este simplu, intuitiv și ușor de implementat. Cu atât mai mult trebuie să regretăm imposibilitatea de a susține o serie de cerințe cheie ale metodei OO, reflectate în principiul Open-Closed. Dacă am avea într-adevăr o mare intuiție, atunci fixarea ar fi o soluție grozavă, dar care 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 nevoiți să renunțăm la fixare, atunci soluția Catcall pare a fi cea mai potrivită, este destul de ușor explicabilă ș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 un operator „legitim”, îl puteți permite oricând în siguranță prin introducerea unei încercări de atribuire. Astfel, o serie de verificări pot fi transferate în timpul de execuție al programului. Cu toate acestea, numărul acestor cazuri ar trebui să fie extrem de mic.

Cu titlu de clarificare, ar trebui să remarc că la momentul scrierii acestui articol, soluția Catcall nu a fost implementată. Până când compilatorul nu este adaptat la verificarea 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 în problema reconcilierii tipăririi statice cu polimorfismul combinat cu covarianța și ascunderea copiilor. ..

Deplină concordanță

Pentru a încheia discuția noastră despre covarianță, este util să înțelegem cum metoda generală poate fi aplicată 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ă presupunem că există două liste potrivite, în care prima este schiorii și a doua este colegul de cameră al schiorului din prima listă. Dorim să efectuăm procedura de plasare corespunzătoare acțiune, numai dacă este permis de regulile de descriere a tipurilor, care permit fete cu fete, fete-câștigători cu fete-câștigători etc. Problemele de acest fel sunt frecvente.

Posibil o soluție simplă bazată pe discuții anterioare și pe o încercare de atribuire. Luați în considerare funcția generică montate(a aproba):


montat (altul: GENERAL): ca altul este
- Obiectul curent (Actual), dacă tipul său corespunde tipului obiectului,
- atasat de altul, altfel nul.
if other / = Void and then conforms_to (altul) atunci

Funcţie montate returnează obiectul curent, dar cunoscut ca entitatea de tip atașată argumentului. Dacă tipul obiectului curent nu se potrivește cu tipul obiectului atașat argumentului, atunci acesta revine Vidul... Observați rolul încercării de atribuire. Funcția folosește componenta se conformeaza la din clasa GENERAL, care află compatibilitatea tipurilor 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 schimb acțiune, (acesta din urmă se poate face cu o procedură ascunsă).


- Selectați, dacă este cazul, altul ca vecin după număr.
- gender_acertained - gen stabilit
gender_acertained_other: ca Current
gender_acertained_other: = other .fitted (Actual)
if gender_acertained_other / = Void atunci
distribuie (gender_acertained_other)
„Concluzie: nu este posibilă asocierea cu alții”

Pentru alte tip arbitrar SCHIOR(nu numai precum Current) definiți versiunea sex_determinat_altul de tipul atribuit Actual... Funcția ne va ajuta să garantăm identitatea tipurilor. perfect_ montat.

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


ocupant1, ocupant2: LISTĂ

puteți organiza o buclă apelând la fiecare pas:


ocupant1.item.safe_share (occupant2.item)

articolele din listă care se potrivesc 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, versatilitate limitată versus nelimitată, declarații fixate. Sistemul de tip nu trebuie să permită capcane (tip aruncări).

[X]. O regulă generală pentru redeclarare este să permită redefinirea covariantă. Tipurile de rezultate și argumente atunci când sunt suprascrise trebuie să fie compatibile cu originalul.

[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 gravă.

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

Note bibliografice

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

A fost introdusă noțiunea de deducere automată a tipului, unde este descris algoritmul de deducere a tipului al limbajului funcțional ML. Relația dintre polimorfism și verificarea tipului a fost explorată în lucrare.

Tehnici pentru îmbunătățirea eficienței codului limbilor 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.

Ghidul ISE include o introducere în problemele de utilizare a polimorfismului, covarianței și a ascunderii copiilor împreună. 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 a fost comentariul lui Philippe Elinck în lucrarea sa de licență „De la Conception-Programmation par Objets”, Memoire de licence, Universite Libre de Bruxelles (Belgia), 1988), exprimată în lucrările lui I. Articolul lui Cook oferă câteva exemple legate de problema covarianței și încearcă să o rezolve. O soluție bazată pe parametri tipici pentru entitățile covariante pe TOOLS EUROPE 1992 a fost propusă de Franz Weber. Definițiile exacte ale conceptelor de corectitudine a sistemului, precum și corectitudinea clasei, sunt date în, unde se propune o soluție folosind o analiză completă a sistemului. Soluția lui Catcall a fost propusă pentru prima dată în; Vezi si .

Soluția de fixare a fost prezentată în prezentarea mea la atelierul TOOLS EUROPE 1994. La acel moment însă, nu vedeam nevoia ancoră- declarații și restricții de compatibilitate aferente. Paul Dubois și Amiram Yehudai s-au grăbit să sublinieze că în aceste condiții rămâne problema covarianței. Ei, împreună cu Reinhardt Budde, Karl-Heinz Sylla, Kim Walden și James McKim, au făcut multe puncte critice în munca care a condus la scrierea acestei prelegeri.

O mare parte de literatură a fost 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 materialele de teorie de tip OOP online ș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 de la începutul anilor '80, dar nu le-a folosit în tipărire până la sfârșitul anilor '80.

Trucurile bazate pe variabile generice sunt descrise în,,.

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

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

  • Tastare dinamică: așteptați momentul finalizării fiecărui apel și apoi luați o decizie.
  • Tastare statică: Pe baza unui 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: când tastare dinamică verificarea tipului are loc în timp ce sistemul rulează (dinamic) și când tastare statică verificarea se efectuează asupra textului static (înainte de execuție).

Tastare statică presupune o verificare automată, de obicei atribuită compilatorului. Ca rezultat, avem o definiție simplă:

Definiție: limbaj tipizat static

Un limbaj OO este tipizat static dacă vine cu un set de reguli consecvente verificate de compilator pentru a se asigura că execuția sistemului nu duce la încălcarea tipului.

În literatură, termenul „ puternic tastare "( puternic). Se conformează naturii ultimatum a definiției, care nu necesită deloc încălcarea tipului. Posibil și slab (slab) forme tastare staticăîn care regulile elimină anumite încălcări fără a le elimina în totalitate. În acest sens, unele limbi OO sunt tastate slab static. Vom lupta pentru cea mai puternică tastare.

În dinamic limbi dactilografiate cunoscut ca netipizat, 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 sale de tip au fost introduse în prelegerile anterioare și se reduc la trei cerințe simple.

  • 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 specificat, de exemplu: put (x: G; i: INTEGER).
  • În orice atribuire x: = y și în orice apel de subrutină în care y este argumentul real pentru argumentul formal x, tipul sursă y trebuie să fie compatibil cu tipul țintă x. Definiția compatibilității se bazează pe moștenire: B este compatibil cu A dacă este un descendent al acestuia, completat de reguli pentru parametrii generici (vezi „Introducere în Moștenire”).
  • Apelul către x.f (arg) necesită ca f să fie o componentă a clasei de bază pentru tipul țintă x și f trebuie să fie 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. Luați în considerare două cazuri extreme.

  • Limbajul perfect corect, în care fiecare sistem corect din punct de vedere sintactic este corect în ceea ce privește tipurile. Regulile de declarare a tipului nu sunt necesare. Astfel de limbi există (imaginați-vă notația poloneză pentru o expresie cu adunare și scădere de numere întregi). Din păcate, niciun limbaj universal real nu îndeplinește acest criteriu.
  • Limbă complet incorectă 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 tastat: deoarece nu există sisteme care se potrivesc cu regulile, niciun sistem nu va cauza încălcarea tipului.

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

În practică, avem nevoie de un sistem de tip care să fie în același timp potrivit și util: suficient de puternic pentru a răspunde nevoilor de calcul și suficient de convenabil pentru a nu ne obliga să complicăm lucrurile pentru a satisface regulile de tastare.

Să spunem că limba realist dacă este potrivit pentru utilizare și util în practică. Spre deosebire de definitie tastare statică dând un răspuns categoric la întrebarea: „ Este tip X static?”, definiția realismului este parțial subiectivă.

În această prelegere, vom verifica dacă notația pe care o propunem este realistă.

Pesimism

Tastare statică duce prin natura sa la o politica „pesimista”. O încercare de a garanta asta toate calculele nu duc la eșecuri, respinge calcule care s-ar fi putut termina fără eroare.

Luați în considerare un limbaj obișnuit, non-obiect, asemănător Pascal, cu diferite tipuri de REAL și INTEGER. Când se descrie n: INTEGER; r: Operatorul real n: = r va fi respins ca încălcare a regulilor. Astfel, compilatorul va respinge toate următoarele afirmații:

n: = 0,0 [A] n: = 1,0 [B] n: = -3,67 [C] n: = 3,67 - 3,67 [D]

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

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

unde merge atribuirea de neatins (n ​​^ 2 este pătratul lui n). După înlocuirea n ^ 2 cu n, doar o serie de porniri va da rezultatul corect. Atribuirea unei valori mari în virgulă mobilă non-întreg la n va eșua.

V limbi dactilografiate toate aceste exemple (funcționează, nu funcționează, uneori funcționează) sunt interpretate fără milă ca încălcări ale regulilor de declarare a tipului și respinse de orice compilator.

Întrebarea nu este vom suntem pesimiști și în asta, cât costă ne putem permite să fim pesimiști. Să ne întoarcem 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 vine cu o mică pierdere a puterii expresive, le vom accepta. De exemplu, într-un mediu de dezvoltare care oferă rotunjire și trunchiere, operatorul n: = r este considerat invalid deoarece vă obligă să scrieți în mod explicit conversia real-întreg în loc să utilizați conversiile ambigue implicite.

Tastarea statică: cum și de ce

Deși beneficiile tastare statică evident, este o idee bună să vorbim din nou despre ele.

Avantaje

Motive pentru utilizare tastare staticăîn tehnologia obiectelor am enumerat la începutul prelegerii. Acestea sunt fiabilitatea, ușurința de înțelegere și eficiența.

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

Detectarea din timp a erorilor este, de asemenea, importantă, deoarece cu cât întârziem găsirea lor, cu atât costul remedierii lor va crește. Această proprietate, înțeleasă intuitiv de către toți programatorii profesioniști, este confirmată cantitativ de binecunoscutele lucrări ale lui Boehm. Dependența costului remedierii de timpul de găsire a erorilor este prezentată în grafic, construit în funcție de datele unui număr de proiecte industriale mari și experimente efectuate cu un proiect mic de gestionat:


Orez. 17.1.

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 faza de întreținere.

In cele din urma, eficienţă poate determina succesul sau eșecul tehnologiei obiectelor în practică. În lipsa tastare statică x.f (arg) poate dura orice perioadă de execuție. Motivul pentru aceasta este că în timpul execuției, dacă f nu este găsit în clasa de bază a țintei x, căutarea va continua în descendenții săi, ceea ce este un drum sigur către ineficiență. Puteți atenua problema îmbunătățind căutarea unei componente în ierarhie. Autorii cărții Self au făcut o treabă grozavă încercând să genereze cel mai bun cod pentru limbajul tip dinamic. Dar este exact tastare statică a permis unui astfel de produs OO să se apropie sau să egaleze eficiența software-ului tradițional.

Cheia pentru tastare statică este ideea deja afirmată că compilatorul care generează codul pentru construcția x.f (arg) cunoaște tipul x. Datorită polimorfismului, nu există nicio modalitate de a determina fără ambiguitate versiunea adecvată a componentei f. Dar declarația restrânge numeroasele tipuri posibile, permițând compilatorului să construiască un tabel care oferă acces la f-ul corect cu o supraîncărcare minimă - constantă mărginită complexitatea accesului. Optimizări suplimentare efectuate legarea staticăși inliniere- a fost, de asemenea, mai ușor datorită tastare statică eliminând complet costurile acolo unde este cazul.

Argumente pentru tastarea dinamică

Cu toate acestea, tastare 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 sunt siguri că tastare staticăîi limitează prea mult, împiedicându-i să-și exprime liber ideile creative, numindu-i uneori „centa de castitate”.

Se poate fi de acord cu acest raționament, 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ă acțiunilor noastre flexibilitate și oferă ne oferă oportunitatea de a ne bucura pe deplin de practic. tastare statică.

Tastarea: termenii succesului

Care sunt mecanismele realistei tastare statică? Toate au fost introduse în prelegerile anterioare și, prin urmare, trebuie doar să le amintim pe scurt. Listarea lor împreună arată consistența și puterea combinării lor.

Sistemul nostru de tip se bazează în întregime pe concept clasă... Chiar și tipurile de bază, cum ar fi INTEGER, sunt clase ș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 mai vechi este combinat cu tehnologia obiectelor bazată pe clasă.)

Tipuri extinse oferă-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... Aceasta depășește o limitare majoră a limbajelor tipizate clasice, de exemplu, Pascal și Ada, în care operatorul x: = y necesită ca tipurile de x și y să fie aceleași. Această regulă este prea strictă: interzice utilizarea entităților care pot denota obiecte de tipuri înrudite (SAVINGS_ACCOUNT și CHECKING_ACCOUNT). În moștenire, cerem doar compatibilitate de tip y cu tipul x, de exemplu, x este de tip ACCOUNT, y este SAVINGS_ACCOUNT, iar a doua clasă moștenește de la prima.

În practică, un limbaj tipizat static are nevoie de sprijin moștenire multiplă... Sunt cunoscute acuzațiile fundamentale tastare statică prin aceea că nu oferă o oportunitate de a interpreta obiectele în mod diferit. De exemplu, obiectul DOCUMENT (document) poate fi transmis prin rețea și, prin urmare, are nevoie de componente asociate cu tipul MESSAGE (mesaj). Dar această critică este valabilă numai pentru limbi limitate moștenire unică.


Orez. 17.2.

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

În unele cazuri, este necesară versatilitatea limită, care vă permite să utilizați operațiuni care sunt aplicabile numai entităților de tip generic. Dacă clasa generică SORTABLE_LIST acceptă sortarea, aceasta necesită entități de tip G, unde G este un parametru generic, să aibă o operație de comparare. Acest lucru se realizează prin legarea unei clase de constrângeri generice la G, COMPARABLE:

clasa SORTABLE_LIST...

Orice SORTABLE_LIST generic real trebuie să fie un descendent al clasei COMPARABLE care are componenta necesară.

Un alt mecanism necesar este încercare de atribuire- organizează accesul la acele obiecte, tipul cărora software-ul nu le controlează. Dacă y este un obiect de bază de date sau un obiect preluat printr-o rețea, atunci x? = Y va atribui x lui y dacă y este un tip compatibil sau, dacă nu este, dă x lui Void.

Afirmații asociate 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 specificarea tipului... Limbi precum Pascal și Ada au tipuri de intervale care pot limita valorile unei entități, de exemplu, la intervalul de la 10 la 20, cu toate acestea, folosindu-le, nu vă veți putea asigura că valoarea lui i este negativ, întotdeauna de două ori mai mare decât j. Invarianții de clasă vin în ajutor, proiectați să reflecte cu acuratețe constrângerile impuse, indiferent cât de complexe ar fi acestea.

Anunțuri fixate sunt necesare pentru a evita duplicarea codului de avalanșă în practică. Anunțând y: ca x, sunteți garantat că y se va schimba în urma oricăror declarații repetate de tip x în copil. Fără acest mecanism, dezvoltatorii ar fi neîncetat ocupați cu re-declarații, încercând să mențină diferitele tipuri consistente.

Declarațiile lipicioase sunt un caz special al ultimului motor de limbă de care avem nevoie - covarianta, despre care vom discuta în detaliu mai târziu.

La dezvoltarea sistemelor software, de fapt, este necesară încă o proprietate, care este inerentă mediului de dezvoltare însuși - recompilare incrementală rapidă... Când scrieți sau modificați un sistem, doriți să vedeți efectul schimbării cât mai curând posibil. La tastare statică ar trebui să acordați timp compilatorului să verifice tipul. Rutinele tradiționale de compilare necesită recompilarea întregului sistem (și ansamblurile sale), iar acest proces poate fi chinuitor de lung, mai ales odată 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 au pornit sistemul cu procesare redusă sau deloc, fără verificare de tip. Acest argument este acum uitat. Un compilator modern bun detectează modul în care s-a schimbat codul de la ultima compilare și procesează doar modificările pe care le găsește.

— Copilul este tastat?

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

Cea mai comună lacună în static limbi dactilografiate este prezența transformărilor care schimbă tipul de entitate. În C și derivatele sale, ele sunt numite „turnare” sau turnare (turnare). Intrarea (OTHER_TYPE) x indică faptul că valoarea x este interpretată de compilator ca fiind de tip OTHER_TYPE, sub rezerva unor restricții privind tipurile posibile.

Astfel de mecanisme ocolesc limitările verificării tipului. Castingul este larg răspândit în programarea C, inclusiv în dialectul ANSI C. Chiar și în C ++, castingul, deși nu este la fel de frecvent, rămâne obișnuit și poate necesar.

Pentru a respecta regulile tastare statică nu este atât de ușor dacă în orice moment pot fi ocolite prin turnare.

Tastarea și legarea

Deși, în calitate de cititor al acestei cărți, cu siguranță vei distinge între scrierea statică și cea statică. legare, sunt oameni care nu pot face asta. Acest lucru se poate datora în parte influenței Smalltalk, care susține abordare dinamică la ambele probleme și este capabil să-și formeze concepția greșită că au aceeași soluție. (În cartea noastră susținem că este de dorit să combinați tastarea statică și conectarea dinamică pentru a crea programe robuste și flexibile.)

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

Tastarea și legarea

  • Întrebarea de tastare: când trebuie să știm sigur că la runtime va exista o operație corespunzătoare lui f, aplicabilă obiectului atașat entității x (cu parametrul arg)?
  • Întrebare de legătură: când trebuie să știm ce operațiune inițiază un anumit apel?

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

În cadrul abordării obiectului:

  • problema cu tastarea este cu polimorfism: din moment ce 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;
  • problema de legare este cauzată de anunţuri repetate: deoarece o clasă poate modifica componentele moștenite, pot exista două sau mai multe operații care pretind că reprezintă f într-un apel dat.

Ambele sarcini pot fi rezolvate atât dinamic, cât și static. Toate cele patru soluții sunt prezentate în limbile existente.

Și legătura dinamică este întruchipată în notația sugerată î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, legătură statică(implicit), legătură dinamică atunci când se specifică explicit virtual ( virtual) reclame.

Motivul alegerii tastare statică iar legătura dinamică este evidentă. Prima întrebare este: „Când vom ști despre existența componentelor?” - sugerează un răspuns static: " Cu cat mai repede cu atat mai bine", ceea ce înseamnă: la momentul compilării. A doua întrebare," Ce componentă ar trebui să folosesc?" sugerează un răspuns dinamic:" cel de care ai nevoie", - corespunzător tip dinamic un obiect care este definit în timpul execuției. Aceasta este singura soluție viabilă dacă legătura statică și dinamică produce rezultate diferite.

La tastare statică compilatorul nu va respinge apelul dacă se poate garanta că atunci când programul este executat, obiectul furnizat cu componenta corespunzătoare low_landing_gear va fi atașat la entitatea my_aircraft. Tehnica de bază pentru obținerea garanțiilor este simplă: declarația obligatorie a my_aircraft necesită ca clasa de bază de tipul acesteia să includă o astfel de componentă. Prin urmare, my_aircraft nu poate fi declarat ca AIRCRAFT, deoarece acesta din urmă nu are low_aterizare_train la acest nivel; elicopterele, cel puțin în exemplul nostru, nu știu cum să elibereze trenul de aterizare. Dacă declarăm entitatea ca AVION, - clasa care conține componenta necesară - totul va fi bine.

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

Acest articol discută diferența dintre limbile tip static și tip dinamic, examinează conceptele de tastare „puternică” și „slabă” și compară puterea sistemelor de tastare în diferite limbi. Recent, a existat o mișcare clară către sisteme de tastare mai stricte și mai puternice în programare, așa că este important să înțelegem ce înseamnă când vorbim despre tipuri și tastare.



Un tip este o colecție de valori posibile. Un număr întreg poate avea valorile 0, 1, 2, 3 și așa mai departe. Booleanul poate fi adevărat sau fals. Puteți veni cu propriul tip, de exemplu, tipul „GiveFive”, în care valorile „dau” și „5” sunt posibile și nimic altceva. Nu este un șir sau un număr, este un tip nou, separat.


Limbile tipizate static restricționează tipurile de variabile: un limbaj de programare ar putea ști, de exemplu, că x este un număr întreg. În acest caz, programatorului îi este interzis să facă x = true, acesta va fi un cod incorect. Compilatorul va refuza să îl compileze, așa că nici măcar nu putem rula un astfel de cod. Un alt limbaj tipizat static poate avea capacități expresive diferite și niciunul dintre sistemele de tip populare nu este capabil să exprime tipul nostru de DayFive (dar mulți pot exprima alte idei mai sofisticate).


Limbile tastate dinamic marchează valorile cu tipuri: limba știe că 1 este un întreg, 2 este un întreg, dar nu poate ști că variabila x conține întotdeauna un întreg.


Runtime-ul limbajului verifică aceste etichete în momente diferite. Dacă încercăm să adunăm două valori împreună, poate verifica dacă sunt numere, șiruri sau matrice. Apoi va adăuga aceste valori, le va lipi sau va da o eroare, în funcție de tip.

Limbi tipizate static

Limbajele statice verifică tipurile dintr-un program în timpul compilării, chiar înainte ca programul să ruleze. Orice program în care tipurile încalcă regulile limbii este considerat nevalid. De exemplu, majoritatea limbilor statice vor respinge expresia „a” + 1 (C este excepția de la această regulă). Compilatorul știe că „a” este un șir și 1 este un număr întreg și că + funcționează numai atunci când părțile din stânga și din dreapta sunt de același tip. Deci nu trebuie să ruleze programul pentru a realiza că există o problemă. Fiecare expresie dintr-un limbaj tipizat static este de un tip specific pe care îl puteți defini fără a rula codul.


Multe limbi tipizate static necesită desemnarea tipului. Funcția Java public int add (int x, int y) ia două numere întregi și returnează al treilea număr întreg. Alte limbi tipizate static pot determina tipul automat. Aceeași funcție de adunare în Haskell arată astfel: adăugați x y = x + y. Nu spunem limbajului despre tipuri, dar poate să le dea seama singur, deoarece știe că + funcționează doar pe numere, deci x și y trebuie să fie numere, așa că adunarea ia două numere ca argumente.


Acest lucru nu reduce natura „statică” a sistemului de tip. Sistemul de tip Haskell este renumit pentru că este static, strict și puternic, iar Haskell este înaintea Java pe toate aceste fronturi.

Limbi tipizate dinamic

Limbile tastate dinamic nu necesită specificarea tipului, dar nu îl definesc ele însele. Tipurile de variabile sunt necunoscute până când au valori specifice la pornire. De exemplu, o funcție în Python


def f (x, y): returnează x + y

putem adăuga două numere întregi, șiruri de lipire, liste și așa mai departe și nu ne putem da seama ce se întâmplă exact până când rulăm programul. Poate că, la un moment dat, f va fi numit cu două șiruri și cu două numere într-un alt moment. În acest caz, x și y vor conține valori de diferite tipuri în momente diferite. Prin urmare, se spune că valorile în limbaje dinamice au un tip, dar variabilele și funcțiile nu. O valoare de 1 este cu siguranță un număr întreg, dar x și y pot fi orice.

Comparaţie

Majoritatea limbajelor dinamice vor genera o eroare dacă tipurile sunt utilizate incorect (JavaScript este o excepție cunoscută; încearcă să returneze o valoare pentru orice expresie, chiar și atunci când nu are sens). Când utilizați limbaje tastate dinamic, chiar și o eroare simplă precum „a” + 1 poate apărea în mediul de producție. Limbajele statice previn astfel de erori, dar, desigur, gradul de prevenire depinde de cardinalitatea sistemului de tip.


Limbajele statice și dinamice sunt construite pe idei fundamental diferite despre corectitudinea programului. În limbajul dinamic „a” + 1, acesta este un program corect: codul va rula și o eroare va apărea în mediul de rulare. Cu toate acestea, în majoritatea limbilor tipizate static, expresia „a” + 1 este nu un program: nu va fi compilat și nu va rula. Acesta nu este un cod valid, la fel ca o grămadă de caractere aleatorii! &% ^ @ * &% ^ @ * Este un cod nevalid. Acest concept suplimentar de corectitudine și incorectitudine nu are echivalent în limbajele dinamice.

Tastare puternică și slabă

Conceptele de „puternic” și „slab” sunt foarte ambigue. Iată câteva exemple de utilizare a acestora:

    Uneori „puternic” înseamnă „static”.
    Este simplu, dar este mai bine să folosiți termenul „static” pentru că majoritatea oamenilor îl folosesc și îl înțeleg.

    Uneori, „puternic” înseamnă „nu face conversie implicită de tip”.
    De exemplu, JavaScript vă permite să scrieți „a” + 1, care poate fi numit „tastare slabă”. Dar aproape toate limbile oferă un anumit nivel de conversie implicită care vă permite să treceți automat de la numere întregi la numere în virgulă mobilă, cum ar fi 1 + 1.1. În realitate, majoritatea oamenilor folosesc cuvântul „puternic” pentru a defini linia dintre transformarea acceptabilă și cea inacceptabilă. Nu există o graniță general acceptată, toate sunt imprecise și depind de opinia unei anumite persoane.

    Uneori, „puternic” înseamnă că nu există nicio cale de a evita regulile puternice de tastare din limbă.

  • Uneori, „puternic” înseamnă sigur pentru memorie.
    C este un exemplu de limbaj nesigur în memorie. Dacă xs este o matrice de patru numere, atunci C va executa cu plăcere codul xs sau xs, returnând o valoare din memorie imediat după xs.

Să ne oprim. Iată cum unele limbi îndeplinesc aceste definiții. După cum puteți vedea, doar Haskell este constant puternic în toate privințele. Majoritatea limbilor nu sunt atât de clare.



(„Când cum” din coloana Conversii implicite înseamnă că diviziunea între puternic și slab depinde de conversiile pe care le considerăm acceptabile).


Adesea, termenii „puternic” și „slab” se referă la o combinație vagă de definiții diferite de mai sus și alte definiții care nu sunt prezentate aici. Toată această confuzie face ca cuvintele „puternic” și „slab” să nu aibă sens practic. Când doriți să folosiți acești termeni, este mai bine să descrieți ce înseamnă exact. De exemplu, ați putea spune că „JavaScript returnează atunci când un șir este adăugat cu un număr, dar Python returnează o eroare”. În acest caz, nu ne vom irosi energia încercând să ajungem la un acord asupra multiplelor sensuri ale cuvântului „puternic”. Sau, și mai rău: ajungem cu o neînțelegere nerezolvată din cauza terminologiei.


În cele mai multe cazuri, termenii „puternic” și „slab” de pe internet sunt opinii vagi și prost definite ale anumitor persoane. Sunt folosite pentru a numi un limbaj „rău” sau „bun”, iar această opinie este tradusă în jargon tehnic.



Tastare puternică: un sistem de tipare pe care îl iubesc și cu care mă simt confortabil.

Tastare slabă: sistemul de tip care mă deranjează sau cu care nu mă simt confortabil.

Tastarea treptată

Pot fi adăugate tipuri statice limbilor dinamice? În unele cazuri, da. În altele este dificil sau imposibil. Cea mai evidentă problemă este eval și alte capacități similare ale limbajelor dinamice. Făcând 1 + eval ("2") în Python dă 3. Dar ce oferă 1 + eval (read_from_the_network ())? Depinde de ceea ce este în rețea la momentul execuției. Dacă obținem un număr, atunci expresia este corectă. Dacă un șir, atunci nu. Nu este posibil să știți înainte de lansare, așa că nu este posibil să analizați tipul static.


O soluție nesatisfăcătoare în practică este de a da eval () tipul Any, care amintește de Object în unele limbaje de programare orientate pe obiecte sau interfață () în Go: este un tip pe care orice valoare îl satisface.


Valorile de tip Any nu sunt limitate de nimic, astfel încât capacitatea sistemului de tip de a ne ajuta în codul cu eval dispare. Limbile care au atât eval cât și un sistem de tip trebuie să renunțe la siguranța tipului de fiecare dată când este utilizat eval.


Unele limbi au tastare opțională sau graduală: sunt dinamice în mod implicit, dar permit adăugarea unor adnotări statice. Python a adăugat recent tipuri opționale; TypeScript este un add-on JavaScript care are tipuri opționale; Flow efectuează o analiză statică a codului JavaScript vechi bun.


Aceste limbi oferă unele dintre beneficiile tastării statice, dar nu oferă niciodată garanția absolută că limbile cu adevărat statice sunt. Unele funcții vor fi tastate static, iar altele vor fi tastate dinamic. Programatorul trebuie să știe întotdeauna și să se ferească de diferență.

Compilarea codului tastat static

La compilarea codului tip static, sintaxa este verificată mai întâi, la fel ca orice compilator. Apoi tipurile sunt verificate. Aceasta înseamnă că un limbaj static poate raporta inițial o eroare de sintaxă și, după ce o remediază, să se plângă de 100 de erori de tastare. Remedierea erorilor de sintaxă nu a generat cele 100 de erori de tastare. Compilatorul pur și simplu nu avea nicio modalitate de a detecta erorile de tip până când sintaxa a fost remediată.


Compilatoarele pentru limbaje statice pot genera de obicei cod mai rapid decât compilatoarele pentru cele dinamice. De exemplu, dacă compilatorul știe că funcția de adăugare acceptă numere întregi, atunci poate folosi instrucțiunea nativă ADD CPU. Limbajul dinamic va verifica tipul în timpul execuției, alegând una dintre numeroasele funcții de adăugare în funcție de tipuri (adăugarea de numere întregi sau flotanți, sau lipirea șirurilor de caractere sau poate liste?) Sau trebuie să decideți că a existat o eroare și tipurile nu. Meci. Toate aceste verificări necesită timp. Limbajele dinamice folosesc o varietate de trucuri de optimizare, cum ar fi compilarea just-in-time, în care codul este recompilat în timpul execuției după obținerea tuturor informațiilor de care are nevoie despre tipuri. Cu toate acestea, niciun limbaj dinamic nu poate egala viteza codului static bine scris într-o limbă precum Rust.

Argumente pentru tipurile statice versus dinamice

Susținătorii unui sistem de tip static subliniază că fără un sistem de tip, greșelile simple pot duce la probleme în producție. Acest lucru este, desigur, adevărat. Oricine a folosit limbajul dinamic a experimentat asta pentru ei înșiși.


Susținătorii limbilor dinamice subliniază că astfel de limbi par să fie mai ușor de codificat. Acest lucru este cu siguranță adevărat pentru unele tipuri de cod pe care le scriem din când în când, cum ar fi acel cod cu eval. Aceasta este o decizie controversată pentru munca obișnuită și are sens să ne amintim aici cuvântul vag „ușor”. Rich Hickey a vorbit excelent despre cuvântul „ușor” și legătura acestuia cu cuvântul „simplu”. După ce ai urmărit această discuție, îți vei da seama că nu este ușor să folosești corect cuvântul „ușor”. Atenție la „ușurință”.


Avantajele și dezavantajele sistemelor de tastare statică și dinamică sunt încă puțin înțelese, dar sunt cu siguranță specifice limbii și specifice sarcinii în cauză.


JavaScript încearcă să continue, chiar dacă înseamnă o conversie fără sens (cum ar fi „a” + 1 rezultă „a1”). Python, pe de altă parte, încearcă să fie conservator și returnează adesea erori, așa cum este cazul cu „a” + 1.


Există abordări diferite cu niveluri de securitate diferite, dar Python și JavaScript sunt ambele limbaje tipizate dinamic.



Haskell, pe de altă parte, nu vă va permite să adăugați un întreg și să flotați fără o conversie explicită înainte de a o face. C și Haskell sunt ambele tipizate static, în ciuda diferențelor atât de mari.


Există multe variații ale limbajelor dinamice și statice. Orice afirmație necondiționată precum „limbajele statice sunt mai bune decât limbajele dinamice când vine vorba de X” este aproape garantată o prostie. Acest lucru poate fi adevărat pentru anumite limbi, dar atunci este mai bine să spuneți „Haskell este mai bun decât Python când vine vorba de X”.

Varietate de sisteme de tastare statică

Să aruncăm o privire la două exemple celebre de limbi tipizate static: Go și Haskell. Sistemul de tastare al lui Go nu are tipuri generice, tipuri cu „parametri” din alte tipuri. De exemplu, vă puteți crea propriul tip pentru listele MyList, care poate stoca orice date de care avem nevoie. Dorim să putem crea o MyList de numere întregi, o MyList de șiruri de caractere și așa mai departe, fără a schimba codul MyList original. Compilatorul trebuie să aibă grijă la tastare: dacă există o Lista My de numere întregi și adăugăm accidental un șir acolo, atunci compilatorul trebuie să respingă programul.


Go a fost conceput în mod deliberat, astfel încât să nu puteți defini tipuri precum MyList. Cel mai bun lucru pe care îl puteți face este să creați o MyList de „interfețe goale”: MyList poate conține obiecte, dar compilatorul pur și simplu nu le cunoaște tipul. Când primim obiecte din MyList, trebuie să spunem compilatorului tipul lor. Dacă spunem „Primesc un șir”, dar în realitate valoarea este un număr, atunci va exista o eroare de execuție, așa cum este cazul limbajelor dinamice.


De asemenea, Go nu are multe dintre celelalte caracteristici găsite în limbile moderne tipizate static (sau chiar unele sisteme din anii 1970). Creatorii lui Go au avut propriile motive pentru a lua aceste decizii, dar opiniile celor din afară cu privire la această problemă pot suna uneori dure.


Acum să comparăm cu Haskell, care are un sistem de tip foarte puternic. Dacă este setat să tastați MyList, tipul „listă de numere” este pur și simplu MyList Integer. Haskell ne împiedică să adăugăm accidental un șir în listă și se asigură că nu punem un element din listă într-o variabilă șir.


Haskell poate exprima idei mult mai complexe direct cu tipuri. De exemplu, Num a => MyList a înseamnă „MyList de valori care sunt de același tip de numere”. Ar putea fi o listă de numere întregi, flotanți sau numere zecimale cu precizie fixă, dar cu siguranță nu va fi niciodată o listă de șiruri care este verificată în timpul compilării.


Puteți scrie o funcție de adăugare care funcționează pe orice tip numeric. Această funcție va avea tipul Num a => (a -> a -> a). Inseamna:

  • a poate fi orice tip numeric (Num a =>).
  • Funcția ia două argumente de tip a și returnează tipul a (a -> a -> a).

Ultimul exemplu. Dacă tipul funcției este String -> String, atunci ia un șir și returnează un șir. Dar dacă este String -> IO String, atunci face și unele I/O. Acesta poate fi accesul la un disc, la rețea, citirea de pe un terminal și așa mai departe.


Dacă funcția din tip Nu IO, atunci știm că nu efectuează nicio operație I/O. Într-o aplicație web, de exemplu, puteți spune dacă o funcție modifică o bază de date doar privind tipul acesteia. Nicio limbă dinamică și aproape nicio limbă statică nu este capabilă să facă acest lucru. Aceasta este o caracteristică a limbilor cu cel mai puternic sistem de tastare.


În majoritatea limbilor, ar trebui să ne ocupăm de funcția și de toate funcțiile care sunt apelate de acolo și așa mai departe, încercând să găsim ceva care modifică baza de date. Este un proces obositor și este ușor să faci greșeli. Iar sistemul de tip Haskell poate răspunde la această întrebare simplu și cu garanție.


Comparați această putere cu Go, care este incapabil să exprime ideea simplă a MyList, darămite „o funcție care ia două argumente, ambele sunt numerice și de același tip și face I/O”.


Abordarea Go facilitează scrierea instrumentelor pentru programare în Go (în special, implementarea compilatorului poate fi simplă). În plus, există mai puține concepte de învățat. Cum se compară aceste beneficii cu limitările semnificative este o întrebare subiectivă. Cu toate acestea, nu se poate argumenta că Haskell este mai greu de învățat decât Go și că sistemul de tipări al lui Haskell este mult mai puternic și că Haskell poate preveni mult mai multe tipuri de erori de compilare.


Go și Haskell sunt limbi atât de diferite încât gruparea lor într-o singură clasă de „limbi statice” poate induce în eroare, chiar dacă termenul este folosit corect. În ceea ce privește avantajele practice de securitate, Go este mai aproape de limbajele dinamice decât de Haskell.


Pe de altă parte, unele limbi dinamice sunt mai sigure decât unele limbi statice. (Python este, în general, considerat a fi mult mai sigur decât C). Când doriți să generalizați despre limbile statice sau dinamice ca grupuri, fiți conștienți de numărul mare de diferențe dintre limbi.

Exemple specifice de diferențe în capacitățile sistemelor de tastare

În sistemele de tastare mai puternice, puteți specifica constrângeri la niveluri mai mici. Iată câteva exemple, dar nu vă opriți asupra lor dacă sintaxa nu este clară.


În Go, puteți spune „funcția de adăugare are două numere întregi” și returnează un număr întreg „:


func add (x int, y int) int (întoarce x + y)

În Haskell, puteți spune „o funcție ia orice un tip numeric și returnează un număr de același tip ":


f :: Num a => a -> a -> a adăuga x y = x + y

În Idris, puteți spune „funcția ia două numere întregi” și returnează un număr întreg, dar primul argument trebuie să fie mai mic decât al doilea argument „:


adăugați: (x: Nat) -> (y: Nat) -> (auto mai mic: LT x y) -> Nat adăugați x y = x + y

Dacă încercați să apelați funcția add 2 1, unde primul argument este mai mare decât al doilea, compilatorul va respinge programul. la momentul compilarii... Este imposibil să scrieți un program în care primul argument este mai mare decât al doilea. O limbă rară are această capacitate. În majoritatea limbilor, această verificare are loc în timpul execuției: am scrie ceva de genul if x> = y: raise SomeError ().


Nu există nici un echivalent Haskell cu exemplul Idris de mai sus, iar Go nu are echivalent nici cu exemplul Haskell, nici cu exemplul Idris. Drept urmare, Idris poate preveni multe erori pe care Haskell nu le poate, iar Haskell poate preveni multe erori pe care Go nu le va observa. În ambele cazuri, sunt necesare caracteristici suplimentare ale sistemului de tastare pentru a face limbajul mai complex.

Sistemele de dactilografiere ale unor limbaje statice

Iată o listă aproximativă a sistemelor de tastare ale unor limbi, în ordinea crescătoare a cardinalității. Această listă vă va oferi o idee generală despre puterea sistemelor; nu trebuie să o tratați ca pe adevărul absolut. Limbile grupate într-un grup pot fi foarte diferite unele de altele. Fiecare sistem de tastare are propriile sale particularități, iar cele mai multe dintre ele sunt foarte complexe.

  • C (1972), Go (2009): Aceste sisteme nu sunt deloc puternice, fără suport de tip generic. Nu există nicio modalitate de a seta tipul MyList să însemne „listă de numere întregi”, „listă de șiruri de caractere”, etc. În schimb, trebuie să faci o „listă de valori nealocate”. Programatorul trebuie să raporteze manual „aceasta este o listă de șiruri” de fiecare dată când un șir este preluat din listă, iar acest lucru poate duce la o eroare de rulare.
  • Java (1995), C # (2000): Ambele limbi acceptă tipuri generice, așa că puteți spune MyList și obțineți o listă de șiruri despre care compilatorul le cunoaște și poate aplica regulile de tip. Elementele din listă vor fi de tip String, compilatorul va aplica regulile la compilare ca de obicei, astfel încât erorile de rulare sunt mai puțin probabile.
  • Haskell (1990), Rust (2010), Swift (2014): Toate aceste limbaje au mai multe caracteristici avansate, inclusiv tipuri generice, tipuri de date algebrice (ADT) și clase de tip sau ceva similar (tipuri de clasă, trăsături și, respectiv, protocoale). Rust și Swift sunt mai populare decât Haskell și sunt promovate de organizații mari (Mozilla și, respectiv, Apple).
  • Agda (2007), Idris (2011): Aceste limbaje acceptă tipuri dependente, permițându-vă să creați tipuri precum „o funcție care ia două numere întregi x și y, unde y este mai mare decât x”. Chiar și constrângerea „y este mai mare decât x” este impusă în timpul compilării. Când ați terminat, y nu va fi niciodată mai mic sau egal cu x, indiferent de ce se întâmplă. Proprietățile foarte subtile, dar importante ale sistemului pot fi verificate static în aceste limbi. Foarte puțini programatori le studiază, dar sunt foarte entuziasmați de aceste limbaje.

Există o mișcare clară către sisteme de tastare mai puternice, mai ales când se judecă după popularitatea limbilor, mai degrabă decât simpla existență a limbilor. O excepție notabilă este Go, ceea ce explică de ce mulți susținători ai limbajului static îl consideră un pas înapoi.


Grupul doi (Java și C#) sunt limbi principale, mature și utilizate pe scară largă.


Grupul trei este pe punctul de a intra în mainstream, cu mult sprijin din partea Mozilla (Rust) și Apple (Swift).


Grupa patru (Idris și Agda) sunt departe de mainstream, dar acest lucru se poate schimba în timp. Limbile grupului trei erau departe de curentul mainstream cu un deceniu în urmă.

Acest articol conține minimul necesar dintre acele lucruri pe care trebuie doar să le știți despre tastare pentru a nu numi scrierea dinamică rău, Lisp un limbaj fără tip ș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 ilustrative.

Recomand să citiți mai întâi versiunea scurtă a articolului și apoi, dacă doriți, versiunea completă.

Versiune scurta

Prin tastare, limbajele de programare sunt de obicei împărțite în două tabere mari - tastate și netipizate (fără tip). Primul include, de exemplu, C, Python, Scala, PHP și Lua, în timp ce cel din urmă include limbajul de asamblare, Forth și Brainfuck.

Deoarece „dactilografia fără tip” este în mod inerent la fel de simplă ca o plută, nu poate fi î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 definit prin 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 descoperite în timpul execuției.

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

  • Tastare puternică / slabă (uneori se spune că este puternic / slab). 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ă poate apărea pierderea preciziei sau ambiguitatea.

    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/argumentele acestora trebuie specificat î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 faptul că toate aceste categorii se suprapun, de exemplu, C are o tastare statică explicită slabă, iar Python are o tastare implicită dinamică puternică.

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

Versiune detaliată

Dacă versiunea scurtă nu ți s-a părut suficientă, 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 poate prea lungă pentru ca toată lumea să o citească fără a se eforta.

Tastare fără tip

În limbajele de programare fără tip, toate entitățile sunt considerate simple secvențe de biți de diferite lungimi.

Tastarea fără tip 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țiuni 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ă că acesta sau acela cod este.
dezavantaje
  • Complexitate. Este adesea necesar să se reprezinte valori complexe, cum ar fi liste, șiruri de caractere sau structuri. Acest lucru poate fi incomod.
  • Lipsa controalelor. Orice acțiune lipsită de sens, cum ar fi scăderea unui indicator la o matrice dintr-un caracter, va fi considerată complet normală, care 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, există. De exemplu, în limbajul de asamblare (pentru arhitectura x86 / x86-64, nu cunosc alții), nu puteți asambla un program dacă încercați să încărcați date din registrul rax (64 biți) în registrul cx (16 biți) .

mov cx, eax; eroare de timp de asamblare

Deci, se dovedește că încă mai există tastare în asamblator? Cred că aceste verificări nu sunt suficiente. Și părerea ta, desigur, depinde doar de tine.

Tastare statică versus dinamică

Principalul lucru care diferențiază tastarea statică de tastarea dinamică este că toate verificările de tip se fac la compilare, nu la rulare.

Unii oameni pot crede că tastarea statică este prea limitată (de fapt, este, dar a scăpat de ea cu mult timp în urmă cu ajutorul unor tehnici). Pentru unii, limbile tastate dinamic sunt un joc cu foc, dar care sunt caracteristicile care le fac să iasă în evidență? Ambele specii au șanse de existență? Dacă nu, de ce există multe limbi scrise atât static, cât și dinamic?

Să ne dăm seama.

Beneficiile tastării statice
  • Verificările de tip au loc o singură dată, în timpul compilării. Aceasta î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ă realizăm o conversie).
  • Viteza de executie. Din punctul anterior, este 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 - o grămadă de toate și de toate (o astfel de nevoie apare rar, dar atunci când apare tastarea dinamică, va ajuta).
  • Comoditatea descrierii algoritmilor generalizați (de exemplu, sortarea unui tablou, 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șurință de învățare - Limbile tastate dinamic sunt de obicei foarte bune pentru a începe programarea.

Programare generalizată

Bine, cel mai important argument pentru tastarea dinamică este comoditatea descrierii algoritmilor generici. Să ne imaginăm o problemă - avem nevoie de o funcție pentru a căuta în mai multe matrice (sau liste) - o matrice de numere întregi, o matrice de numere reale ș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ă.

Algoritmul de căutare pe care îl voi lua este unul dintre cele mai simple - forța brută. Funcția va primi elementul necesar, 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 cel puțin numere, chiar și liste, deși nu există alte matrice. Foarte bine. Să mergem mai departe și să rezolvăm 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 este similară individual cu versiunea Python, dar de ce sunt trei? A eșuat cu adevărat 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ă și limbajul C ++ o suportă destul de bine. Să aruncăm o privire la noua versiune:

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

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

BINE! Nu pare mult mai complicat decât versiunea Python și nu trebuie să scrie mult. În plus, am primit o implementare pentru toate matricele, nu doar pentru cele 3 necesare pentru a rezolva problema!

Această versiune pare să fie exact ceea ce aveți 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 limbajul 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 macro-urile.

Dinamica statică

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?, pe baza căruia pot fi implementate simulări de tastare dinamică.
  • Haskell - tastarea dinamică este asigurată de modulul Data.Dynamic.
  • Delphi - prin tipul special Variant.

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 entităților 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ă, fac tot posibilul ca programatorul să amestece diferite tipuri într-o singură expresie, iar compilatorul însuși va aduce totul la un singur tip. Ele mai sunt denumite și „limbi scrise liber”. Termenul englezesc pentru acest lucru 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, 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 chiar mai rău, scade altul dintr-o matrice, la ce ne ajută că toate „verificările de tip” sunt în momentul compilării? Așa este - tastarea statică slabă este chiar mai rea decât tastarea dinamică puternică! (Pai asta e parerea mea)

Deci tastarea slabă nu are deloc avantaje? Poate arăta așa, dar deși sunt un susținător ferm 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 de un comportament incorect.
  • Viteză - în loc de conversii ascunse, care pot fi destul de costisitoare, cu tastare puternică, trebuie să le scrieți în mod explicit, ceea ce face ca programatorul să știe că această bucată de cod poate fi lentă.
  • Înțelegând activitatea programului - 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 scrieți conversii manual, știți exact la ce faceți conversie și ce. De asemenea, veți înțelege întotdeauna că astfel de transformări pot duce la pierderea preciziei și la rezultate incorecte.
Beneficiile tastării slabe
  • Ușurința utilizării expresiilor mixte (de exemplu, din numere întregi și numere reale).
  • Abstracție de la tastare și concentrare pe sarcină.
  • Concizia înregistrării.

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

Se pare că sunt chiar două.

Conversie de tip implicită, în situații clare și fără pierderi de date

Uh... Un punct destul de lung. Permiteți-mi să o prescurt în continuare la „conversie implicită delimitată” Deci, care este sensul unei situații clare și al pierderii de date?

O situație neechivocă este o transformare sau operație în care entitatea este imediat înțeleasă. De exemplu, adăugarea a două numere este o situație clară. Și 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 că numărul va fi convertit într-un șir, 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? Înlăturarea părții fracționale?).

Transformările ambigue și transformările cu pierderi sunt foarte, foarte rele. Nu există nimic mai rău decât asta în programare.

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

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

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

O voi explica folosind minunatul limbaj Haskell ca exemplu.

Constructorii polimorfi sunt rezultatul observației că conversiile implicite sigure sunt cel mai adesea necesare atunci când se utilizează literali numerici.

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

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

Ca rezultat, atunci când scriem o funcție simplă sum xy 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 reale, sumă pentru numere raționale, sumă pentru numere complexe , și chiar suma pentru toate acele tipuri numerice pe care le-ați definit.

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

Astfel, putem spune că cea mai bună soluție ar fi să echilibrezi la limita, între tastarea puternică și slabă. Dar până acum nicio limbă nu are un echilibru perfect, așa că tind să mă înclin mai mult spre limbile puternic tastate (cum ar fi Haskell, Java, C #, Python) decât spre cele slab tastate (cum ar fi C, JavaScript, Lua, PHP) .

Tastarea explicită versus implicită

Un limbaj tipizat explicit presupune că programatorul trebuie 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 tastarea implicită.

La început, puteți decide că tastarea implicită este echivalentă cu dinamică și explicită - cu statică, dar mai târziu vom vedea 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
  • Prezența unei semnături pentru fiecare funcție (de exemplu int add (int, int)) vă permite să determinați cu ușurință ce face funcția.
  • Programatorul notează imediat ce tip de valoare poate fi stocat într-o anumită variabilă, ceea ce elimină nevoia de a reține acest lucru.
Beneficiile tastării implicite
  • Scurtarea pentru def add (x, y) este în mod clar mai scurtă decât int add (int x, int y).
  • Reziliență la schimbare. De exemplu, dacă o variabilă temporară dintr-o funcție a fost de același tip cu argumentul de intrare, atunci într-un limbaj tatat explicit, atunci când tipul argumentului de intrare este schimbat, va trebui să se schimbe și tipul variabilei temporare.

Bine, puteți vedea că ambele abordări au argumente pro și contra (cine se aștepta la altceva?), Așa că haideți să căutăm modalități de a le combina pe cele două!

Tastare explicită la alegere

Există limbaje cu tastare implicită în mod implicit și posibilitatea de a specifica tipul de valori dacă este necesar. Traducatorul va deduce automat tipul real de expresie. Una dintre aceste limbi este Haskell, permiteți-mi să vă dau un exemplu simplu pentru claritate:

Fără indicație explicită de tip adăugare (x, y) = x + y - Indicație explicită de tip adăugare :: (Integer, Integer) -> Integer add (x, y) = x + y

Notă: Am folosit în mod intenționat o funcție fără curs ș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 a vrut să arate ideea, fără a explica sintaxa lui Haskell „a.

HM. După cum putem vedea, este foarte frumos și scurt. Înregistrarea funcției are doar 18 caractere pe o linie, inclusiv spațiile!

Cu toate acestea, inferența de tip automat este un lucru complicat și chiar și într-un limbaj cool precum Haskell, uneori eșuează. (de exemplu, se poate cita restricția monomorfismului)

Există limbi care sunt tastate explicit implicit și implicit tastate de necesitate? Con
pentru totdeauna.

Tastare implicită opțională

Noul standard al limbajului C++ numit C++ 11 (numit anterior C++ 0x) a introdus cuvântul cheie auto, care permite compilatorului să deducă tipul în funcție de context:

Să comparăm: // Specificarea manuală a tipului nesemnat int a = 5; unsigned int b = a + 3; // Inferența automată a tipului nesemnat 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 este să rețineți că înregistrarea este mult redusă datorită ieșirii automate):

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

Wow! Aceasta este abrevierea. Bine, dar poți face 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:

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

Poate părea că această notație este foarte bună, dar atunci când este combinată cu programarea generică (șabloane / generice), tastarea implicită sau inferența de tip automat face minuni.

Unele limbaje de programare conform acestei clasificări

Voi oferi o scurtă listă 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 / Implicit Java - Static / Puternic / Explicit PHP - Dinamic / Slab / Implicit C - Static / Slab / Explicit C ++ - Static / Semi- Perl puternic / explicit - Dinamic / Slab / Implicit Obiectiv-C - Static / Slab / Explicit C # - Static / Puternic / Explicit Haskell - Static / Puternic / Implicit Common Lisp - Dinamic / Puternic / Implicit

Poate că m-am înșelat undeva, mai ales cu CL, PHP și Obj-C, dacă aveți o altă părere despre un limbaj - scrieți în comentarii.

Tastare - atribuirea unui tip entităților informaționale.

Cele mai comune tipuri de date primitive sunt:

  • Numeric
  • Caracter
  • Logic

Principalele funcții ale sistemului de tip de date:

  • Securitate
    Fiecare operație este verificată pentru argumente de exact tipurile pentru care este destinată;
  • Optimizare
    În funcție de tip, se selectează o metodă de stocare eficientă și algoritmi pentru prelucrarea acesteia;
  • Documentație
    Se subliniază intențiile programatorului;
  • Abstracția
    Utilizarea tipurilor de date de nivel înalt permite programatorului să se gândească la valori ca entități de nivel înalt, mai degrabă decât o colecție de biți.

Clasificare

Există multe clasificări ale limbajelor de programare de tastare, dar principalele sunt doar 3:

Tastare statică/dinamică

Static - verificarea alocării și a consistenței tipului se face în timpul compilării. Tipurile de date sunt asociate cu variabile, nu cu valori specifice. Tastarea statică vă permite să găsiți erori de tastare făcute în ramuri rar utilizate ale logicii programului la momentul compilării.

Tastarea dinamică este opusul tastării statice. În tastarea dinamică, toate tipurile sunt descoperite în timpul execuției.

Tastarea dinamică vă permite să creați un software mai flexibil, deși cu prețul unei probabilități mai mari de erori de tastare. Testarea unitară este de o importanță deosebită atunci când se dezvoltă software în limbaje de programare cu tastare dinamică, deoarece este singura modalitate de a găsi erori de tastare în ramurile rar utilizate ale logicii programului.

Tastare dinamică

Var luckyNumber = 777; var siteName = "Tyapk"; // ne referim la un număr, scriem un șir var wrongNumber = "999";

Tastare statică

Să luckyNumber: număr = 777; let siteName: string = "Tyapk"; // va genera o eroare let wrongNumber: number = "999";

  • Static: Java, C #, TypeScript.
  • Dinamic: Python, Ruby, JavaScript.

Tastare explicită / implicită.

Limbile scrise explicit diferă prin aceea că tipul de noi variabile/funcții/argumentele acestora trebuie specificat în mod explicit. În consecință, limbile cu tastare implicită transferă această sarcină la compilator / interpret. Tastarea explicită este opusul tastării implicite.

Tastarea explicită necesită o declarație explicită de tip pentru fiecare variabilă utilizată. Acest tip de tastare este un caz special de tastare statică, deoarece tipul fiecărei variabile este determinat în momentul compilării.

Tastare implicită

Fie stringVar = "777" + 99; // obțineți „77799”

Tastare explicită (limbaj fictiv asemănător JS)

Fie wrongStringVar = "777" + 99; // aruncă o eroare let stringVar = "777" + String (99); // obțineți „77799”

Tastare puternică / liberă

Se mai numește și tastare puternică / slabă. Cu tastare puternică, tipurile sunt atribuite „o dată pentru totdeauna”, cu tastare laxă, acestea se pot schimba în timpul execuției programului.

Limbile cu tastare puternică nu permit modificări ale tipului de date al unei variabile și sunt permise numai conversiile explicite ale tipurilor de date. Tastarea puternică se distinge prin faptul că limbajul nu permite amestecarea diferitelor tipuri în expresii și nu realizează conversii implicite automate, de exemplu, nu puteți scădea un număr dintr-un șir. Limbile scrise slab realizează automat multe conversii implicite, chiar dacă poate apărea pierderea preciziei sau ambiguitatea.

Tastare puternică (limbaj fictiv asemănător JS)

Fie wrongNumber = 777; greșitNumăr = greșitNumăr + „99”; // obținem o eroare că un șir este adăugat la variabila numerică wrongNumber let trueNumber = 777 + Number ("99"); // obțineți 876

Tastare liberă (cum este în js)

Fie wrongNumber = 777; greșitNumăr = greșitNumăr + „99”; // am primit șirul „77799”

  • Strict: Java, Python, Haskell, Lisp.
  • Lax: C, JavaScript, Visual Basic, PHP.
Imparte asta