Kaikki mitä sinun tarvitsee tietää vertailun tai arvon perusteella

Ohjelmistosuunnittelussa on melko vähän väärinkäsityksiä ja väärinkäytettyjä termejä. Vertailun ja arvon perusteella on ehdottomasti yksi niistä.

Muistan takaisin sinä päivänä, kun luin aihetta ja jokainen lähde, jonka kävin läpi, näytti olevan ristiriidassa edellisen kanssa. Kesti jonkin aikaa, jotta saatiin siitä vankka käsitys. Minulla ei ollut valintaa, koska se on keskeinen aihe, jos olet ohjelmistosuunnittelija.

Jouduin ilkeäyn vikaan muutama viikko sitten ja päätin kirjoittaa artikkelin, jotta muilla ihmisillä olisi helpompi ajatella tätä koko asiaa.

Koodin Rubyssa päivittäin. Käytän JavaScriptiä melko usein, joten olen valinnut nämä kaksi kieltä tähän esitykseen.

Ymmärrämme kaikki käsitteet, vaikka käytämme myös joitain Go- ja Perl-esimerkkejä.

Koko aiheen ymmärtämiseksi sinun on ymmärrettävä 3 eri asiaa:

  • Kuinka taustalla olevat tietorakenteet toteutetaan kielellä (esineet, alkeelliset tyypit, muutettavuus).
  • Kuinka muuttuva tehtävä / kopiointi / uudelleenmääritys / vertailu toimii
  • Kuinka muuttujat siirretään funktioihin

Perustietotyypit

Rubyssa ei ole primitiivisiä tyyppejä ja kaikki on esine, mukaan lukien kokonaisluvut ja booleanit.

Ja kyllä, Rubyssa on TrueClass.

true.is_a? (TrueClass) => totta
3.is_a? (Kokonaisluku) => totta
totta.is_a? (esine) => totta
3.is_a? (Esine) => totta
TrueClass.is_a? (Object) => totta
Integer.is_a? (Object) => totta

Nämä esineet voivat olla joko muutettavissa tai muuttumattomia.

Muuttamaton tarkoittaa, että objektia ei voi muuttaa, kun se on luotu. Annetulle arvolle on vain yksi esimerkki yhdellä object_id ja se pysyy samana riippumatta siitä, mitä teet.

Oletuksena on Rubyn muuttumattomat objektityypit: Boolen, Numeerinen, nolla ja Symboli.

MRI: ssä objektin object_id on sama kuin arvo, joka edustaa objektia C-tasolla. Useimpien objektityyppien tapauksessa tämä ARVO on osoitin muistiin siihen kohtaan, johon todelliset objektitiedot on tallennettu.

Tästä eteenpäin käytämme object_id- ja muistiosoitteita vaihtokelpoisesti.

Suoritetaan jokin Ruby-koodi MRI: ssä muuttumattomalle symbolille ja muuttuvalle merkkijonolle:

: symbol.object_id => 808668
: symbol.object_id => 808668
'string'.object_id => 70137215233780
'string'.object_id => 70137215215120

Kuten näet, kun symboliversio pitää saman objektin_id samalla arvolla, merkkijonoarvot kuuluvat eri muistiosoitteisiin.

Toisin kuin Ruby, JavaScriptillä on primitiivisiä tyyppejä.

Ne ovat - Boolean, nolla, määrittelemätön, merkkijono ja numero.

Loput tietotyypit ovat objektien (taulukko, toiminto ja objekti) alaisuudessa. Tässä ei ole mitään hienoa, se on paljon suoraviivaisempi kuin Ruby.

[] taulukon esimerkki => totta
[] Objektin esimerkki => totta
3 esiintymäobjektia => väärä

Muuttuva tehtävä, kopiointi, uudelleenmääritys ja vertailu

Rubyssa jokainen muuttuja on vain viittaus esineeseen (koska kaikki on esine).

a = 'merkkijono'
b = a
# Jos määrität saman arvon uudelleen
a = 'merkkijono'
asettaa b => 'merkkijono'
asettaa == b => tosi # arvot ovat samat
asettaa a.object_id == b.object_id => false # memory adr-s. erota
# Jos määrität jonkin toisen arvon
a = 'uusi merkkijono'
asettaa => 'uusi merkkijono'
asettaa b => 'merkkijono'
asettaa == b => vääriä # arvot ovat erilaisia
asettaa a.object_id == b.object_id => false # memory adr-s. eroavat myös

Kun määrität muuttujan, se on viittaus objektiin, ei itse esineeseen. Kun kopioit objektia b = a, molemmat muuttujat osoittavat samaan osoitteeseen.

Tätä käyttäytymistä kutsutaan kopioksi viitearvon perusteella.

Tarkkaan ottaen Rubyn ja JavaScriptin avulla kaikki on kopioitu arvon perusteella.

Objektien suhteen arvot kuitenkin sattuvat olemaan näiden objektien muistiosoitteet. Tämän ansiosta voimme muokata muissa osoitteissa olevia arvoja. Tätä kutsutaan jälleen kopioksi viitearvon mukaan, mutta suurin osa ihmisistä viittaa tähän kopioon viittauksena.

Se olisi kopio viitteestä, jos b: n osoittamisen jälkeen uudelle merkkijonolle osoittaisi myös samaan osoitteeseen ja sillä olisi sama 'uusi merkkijono' -arvo.

Kun julistat b = a, a ja b osoittavat samaan muistiosoitteeseenKun olet määrittänyt uudelleen (a = 'merkkijono'), a ja b osoittavat eri muistiosoitteisiin

Sama muuttumattoman tyypin kuten Integer kanssa:

a = 1
b = a
a = 1
asettaa b => 1
asettaa == b => true # -vertailun arvon perusteella
laittaa a.object_id == b.object_id => true # vertailun muistilla.

Kun määrität a samalle kokonaisluvulle, muistiosoite pysyy samana, koska annetulla kokonaisluvulla on aina sama object_id.

Kuten näet vertaamalla objektia toiseen, sitä verrataan arvoon. Jos haluat tarkistaa, ovatko kyseessä sama objekti, sinun on käytettävä object_id.

Katsotaanpa JavaScript-versio:

var a = 'merkkijono';
var b = a;
a = 'merkkijono'; # a on osoitettu uudelleen samaan arvoon
console.log (a); => 'merkkijono'
console.log (b); => 'merkkijono'
konsoli.logi (a === b); => tosi // vertailun arvo
var a = [];
var b = a;
konsoli.logi (a === b); => totta
a = [];
console.log (a); => []
console.log (b); => []
konsoli.logi (a === b); => väärä // vertailu muistiosoitteen perusteella

Paitsi vertailu - JavaScript käyttää arvoa primitiivityypeille ja viittausta objekteille. Käyttäytyminen näyttää olevan samalla tavalla kuin Rubyssa.

No, ei aivan.

JavaScriptin primitiivisiä arvoja ei jaeta useiden muuttujien välillä. Vaikka asettaisit muuttujat yhtä suuret toisiinsa. Jokaisen primitiivistä arvoa edustavan muuttujan on taatusti kuuluvan yksilölliseen muistipaikkaan.

Tämä tarkoittaa, että mikään muuttujista ei koskaan osoita samaan muistiosoitteeseen. On myös tärkeää, että arvo itse tallennetaan fyysiseen muistipaikkaan.

Esimerkissämme, kun julistamme b = a, b osoittaa toiseen muistiosoitteeseen, jolla on sama 'merkkijono' arvo heti. Joten sinun ei tarvitse määrittää uudelleen osoittaaksesi toiseen muistiosoitteeseen.

Tätä kutsutaan kopioiduksi arvon perusteella, koska sinulla ei ole pääsyä vain muistiosoitteeseen.

Kun julistat a = b, se annetaan arvolla, joten a ja b osoittavat eri muistiosoitteille

Katsotaanpa parempi esimerkki, missä kaikki tämä on tärkeää.

Jos muokkaamme muistiosoitteessa olevaa arvoa Ruby-merkinnässä, kaikilla osoitteeseen osoittavilla viitteillä on sama päivitetty arvo:

a = 'x'
b = a
a.concat (y)
asettaa => 'xy'
asettaa b => 'xy'
b.concat ( 'z')
asettaa => 'xyz'
asettaa b => 'xyz'
a = 'z'
asettaa => 'z'
asettaa b => 'xyz'
a [0] = 'y'
asettaa => 'y'
asettaa b => 'xyz'

Saatat ajatella JavaScriptissä vain a: n arvo muuttuisi, mutta ei. Et voi edes muuttaa alkuperäistä arvoa, koska sinulla ei ole suoraa pääsyä muistiosoitteeseen.

Voisit sanoa, että annoit x: n, mutta se annettiin arvon mukaan, joten muistin osoitteessa on arvo x, etkä voi muuttaa sitä, koska sinulla ei ole viitettä siihen.

var a = 'x';
var b = a;
a.concat ( 'Y');
console.log (a); => 'x'
console.log (b); => 'x'
a [0] = 'z';
console.log (a); => 'x';

JavaScript-objektien käyttäytyminen ja toteutus ovat samat kuin Rubyn muutettavissa olevilla objekteilla. Molemmat kopiot ovat viitearvoja.

JavaScript-primitiivityypit kopioidaan arvon mukaan. Käyttäytyminen on sama kuin Rubyn muuttumattomilla objekteilla, jotka kopioidaan viitearvon avulla.

Häh?

Jälleen kerran, kun kopioit jotain arvon perusteella, se tarkoittaa, että et voi muuttaa (mutatoida) alkuperäistä arvoa, koska muistiosoitteeseen ei viitata. Kirjoittamiskoodin kannalta tämä on sama asia kuin muuttumattomien olioiden muodostaminen, joita et voi muuttaa.

Jos vertailet Rubyn ja JavaScriptiä, ainoa tietotyyppi, joka "käyttäytyy" eri tavalla oletuksena, on merkkijono (siksi käytimme merkkijonoa yllä olevissa esimerkeissä).

Rubyssa se on muuttuva objekti ja se kopioidaan / siirretään viitearvon avulla, kun taas JavaScriptissä se on alkeellista tyyppiä ja kopioi / ohittaa arvon.

Kun haluat kloonata (ei kopioida) objektia, se on tehtävä selvästi molemmilla kielillä, jotta voit varmistaa, että alkuperäistä objektia ei muokata:

a = {'nimi': 'Kate'}
b = a.klooni
b ['nimi'] = 'Anna'
asettaa => {: name => "Kate"}
var a = {'nimi': 'Kate'};
var b = {... a}; // uudella ES6-syntaksilla
b ['nimi'] = 'Anna';
console.log (a); => {nimi: "Kate"}

On tärkeää muistaa tämä, muuten joudut ilkeisiin virheisiin, kun käytät koodiasi useammin kuin kerran. Hyvä esimerkki olisi rekursiivinen funktio, jossa käytät objektia argumenttina.

Toinen on React (JavaScript-käyttöliittymäkehys), jossa sinun on aina läpäistävä uusi objekti päivittämistä varten, koska vertailu toimii objektitunnuksen perusteella.

Tämä on nopeampaa, koska sinun ei tarvitse käydä objektin läpi riviltä nähdäksesi onko sitä muutettu.

Kuinka muuttujat siirretään funktioihin

Muuttujien siirtäminen toimintoihin toimii samalla tavalla kuin kopiointi samoille tietotyypeille useimmissa kielissä.

JavaScript-ohjelmassa primitiiviset tyypit kopioidaan ja välitetään arvolla ja objektit kopioidaan ja välitetään vertailuarvon avulla.

Mielestäni tämä on syy siihen, miksi ihmiset puhuvat vain ohittamisesta tai viittaamisesta eivätkä koskaan näytä mainitsevan kopiointia. Luulen, että heidän oletetaan, että kopiointi toimii samalla tavalla.

a = 'b'
def lähtö (merkkijono) # ohitettu viitearvon avulla
  string = 'c' # määritetty uudelleen, joten ei viittausta alkuperäiseen
  laittaa merkkijono
pää
lähtö (a) => 'c'
asettaa => 'b'
def output2 (merkkijono) # ohitettu viitearvon avulla
  string.concat ('c') # muutamme osoitteessa olevan arvon
  laittaa merkkijono
pää
lähtö (a) => 'bc'
asettaa => 'bc'

Nyt JavaScript:

var a = 'b';
toiminnon lähtö (merkkijono) {// arvon ohi
  merkkijono = 'c'; // määritetty toiselle arvolle
  console.log (string);
}
ulostulo (a); => 'c'
console.log (a); => 'b'
toiminnon output2 (merkkijono) {// arvon ohi
  string.concat ( 'c'); // emme voi muokata sitä ilman viittausta
  console.log (string);
}
output2 (a); => 'b'
console.log (a); => 'b'

Jos välität objektin (ei primitiivisen tyyppisen kuin me) JavaScript-toiminnossa, se toimii samalla tavalla kuin Ruby-esimerkki.

Muut kielet

Olemme jo nähneet, kuinka kopiointi / ohjaus arvolla ja kopiointi / ohjaus vertailuarvon mukaan toimii. Nyt näemme, mistä viite kulkee, ja löydämme myös, kuinka voimme muuttaa esineitä, jos ohitamme arvon mukaan.

Kun etsin ohikieliä, en löytänyt liian monta, ja päädyin valitsemaan Perl. Katsotaanpa kuinka kopiointi toimii Perlissa:

minun $ x = 'merkkijono';
minun $ y = $ x;
$ x = 'uusi merkkijono';
tulosta "$ x"; => 'uusi merkkijono'
tulosta "$ y"; => 'merkkijono'
minun $ a = {data => "merkkijono"};
minun $ b = $ a;
$ a -> {data} = "uusi merkkijono";
tulosta "$ a -> {data} \ n"; => 'uusi merkkijono'
tulosta "$ b -> {data} \ n"; => 'uusi merkkijono'

No tämä näyttää olevan sama kuin Ruby. En ole löytänyt todisteita, mutta sanoisin, että Perl kopioidaan merkkijonon viitearvolla.

Katsotaan nyt, mitä viittauksella tarkoitetaan:

minun $ x = 'merkkijono';
tulosta "$ x"; => 'merkkijono'
sub foo {
  $ _ [0] = 'uusi merkkijono';
  tulosta "$ _ [0]"; => 'uusi merkkijono'
}
foo ($ x);
tulosta "$ x"; => 'uusi merkkijono'

Koska Perliä ohitetaan viittauksella, jos teet uudelleenmäärityksen funktion sisällä, se muuttaa myös muistiosoitteen alkuperäistä arvoa.

Arvokielen ohittamiseksi olen valinnut Go, koska aion syventää Go-tietämystä lähitulevaisuudessa:

paketin pää
tuo "fmt"
func changeAddress (a * int) {
  fmt.Println (a)
  * a = 0 // asettaa muistiosoitteen arvoksi 0
}
func changeValue (int) {
  fmt.Println (a)
  a = 0 // muutamme arvoa funktion sisällä
  fmt.Println (a)
}
func main () {
  a: = 5
  fmt.Println (a)
  fmt.Println (& A)
  muutosarvo (a) // a välitetään arvolla
  fmt.Println (a)
  changeAddress (& a) //: n muistiosoite välitetään arvon perusteella
  fmt.Println (a)
}
Kun käännät ja suoritat koodin, saat seuraavan:
0xc42000e328
5
5
0
5
0xc42000e328
0

Jos haluat muuttaa muistiosoitteen arvoa, sinun on käytettävä osoittimia ja siirrettävä muistiosoitteet ympäri arvoa. Osoitin pitää arvon muistiosoitetta.

& Operaattori luo osoittimen operandilleen ja * -operaattori osoittaa osoittimen perusarvon. Tämä tarkoittaa pohjimmiltaan, että siirrät arvon muistiosoitteen näppäimellä & ja asetat muistiosoitteen arvon *: lla.

johtopäätös

Kuinka arvioida kieltä:

  1. Ymmärtää taustalla olevat tietotyypit kielellä. Lue joitain eritelmiä ja pelaa heidän kanssaan. Se tyypillisesti alkeellisia tyyppejä ja esineitä. Tarkista sitten, ovatko nämä esineet muutettavissa vai muuttumattomia. Jotkut kielet käyttävät erilaisia ​​kopiointi- / välitystaktiikoita erityyppisille tietotyypeille.
  2. Seuraava vaihe on muuttujan määritys, kopiointi, uudelleenmääritys ja vertailu. Tämä on mielestäni tärkein osa. Kun saat tämän, voit selvittää mitä tapahtuu. Se auttaa paljon, jos tarkistat muistiosoitteet pelatessasi.
  3. Muuttujien siirtäminen funktioihin ei yleensä ole erityistä. Se toimii yleensä samalla tavalla kuin kopioiminen useimmilla kielillä. Kun tiedät miten muuttujat kopioidaan ja määritetään uudelleen, tiedät jo, kuinka ne siirretään toimintoihin.

Käytetyt kielet:

  • Mene: Kopioitu ja ohitettu arvon mukaan
  • JavaScript: Primitiivityypit kopioidaan / välitetään arvolla, objektit kopioidaan / välitetään vertailuarvon avulla
  • Ruby: Kopioitu ja ohitettu vertailuarvon + muutettavien / muuttumattomien kohteiden avulla
  • Perl: Kopioidaan referenssiarvon perusteella ja ohitetaan referenssillä

Kun ihmiset sanovat, että ohitetaan viittauksella, he yleensä tarkoittavat ohitettua viitearvon avulla. Viitearvon ohittaminen tarkoittaa, että muuttujat ohitetaan arvolla, mutta nämä arvot ovat viittauksia objekteihin.

Kuten näit, Ruby käyttää vain ohitusarvoa, kun taas JavaScript käyttää sekoitettua strategiaa. Silti käyttäytyminen on sama melkein kaikille tietotyypeille johtuen tietorakenteiden erilaisesta toteutuksesta.

Suurin osa yleisistä kielistä joko kopioidaan ja välitetään arvolla tai kopioidaan ja välitetään vertailuarvon avulla. Viimeisen kerran: Ohitusarvon ohitusarvoa kutsutaan yleensä ohitusviitearvoksi.

Yleensä ohituksen arvo on turvallisempaa, koska et törmää ongelmiin, koska et voi vahingossa muuttaa alkuperäistä arvoa. Kirjoittaminen on myös hitaampaa, koska sinun on käytettävä osoittimia, jos haluat muuttaa objekteja.

Se on sama idea kuin staattisen kirjoittamisen ja dynaamisen kirjoittamisen kanssa - kehityksen nopeus turvallisuuden kustannuksella. Kuten arvasit ohitusarvo on yleensä ominaisuus alemmilla kielillä, kuten C, Java tai Go.

Viite- tai viitearvoa käytetään yleensä korkeamman tason kielillä, kuten JavaScript, Ruby ja Python.

Kun löydät uuden kielen, käy läpi prosessi, kuten täällä, ja ymmärrät sen toiminnan.

Tämä ei ole helppo aihe, enkä ole varma, onko kaikki oikein, mitä täällä kirjoitin. Jos luulet tehneen joitain virheitä tässä artikkelissa, ota meihin yhteyttä kommenteissa.