Auto-referința (self reference) - modul unui obiect de a se referi la sine însuși - pare să fie tratată foarte diferit în limbajele orientate pe obiecte. În timp ce în limbajele statice asemănătoare cu Java, cuvântul this este magic, direct și în mare parte nefolosit, în Javascript, magia poate să deruteze. Și totuși, în Python, unii l-ar putea descrie drept "explicit în mod redundant".
În alt limbaj dinamic, Ruby, self acționează foarte similar cu this din Java, dar metodele nu sunt obiecte nici acolo. Este această varietate de comportamente datorată deciziilor întâmplătoare de design, sau există vreun tipar comun în toate aceste limbaje OO?
Aș vrea să vă propun un experiment de gândire. Vom încerca să creăm un limbaj OO care să aibă anumite trăsături confortabile din limbajele de mai sus. În timp ce creăm acest limbaj, ne vom întâlni cu anumite situații care să sperăm că vor clarifica de ce auto-referința (self reference) este tratată atât de diferit. De asemenea, vom vedea cum auto-referința se potrivește cu alte funcționalități ale limbajelor noastre preferate.
Limbajul pe care îl creăm va fi un limbaj scris static, dar vom permite înlocuirea dinamică a tuturor membrilor dacă aceștia mențin contractul static. Cu alte cuvinte, întocmai cum am putea schimba o valoare a atributului unui întreg de la 4 la 5, noi putem redefini ceea ce fac metodele, în timpul de rulare, atâta timp cât semnătura (numele, tipurile, tipul returnat etc. al parametrilor formali) rămâne aceeași. Aceasta ne oferă toată siguranța de compilare de care avem nevoie, dar ne și permite puțină magie neagră dacă ne dorim cu adevărat. Vom avea de asemenea funcții, deoarece uneori ne plac astea, iar acestea vor fi obiecte. Pentru a condimenta puțin, vom face metodele rezidente (citizens) de primă clasă și obiecte de asemenea, deoarece ne-ar plăcea să le trecem drept argumente (tipar observator cu numai un callback).
Nu vor exista modificatori de vizibilitate, aceștia sunt irelevanți pentru acest argument: totul este public.
Dacă se ivește vreo confuzie legată de cuvântul "obiect", să îl păstrăm simplu: obiectele sunt instanțe ale claselor. Mai în detaliu, obiectele au o relație specială cu o altă entitate, ceea ce descrie (cel puțin în parte) comportamentul și structura obiectului dat.
Dați-mi voie să fac un rezumat:
scriere statică
obiectele sunt instanțe ale claselor
casele au metode
clasele, metodele și funcțiile sunt obiecte
Haideți să vedem, nu? Iată prima noastră clasă, denumită în mod adecvat First.
class First {
int x
Constructor(int x){
this.x = x
}
int double_the_fun(){
return 2 * x
}
}
Asta ar trebui să pară cunoscut celor care lucrează în limbaje inspirate de Java. Am definit o clasă cu un atribut de date care va fi o valoare a unui întreg și stabilim valoarea sa în constructor. Avem de asemenea o metodă care returnează valoarea acestui atribut multiplicată cu 2. Deja în acest exemplu simplu, aveam nevoie de o modalitate de a face referință la atributul obiectului (cel puțin) din interiorul constructorului său. De ce a fost necesar acest lucru? Pentru că noi umbrim atributul x cu parametrul formal al aceluiași nume. De aceea, aici, auto-referința este utilizată pentru a accesa câmpul (scope) care este bagajul de atribute ale obiectului. Am promis funcții libere, deci, iată una:
int triple_the_fun(int x){
return 3 * x
}
Am promis, de asemenea, și metode care pot fi înlocuite dinamic, deci, dacă am uita pur și simplu pentru o clipă tot ce ne-a învățat programarea orientată pe obiect (OO), am face asta:
First.double_the_fun = triple_the_fun
Noi am tratat metoda double_the_fun exact precum am trata un atribut de date simplu (integru, de exemplu). Un pic ciudat pentru unii dintre noi, știu.
Am putea apela conceptual această nouă metodă astfel:
var my_instance = new First(3)
my_instance.double_the_fun(3)
# ar returna 9
Funcționează, nu-i așa? Nimic nu pare greșit, cu excepția faptului că poate ne-ar fi plăcut ca noua metodă , double_the_fun să utilizeze valoarea lui x care este deja stocată pe obiect. Ne-ar plăcea ca my_instance.double_the_fun()să returneze 9 fără a reda un întreg, sau a face lucruri urâte precum my_instance.double_the_fun(my_instance.x).
Următoarea mișcare pe care o voi încerca, va face iadul să se dezlănțuie, deci este importantă: Ce ar fi să punem cuvântul cheie this în interiorul definiției funcției originale triple_the_fun?
Ei bine, am promis scriere statică cel puțin pentru ceea ce este vizibil înainte de perioada de rulare.
int triple_the_fun(){
return 3 * this.x
}
Deci ce e în neregulă cu această funcție acum? Se pare că în orice limbaj, și în special în cele care se scriu în mod static, ca și al nostru, așa ceva este urât, dacă nu chiar imposibil de făcut, deoarece am promis că funcțiile sunt de asemenea și obiecte. Fiind obiecte, înseamnă că ele au de asemenea și o clasă, să spunem clasa Function, care are la rândul său atribute și metode. Deci, cuvântul cheie this se referă la membrii definiți în clasa Function sau în clasa First?
Haideți să încercăm să clarificăm această ambiguitate și să propunem niște soluții alternative:
Să acceptăm că this se referă la 2 lanțuri moștenite. Imaginați-vă că am subclasat ambele clase: Function și de asemenea First (lanțul 1: subclasele Function ->… -> Function și lanțul 2: subclasele First ->… -> First). Vom obține atributul this.x de oriunde apare (acordând prioritate unuia dintre aceste câmpuri). Problema aici este că, dacă atributul se găsește în ambele câmpuri, tocmai am pierdut o modalitate de a obține pe unul dintre ele. Sintaxa explicită asemănătoare celei din Java, First.this.x VS Function.this.x nu ne poate ajuta prea mult, pentru că atunci când funcția nu este stabilită drept metodă, tot codul din ea trebuie să fie valid înainte de momentul de rulare, de asemenea (iar First.this.x nu ar fi). Mai mult, tipul atributului x s-ar putea să difere de definiția sa din clasa Function versus clasa First, deci codul fie s-ar strica atunci când utilizăm obiectul drept o funcție sau drept o metodă. Acceptarea a 2 lanțuri moștenire ar putea să funcționeze mai bine în limbajele dinamice, dar și aici ambiguitatea este mai dificil de rezolvat. Nici un limbaj pe care îl cunosc nu face asta.
Să acceptăm că această situație se întâmplă și să introducem 2 cuvinte cheie magice. Haideți să luăm cuvintele cheie caller și this. Cuvântul cheie this ar indica clasa Function, iar caller ar indica clasa First. Caller ar putea fi nul, deoarece funcțiile ar putea deveni limitate (bound), iar metodele nelimitate (unbound) pe perioada de rulare. Asta nu este prea frumos să facem în limbajele scrise static, deoarece caller ar putea lua orice tip și noi ar trebui să verificăm tipul său și să îl atribuim înainte de utilizare. Acesta este un lucru ce poate fi făcut, dar nu știu nici un limbaj care să facă așa ceva.
Pur și simplu alegeți unul dintre acele câmpuri (scopes) și uitați de celălalt. Se pare că Javascript face ceva foarte similar cu asta. După cum probabil mulți știți, în Javascript, this se referă întotdeauna la câmpul obiectului său caller (sau obiectul window global), nu la acela al obiectului original, unde funcția a fost definită drept un atribut. Delegații lui C# și Procs și lambdas din Ruby merg în direcția opusă. Rămân întotdeauna legate de clasa în care au fost definite (de asemenea, instanțele delegate nu sunt obiecte reale, deci this nu ar putea niciodată să se refere la vreo metodă delegată sau vreun atribut delegat. Ruby procs și lambdas sunt obiecte reale, dar acestea sunt create într-un asemenea mod în care să nu se refere niciodată la ele însele cu magicul self).
Renunțați la magia lui this. Să îl facem doar un parametru obișnuit și, de asemenea, chemați-l ori de câte ori doriți. După cum știu unii, Python face asta. Această soluție nu convine unora, pentru că fiecare metodă și funcție care se dorește a fi utilizată drept metodă trebuie să accepte un argument adițional.
Sper că acum este puțin mai clar cum toate aceste limbaje au încercat să rezolve aceeași problemă, dar pur și simplu au venit cu soluții diferite.
Înainte de a încheia, aș vrea să mai aduc în discuție o altă problemă interesantă: definițiile în serie. Ce se întâmplă când o clasă este definită în interiorul unei metode, care este definită în interiorul unei funcții, care este definită în interiorul unei metode și așa mai departe? Magia auto-referinței începe să dispară în situații ca aceasta, deoarece am avea nevoie de o sintaxă suplimentară, mai complexă, pentru a accesa câmpurile împrejmuitoare (enclosing scopes). Java are sintaxa ClassName.this.attribute, iar alte limbaje s-ar putea să aibă ceva asemănător, dar aceasta nu rezolvă problema decât parțial. În cele din urmă, totuși, complexitatea rețelei câmpurilor (scope nesting) face dificilă utilizarea unei auto-referințe magice. În acest punct, soluțiile pentru încercarea de a accesa obiectele înconjurătoare (precum bine-cunoscutul var that = this; din Javascript) ajung să semene cu verbozitatea foarte explicită a lui Python. Orice referire la obiectele înconjurătoare va fi tratată explicit în situații ca aceasta. Poate că nu este o coincidență că Python este un limbaj atât de reflexiv. Plătind prețul verbozității, el modelează mult mai multe lucruri drept obiecte, fără a-și face deloc griji în legătură cu auto-referința.
În concluzie, am dorit să arăt că diversitatea implementărilor nu este o alegere pur întâmplătoare sau exotică, ci a fost influențată de o problemă autentică. Aceasta limitează decizia în legătură cu ce poate fi modelat drept un obiect într-un limbaj OO, și cum facem referire la câmpuri (scopes). De asemenea, nu vreau să susțin nicio modalitate anume a vreunui limbaj de a implementa programarea orientată pe obiecte (OOP), deoarece sunt sigur că limitările problemei vor face ca toate implementările să aibă argumente pro și contra.
de Ovidiu Mățan
de Ovidiu Mățan
de Ovidiu Mățan
de Ovidiu Mățan