Prijeđi po vrijednosti. Prosljeđivanje parametara po referenci i po vrijednosti. Zadane postavke

Dakle, neka je faktorijel(n) funkcija za izračunavanje faktorijela broja n. Zatim, s obzirom da "znamo" da je faktorijel 1 1, možemo konstruirati sljedeći lanac:

Faktorijel(4)=Faktorijel(3)*4

Faktorijel(3)=Faktorijel(2)*3

Faktorijel(2)=Faktorijel(1)*2

Ali, da nemamo terminalni uvjet da kada je n=1 faktorska funkcija treba vratiti 1, tada takav teorijski lanac nikada ne bi završio, a to bi mogla biti pogreška Call Stack Overflow - preljev pozivnog stoga. Da bismo razumjeli što je pozivni stog i kako se može prelijevati, pogledajmo rekurzivnu implementaciju naše funkcije:

Faktorijel funkcije (n: Integer): LongInt;

Ako je n=1 Tada

Faktorijel:=Faktorijel(n-1)*n;

Kraj;

Kao što vidimo, da bi lanac radio ispravno, prije svakog sljedećeg poziva funkcije same sebe, potrebno je negdje spremiti sve lokalne varijable, tako da kada se lanac obrne rezultat bude točan (izračunata vrijednost faktorijela od n-1 pomnoženo s n). U našem slučaju, svaki put kada se faktorijelna funkcija pozove iz same sebe, sve vrijednosti varijable n moraju biti spremljene. Područje u kojem su lokalne varijable funkcije pohranjene kada se rekurzivno poziva naziva se pozivni stog. Naravno, ovaj stog nije beskonačan i može se iscrpiti ako su rekurzivni pozivi neispravno konstruirani. Konačnost ponavljanja našeg primjera zajamčena je činjenicom da kada je n=1 poziv funkcije prestaje.

Prosljeđivanje parametara po vrijednosti i po referenci

Do sada nismo mogli promijeniti vrijednost u potprogramu stvarni parametar(tj. parametar koji je naveden prilikom pozivanja potprograma), au nekim aplikacijskim zadacima to bi bilo zgodno. Sjetimo se procedure Val, koja mijenja vrijednost dva svoja stvarna parametra odjednom: prvi je parametar u koji će biti zapisana konvertirana vrijednost varijable niza, a drugi je parametar Code, gdje se upisuje broj pogrešnih znak se postavlja u slučaju kvara tijekom pretvorbe tipa. Oni. još uvijek postoji mehanizam kojim potprogram može promijeniti stvarne parametre. To je moguće zahvaljujući različitim načinima prosljeđivanja parametara. Pogledajmo pobliže ove metode.

Programiranje u Pascalu

Prosljeđivanje parametara po vrijednosti

U biti, ovako smo proslijedili sve parametre našim rutinama. Mehanizam je sljedeći: kada se specificira stvarni parametar, njegova vrijednost se kopira u memorijsko područje gdje se nalazi potprogram, a zatim, nakon što funkcija ili procedura završi svoj rad, ovo područje se briše. Grubo govoreći, dok potprogram radi, postoje dvije kopije njegovih parametara: jedan u opsegu programa koji poziva, a drugi u opsegu funkcije.

Ovom metodom prosljeđivanja parametara potrebno je više vremena za pozivanje potprograma, jer je osim samog poziva potrebno kopirati sve vrijednosti svih stvarnih parametara. Ako se potprogramu proslijedi velika količina podataka (na primjer, niz s velikim brojem elemenata), vrijeme potrebno za kopiranje podataka u lokalno područje može biti značajno i to se mora uzeti u obzir pri razvoju programa i pronalaženje uskih grla u njihovoj izvedbi.

S ovom metodom prijenosa, stvarni parametri se ne mogu promijeniti potprogramom, budući da će promjene utjecati samo na izolirano lokalno područje, koje će biti otpušteno nakon završetka funkcije ili procedure.

Prosljeđivanje parametara prema referenci

Ovom metodom vrijednosti stvarnih parametara se ne kopiraju u potprogram, već se prenose adrese u memoriji (veze na varijable) gdje se nalaze. U ovom slučaju, potprogram već mijenja vrijednosti koje nisu u lokalnom opsegu, tako da će sve promjene biti vidljive programu koji poziva.

Kako bi se naznačilo da se argument mora proslijediti referencom, ključna riječ var dodaje se prije njegove deklaracije:

Procedura getTwoRandom(var n1, n2:Integer; range: Integer);

n1:=slučajni(raspon);

n2:=slučajni(raspon); kraj ;

var rand1, rand2: Integer;

Početi getTwoRandom(rand1,rand2,10); WriteLn(rand1); WriteLn(rand2);

Kraj.

U ovom primjeru, reference na dvije varijable prosljeđuju se proceduri getTwoRandom kao stvarni parametri: rand1 i rand2. Treći stvarni parametar (10) prosljeđuje se po vrijednosti. Procedura piše koristeći formalne parametre

Metode programiranja korištenjem nizova

Svrha laboratorijskog rada : naučiti metode u jeziku C#, pravila za rad sa znakovnim podacima i komponentu ListBox. Napišite program za rad sa stringovima.

Metode

Metoda je element klase koji sadrži programski kod. Metoda ima sljedeću strukturu:

[atributi] [specifikatori] naziv tipa ([parametri])

tijelo metode;

Atributi su posebne upute prevoditelju o svojstvima metode. Atributi se rijetko koriste.

Kvalifikatori su ključne riječi koje služe u različite svrhe, na primjer:

· Određivanje dostupnosti metode za druge klase:

o privatna– metoda će biti dostupna samo unutar ove klase

o zaštićen– metoda će također biti dostupna dječjim razredima

o javnost– metoda će biti dostupna bilo kojoj drugoj klasi koja može pristupiti ovoj klasi

Označavanje dostupnosti metode bez stvaranja klase

· Vrsta postavke

Tip određuje rezultat koji metoda vraća: to može biti bilo koji tip dostupan u C#, kao i ključna riječ void ako rezultat nije potreban.

Naziv metode je identifikator koji će se koristiti za pozivanje metode. Isti zahtjevi vrijede za identifikator kao i za imena varijabli: može se sastojati od slova, brojeva i podvlake, ali ne može započeti brojem.

Parametri su popis varijabli koje se mogu proslijediti metodi kada se pozove. Svaki parametar sastoji se od vrste varijable i naziva. Parametri su odvojeni zarezima.

Tijelo metode je uobičajeni programski kod, osim što ne može sadržavati definicije drugih metoda, klasa, prostora imena, itd. Ako metoda mora vratiti neki rezultat, tada ključna riječ return mora biti prisutna na kraju s povratnom vrijednošću. što znači . Ako vraćanje rezultata nije potrebno, tada korištenje ključne riječi return nije potrebno, iako je dopušteno.

Primjer metode koja procjenjuje izraz:

public double Calc(double a, double b, double c)

return Math.Sin(a) * Math.Cos(b);

dvostruko k = Math.Tan(a * b);

return k * Math.Exp(c / k);

Preopterećenje metode

Jezik C# omogućuje stvaranje više metoda s istim imenima, ali različitim parametrima. Prevodilac će automatski odabrati najprikladniju metodu prilikom izrade programa. Na primjer, možete napisati dvije odvojene metode za podizanje broja na potenciju: jedan algoritam bi se koristio za cijele brojeve, a drugi bi se koristio za realne brojeve:

///

/// Izračunaj X na potenciju Y za cijele brojeve

///

privatno int Pow(int X, int Y)

///

/// Izračunajte X na potenciju Y za realne brojeve

///

privatni dvostruki Pow (dvostruki X, dvostruki Y)

return Math.Exp(Y * Math.Log(Math.Abs(X)));

inače ako (Y == 0)

Ovaj kod se poziva na isti način, jedina razlika je u parametrima - u prvom slučaju, prevodilac će pozvati Pow metodu s cjelobrojnim parametrima, au drugom - sa stvarnim parametrima:

Zadane postavke

Jezik C#, počevši od verzije 4.0 (Visual Studio 2010), omogućuje postavljanje zadanih vrijednosti za neke parametre, tako da prilikom pozivanja metode možete izostaviti neke od parametara. Da biste to učinili, prilikom implementacije metode, potrebnim parametrima treba dodijeliti vrijednost izravno na popisu parametara:

private void GetData(int Number, int Optional = 5 )

Console.WriteLine("Broj: (0)", Broj);

Console.WriteLine("Optional: (0)", Optional);

U ovom slučaju metodu možete pozvati na sljedeći način:

GetData(10, 20);

U prvom slučaju će opcijski parametar biti jednak 20, jer je eksplicitno naveden, au drugom slučaju će biti jednak 5, jer nije eksplicitno navedeno i prevodilac uzima zadanu vrijednost.

Zadani parametri mogu se postaviti samo na desnoj strani popisa parametara; na primjer, prevodilac neće prihvatiti takav potpis metode:

private void GetData(int Neobavezno = 5 , int broj)

Kada se parametri prosljeđuju metodi na normalan način (bez dodatnih ključnih riječi ref i out), sve promjene parametara unutar metode ne utječu na njezinu vrijednost u glavnom programu. Recimo da imamo sljedeću metodu:

privatni void Calc(int broj)

Vidi se da se unutar metode mijenja varijabla Number koja je proslijeđena kao parametar. Pokušajmo pozvati metodu:

Console.WriteLine(n);

Na ekranu će se pojaviti broj 1, odnosno unatoč promjeni varijable u Calc metodi, vrijednost varijable u glavnom programu nije promijenjena. To je zbog činjenice da kada se pozove metoda, a kopirati proslijeđena varijabla, to je varijabla koju metoda mijenja. Kada se metoda prekine, vrijednost kopija se gubi. Ova metoda prosljeđivanja parametra se zove proći po vrijednosti.

Da bi metoda promijenila varijablu koja joj je proslijeđena, mora se proslijediti s ključnom riječi ref - mora biti i u potpisu metode i kada se poziva:

privatni void Calc (ref int broj)

Console.WriteLine(n);

U tom će se slučaju na zaslonu pojaviti broj 10: promjena vrijednosti u metodi također je utjecala na glavni program. Ova metoda prijenosa zove se prolazeći referencom, tj. Više se ne prenosi kopija, već referenca na stvarnu varijablu u memoriji.

Ako metoda koristi varijable prema referenci samo za vraćanje vrijednosti i nije ju briga što je u njima inicijalno, tada ne možete inicijalizirati takve varijable, već ih proslijediti s ključnom riječi out. Prevodilac razumije da početna vrijednost varijable nije važna i ne žali se na nedostatak inicijalizacije:

privatni void Calc(out int broj)

int n; // Ništa ne dodjeljujemo!

vrsta podataka string

Jezik C# koristi tip niza za pohranjivanje nizova. Da biste deklarirali (i, u pravilu, odmah inicijalizirali) string varijablu, možete napisati sljedeći kod:

string a = "Tekst";

niz b = "nizovi";

Možete izvršiti operaciju dodavanja na redovima - u ovom slučaju, tekst jednog retka bit će dodan tekstu drugog:

niz c = a + " " + b; // Rezultat: String tekst

Vrsta niza zapravo je alias za klasu String, koja vam omogućuje izvođenje niza složenijih operacija na nizovima. Na primjer, metoda IndexOf može tražiti podniz u nizu, a metoda Substring vraća dio niza određene duljine, počevši od određene pozicije:

niz a = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";

int indeks = a.IndexOf("OP"); // Rezultat: 14 (brojeći od 0)

niz b = a.Podniz(3, 5); // Rezultat: DEFGH

Ako nizu trebate dodati posebne znakove, to možete učiniti pomoću izlaznih nizova koji počinju obrnutom kosom crtom:

Komponenta ListBox

komponenta ListBox je popis čiji se elementi biraju pomoću tipkovnice ili miša. Popis elemenata određen je svojstvom Predmeti. Stavke su element koji ima svoja svojstva i svoje metode. Metode Dodati, RemoveAt I Umetnuti koriste se za dodavanje, brisanje i umetanje elemenata.

Objekt Predmeti pohranjuje objekte na popis. Objekt može biti bilo koja klasa - podaci klase pretvaraju se za prikaz u prikaz znakovnog niza metodom ToString. U našem slučaju, nizovi će se ponašati kao objekti. Međutim, budući da objekt Items pohranjuje objekte pretvorene u objekt tipa, prije nego što ga upotrijebite morate ih vratiti u izvorni tip, u našem slučaju niz:

string a = (string)listBox1.Items;

Da biste odredili broj odabranog elementa, koristite svojstvo SelectedIndex.

Kad sam počeo programirati u C++ i intenzivno proučavao knjige i članke, uvijek sam nailazio na isti savjet: ako trebamo proslijediti neki objekt funkciji koji se ne bi trebao mijenjati u funkciji, onda ga uvijek treba proslijediti referencom na konstantu(PPSK), osim u onim slučajevima kada trebamo prenijeti ili primitivni tip ili strukturu slične veličine njima. Jer Za više od 10 godina programiranja u C++-u, često sam nailazio na ovaj savjet (i sam sam ga dao više od jednom), odavno sam ga "upijao" - automatski prosljeđujem sve argumente referencom na konstantu . Ali vrijeme prolazi i već je prošlo 7 godina otkako nam je na raspolaganju C++11 sa svojom semantikom poteza, u vezi s kojom sve više čujem glasove koji propituju staru dobru dogmu. Mnogi počinju tvrditi da je prelazak referencom na konstantu stvar prošlosti i da je sada neophodan proći po vrijednosti(PPZ). Što stoji iza ovih razgovora, kao i kakve zaključke iz svega toga možemo izvući, želim raspravljati u ovom članku.

Knjiška mudrost

Kako bismo shvatili kojeg se pravila trebamo pridržavati, predlažem da se okrenemo knjigama. Knjige su izvrstan izvor informacija koje nismo dužni prihvatiti, ali koje svakako vrijedi poslušati. A mi ćemo početi s poviješću, s porijeklom. Neću otkrivati ​​tko je bio prvi apologet PPSC-a, samo ću kao primjer navesti knjigu koja je na mene osobno imala najveći utjecaj po pitanju korištenja PPSC-a.

Mayers

U redu, ovdje imamo klasu u kojoj se svi parametri prenose referencom, ima li problema s ovom klasom? Nažalost, postoji, i taj problem leži na površini. U našoj klasi imamo 2 funkcionalna entiteta: prvi uzima vrijednost u fazi stvaranja objekta, a drugi vam omogućuje promjenu prethodno postavljene vrijednosti. Imamo dva entiteta, ali četiri funkcije. Sada zamislite da ne možemo imati 2 slična entiteta, već 3, 5, 6, što onda? Tada ćemo se suočiti s ozbiljnim napuhavanjem koda. Stoga, kako se ne bi stvarala masa funkcija, postojao je prijedlog da se u potpunosti napuste veze u parametrima:

Predložak klasa Holder ( public: eksplicitno Holder(T value): m_Value(move(value)) ( ) void setValue(T value) (​m_Value = move(value); ) const T& value() const noexcept ( return m_Value; ) privatno: T m_Vrijednost; );

Prva prednost koja odmah upada u oči je znatno manje koda. Ima ga još manje nego u prvoj verziji, zbog uklanjanja const i & (iako su dodali move ). Ali oduvijek su nas učili da je prijenos po referenci produktivniji od prijenosa po vrijednosti! Tako je bilo prije C++11, tako je i sada, ali sada ako pogledamo ovaj kod, vidjet ćemo da ovdje nema više kopiranja nego u prvoj verziji, uz uvjet da T ima konstruktor poteza. Oni. Sam PPSC je bio i bit će brži od PPZ-a, ali kod nekako koristi proslijeđenu referencu, a često se ovaj argument kopira.

Međutim, to nije cijela priča. Za razliku od prve opcije, gdje imamo samo kopiranje, ovdje dodajemo i pokret. Ali selidba je jeftina operacija, zar ne? O ovoj temi, Mayersova knjiga koju razmatramo također ima poglavlje ("Stavka 29"), koje nosi naslov: "Pretpostavimo da operacije premještanja nisu prisutne, nisu jeftine i ne koriste se." Glavna ideja bi trebala biti jasna iz naslova, ali ako želite detalje, svakako ga pročitajte - neću se zadržavati na tome.

Ovdje bi bilo prikladno provesti potpunu komparativnu analizu prve i posljednje metode, ali ne bih želio odstupiti od knjige, pa ćemo analizu odgoditi za druge odjeljke, a ovdje ćemo nastaviti razmatrati Scottove argumente. Dakle, osim činjenice da je treća opcija očito kraća od druge, što Scott vidi kao prednost PPZ-a nad PPSC-om u modernom kodu?

On to vidi u tome što u slučaju prolaska rvalue, tj. neki pozivaju ovako: Holder holder(string("me")); , opcija s PPSC će nam omogućiti kopiranje, a opcija s PPZ kretanje. S druge strane, ako je prijenos ovakav: Holder holder(someLvalue); , onda PPZ definitivno gubi jer će vršiti i kopiranje i premještanje, dok će u verziji s PPSC biti samo jedno kopiranje. Oni. ispada da je PPZ, ako uzmemo u obzir čisto učinkovitost, neka vrsta kompromisa između količine koda i "pune" (preko && ) podrške za semantiku kretanja.

Zato je Scott tako pažljivo formulirao svoje savjete i tako ih pažljivo promovira. Čak mi se činilo da je to iznio nevoljko, kao pod pritiskom: nije mogao a da rasprave o ovoj temi ne smjesti u knjigu, jer... o tome se dosta naširoko raspravljalo, a Scott je uvijek bio sakupljač kolektivnog iskustva. Uz to, daje vrlo malo argumenata u obranu PPZ-a, ali daje puno onih koji tu “tehniku” dovode u pitanje. Pogledat ćemo njegove argumente protiv u kasnijim odjeljcima, ali ovdje ćemo ukratko ponoviti argument koji Scott iznosi u obranu JPP-a (mentalno dodajući “ako objekt podržava kretanje i ako je jeftin”): omogućuje vam da izbjegnete kopiranje kada prosljeđujete rvalue izraz kao argument funkcije. Ali dosta mučne Meyersove knjige, prijeđimo na drugu knjigu.

Usput, ako je netko pročitao knjigu i iznenađen je što ovdje ne uključujem opciju s onim što je Mayers nazvao univerzalnim referencama - sada poznatim kao reference za prosljeđivanje - onda je to lako objasniti. Razmišljam samo o PPZ i PPSC, jer... Smatram lošim oblikom uvoditi funkcije predložaka za metode koje nisu predlošci samo radi podržavanja prosljeđivanja po referenci obje vrste (rvalue/lvalue). Da ne spominjemo činjenicu da kod ispada drugačiji (nema više postojanosti) i sa sobom donosi druge probleme.

Josattis i društvo

Posljednja knjiga koju ćemo pogledati je “C++ Templates”, koja je ujedno i najnovija od svih knjiga spomenutih u ovom članku. Objavljena je krajem 2017. (a unutar knjige je naznačena 2018.). Za razliku od drugih knjiga, ova je u potpunosti posvećena obrascima, a ne savjetima (kao Mayers) ili C++ općenito, kao Stroustrup. Stoga se prednosti i mane ovdje razmatraju sa stajališta predložaka za pisanje.

Ovoj temi posvećeno je cijelo 7. poglavlje koje nosi rječiti naslov “Po vrijednosti ili po referenci?”. U ovom poglavlju autori vrlo kratko, ali jezgrovito opisuju sve metode prijenosa sa svim njihovim prednostima i manama. Ovdje se praktički ne daje analiza učinkovitosti, a podrazumijeva se da će PPSC biti brži od PPZ-a. Ali uz sve to, na kraju poglavlja autori preporučuju korištenje zadanog PPP-a za funkcije predloška. Zašto? Zato što se korištenjem veze parametri predloška prikazuju u cijelosti, a bez poveznice se „raspadaju“, što povoljno utječe na obradu nizova i string literala. Autori vjeruju da ako se za neku vrstu PPP-a pokaže neučinkovitim, uvijek možete koristiti std::ref i std::cref. Ovo je savjet, da budem iskren, jeste li vidjeli mnogo ljudi koji žele koristiti gore navedene funkcije?

Što savjetuju u vezi PPSC-a? Oni savjetuju korištenje PPSC-a kada je izvedba kritična ili postoje drugi težak razloge da se ne koristi PPP. Naravno, ovdje govorimo samo o standardnom kodu, ali ovaj savjet je u izravnoj suprotnosti sa svime što su programeri učili desetljeće. Ovo nije samo savjet da se PPP razmotri kao alternativa - ne, ovo je savjet da PPSC postane alternativa.

Ovime završavamo naše putovanje knjigama, jer... Ne znam ni za jednu drugu knjigu koju bismo trebali konzultirati o ovom pitanju. Prijeđimo na drugi medijski prostor.

Mrežna mudrost

Jer Živimo u doba interneta, pa se ne treba oslanjati samo na mudrost iz knjiga. Štoviše, mnogi autori koji su prije pisali knjige sada jednostavno pišu blogove i napustili su knjige. Jedan od tih autora je Herb Sutter, koji je u svibnju 2013. godine na svom blogu objavio članak “GotW #4 Solution: Class Mechanics” koji, iako nije u potpunosti posvećen problemu koji obrađujemo, ipak ga se dotiče.

Dakle, u originalnoj verziji članka, Sutter je jednostavno ponovio staru mudrost: "prenesi parametre referencom na konstantu", ali više nećemo vidjeti ovu verziju članka, jer Članak sadrži suprotan savjet: “ Ako parametar će se i dalje kopirati, a zatim ga proslijediti po vrijednosti.” Opet ono notorno "ako". Zašto je Sutter promijenio članak i kako sam ja za to znao? Iz komentara. Pročitajte komentare na njegov članak; usput, oni su zanimljiviji i korisniji od samog članka. Istina, nakon što je napisao članak, Sutter je konačno promijenio mišljenje i više ne daje takve savjete. Promjena mišljenja može se pronaći u njegovom govoru na CppConu 2014.: „Povratak osnovama! Osnove modernog C++ stila". Svakako pogledajte, idemo na sljedeći internetski link.

A zatim imamo glavni programski resurs 21. stoljeća: StackOverflow. Odnosno odgovor, čiji je broj pozitivnih reakcija premašio 1700 u trenutku pisanja ovog članka. Pitanje je: Što je idiom kopiraj i zamijeni? , i, kao što bi naslov trebao sugerirati, nije baš u vezi s temom koju razmatramo. Ali u odgovoru na ovo pitanje autor se dotiče i teme koja nas zanima. Također savjetuje korištenje PPZ-a "ako će argument ionako biti kopiran" (vrijeme je da se i za ovo uvede kratica, bogami). I općenito, ovaj savjet se čini sasvim prikladnim, u okviru njegovog odgovora i operatora o kojem se govori, ali autor si uzima slobodu dati takav savjet u širem smislu, a ne samo u ovom konkretnom slučaju. Štoviše, on ide dalje od svih savjeta o kojima smo prethodno raspravljali i poziva da se to učini čak i u C++03 kodu! Što je autora potaknulo na takve zaključke?

Očigledno je autor odgovora izvukao glavnu inspiraciju iz članka drugog autora knjige i honorarnog programera Boost.MPL-a - Davea Abrahamsa. Članak se zove “Want Speed? Prođi pored vrijednosti.” , a objavljena je još u kolovozu 2009. godine, tj. 2 godine prije usvajanja C++11 i uvođenja semantike poteza. Kao i u prethodnim slučajevima, preporučam čitatelju da sam pročita članak, ali ja ću navesti glavne argumente (postoji, zapravo, samo jedan argument) koje Dave daje u korist PPZ-a: trebate koristiti PPZ , jer optimizacija "preskoči kopiju" dobro radi s njim (copy elision), koja je odsutna u PPSC-u. Čitajući komentare na članak, vidi se da savjeti koje promiče nisu univerzalni, što potvrđuje i sam autor odgovarajući na kritike komentatora. Međutim, članak sadrži izričit savjet (smjernicu) za korištenje PPP-a ako će argument svejedno biti kopiran. Usput, ako koga zanima, može pročitati članak “Želite brzinu? Nemojte (uvijek) prolaziti kroz vrijednost.” . Kao što bi naslov trebao pokazati, ovaj je članak odgovor na Daveov članak, pa ako ste pročitali prvi, svakako pročitajte i ovaj!

Nažalost (na sreću nekih), takvi članci i (još više) popularni odgovori na popularnim stranicama dovode do masovne upotrebe sumnjivih tehnika (trivijalan primjer) jednostavno zato što to zahtijeva manje pisanja, a stara dogma više nije nepokolebljiva - Uvijek se možete pozvati na “taj popularni savjet” ako ste pritjerani uza zid. Sada predlažem da se upoznate s onim što nam razni resursi nude s preporukama za pisanje koda.

Jer Budući da su razni standardi i preporuke sada također objavljeni na internetu, odlučio sam ovaj odjeljak klasificirati kao "mrežnu mudrost". Dakle, ovdje bih želio govoriti o dva izvora, čija je svrha poboljšati kod C++ programera dajući im savjete (smjernice) kako napisati ovaj kod.

Prvi skup pravila koji želim razmotriti bila je kap koja je prelila čašu koja me natjerala da prihvatim ovaj članak. Ovaj skup je dio pomoćnog programa clang-tidy i ne postoji izvan njega. Kao i sve što je povezano s clangom, ovaj je uslužni program vrlo popularan i već je dobio integraciju s CLion i Resharper C++ (tako sam naišao na njega). Dakle, clang-tydy sadrži modernize-pass-by-value pravilo koje radi na konstruktorima koji prihvaćaju argumente putem PPSC-a. Ovo pravilo predlaže da PPSC zamijenimo s PPZ. Štoviše, u vrijeme pisanja članka, opis ovog pravila sadrži napomenu da ovo pravilo Pozdrav radi samo za konstruktore, ali će oni (tko su oni?) rado prihvatiti pomoć onih koji proširuju ovo pravilo na druge entitete. Tamo, u opisu, postoji poveznica na Daveov članak - jasno je odakle dolaze noge.

Na kraju, da zaključimo ovaj pregled tuđe mudrosti i autoritativnih mišljenja, predlažem da pogledate službene smjernice za pisanje C++ koda: C++ Core Guidelines, čiji su glavni urednici Herb Sutter i Bjarne Stroustrup (nije loše, zar ne?). Dakle, ove preporuke sadrže sljedeće pravilo: “Za “in” parametre, proslijedite jeftino kopirane tipove prema vrijednosti, a ostale prema referenci na const”, što u potpunosti ponavlja staru mudrost: PPSK posvuda i PPP za male objekte. Ovaj savjet navodi nekoliko alternativa koje treba razmotriti. u slučaju da prosljeđivanje argumenta treba optimizaciju. Ali PPZ nije uvršten na popis alternativa!

Budući da nemam drugih izvora vrijednih pažnje, predlažem prijeći na izravnu analizu oba načina prijenosa.

Analiza

Cijeli prethodni tekst napisan je na meni pomalo neuobičajen način: iznosim tuđa mišljenja, a svoja se čak trudim ne iznositi (znam da loše ispada). Velikim dijelom zbog činjenice da su tuđa mišljenja, a cilj mi je bio napraviti njihov kratak pregled, odgodio sam detaljnije razmatranje pojedinih argumenata koje sam pronašao kod drugih autora. U ovom dijelu se neću pozivati ​​na autoritete i iznositi mišljenja, ovdje ćemo se osvrnuti na neke objektivne prednosti i nedostatke PPSC i PPZ, koje će biti začinjene mojom subjektivnom percepcijom. Naravno, nešto od onoga o čemu smo ranije raspravljali bit će ponovljeno, ali, nažalost, ovo je struktura ovog članka.

Ima li JPP prednost?

Dakle, prije nego što razmotrimo argumente za i protiv, predlažem da pogledamo što iu kojim slučajevima daje prednost koju nam daje prolaz kroz vrijednost. Recimo da imamo klasu poput ove:

Klasa CopyMover ( public: void setByValuer(Accounter byValuer) ( m_ByValuer = std::move(byValuer); ) void setByRefer(const Accounter& byRefer) ( m_ByRefer = byRefer; ) void setByValuerAndNotMover(Accounter byValuerAndNotMover) ( m _ByValuerAndNotMover = byVal uerAndNotMover; ) void setRvaluer (Račun&& rvaluer) ( m_Rvaluer = std::move(rvaluer); ) );

Iako nas za potrebe ovog članka zanimaju samo prve dvije funkcije, uključio sam četiri opcije samo da ih koristim kao kontrast.

Klasa Accounter je jednostavna klasa koja broji koliko je puta kopirana/premještena. A u klasi CopyMover implementirali smo funkcije koje nam omogućuju razmatranje sljedećih opcija:

    krećući se prošao argument.

    Prijeđi po vrijednosti, nakon čega slijedi kopiranje prošao argument.

Sada, ako proslijedimo lvalue svakoj od ovih funkcija, na primjer ovako:

Accounting byRefer; Accountant byValuer; Accounter byValuerAndNotMover; CopyMover copyMover; copyMover.setByRefer(byRefer); copyMover.setByValuer(byValuer); copyMover.setByValuerAndNotMover(byValuerAndNotMover);

tada dobivamo sljedeće rezultate:

Očigledni pobjednik je PPSC, jer... daje samo jedan primjerak, dok PPZ daje jedan primjerak i jedan potez.

Sada pokušajmo prenijeti rvalue:

CopyMover copyMover; copyMover.setByRefer(Račun()); copyMover.setByValuer(Račun()); copyMover.setByValuerAndNotMover(Račun()); copyMover.setRvaluer(Račun());

Dobivamo sljedeće:

Ovdje nema jasnog pobjednika, jer... i PPZ i PPSK imaju po jednu operaciju, ali zbog činjenice da PPZ koristi kretanje, a PPSK kopiranje, možemo dati pobjedu PPZ-u.

Ali naši eksperimenti tu ne završavaju; dodajmo sljedeće funkcije za simulaciju neizravnog poziva (s naknadnim prosljeđivanjem argumenata):

Void setByValuer(Accounter byValuer, CopyMover& copyMover) ( copyMover.setByValuer(std::move(byValuer)); ) void setByRefer(const Accounter& byRefer, CopyMover& copyMover) ( copyMover.setByRefer(byRefer); ) ...

Koristit ćemo ih na potpuno isti način kao što smo radili bez njih, stoga neću ponavljati kod (po potrebi pogledajte u repozitorij). Dakle, za lvalue rezultati bi bili ovakvi:

Imajte na umu da PPSC povećava razmak u odnosu na PPZ, ostajući s jednom kopijom, dok PPZ već ima čak 3 operacije (jedan pokret više)!

Sada prosljeđujemo rvalue i dobivamo sljedeće rezultate:

Sada PPZ ima 2 pokreta, a PPSC još uvijek ima jedan primjerak. Je li sada moguće nominirati PPZ kao pobjednika? Ne, jer ako jedan potez ne bi trebao biti barem ništa lošiji od jedne kopije, ne možemo reći isto za 2 poteza. Stoga u ovom primjeru neće biti pobjednika.

Mogu mi prigovoriti: “Autore, ti imaš pristrano mišljenje i uvlačiš ono što ti je od koristi. Čak 2 poteza bit će jeftinija od kopiranja!” Ne mogu se složiti s ovom tvrdnjom Sve u svemu, jer Koliko je selidba brža od preslikavanja ovisi o konkretnoj klasi, ali ćemo se o "jeftini" selidbi osvrnuti u zasebnom odjeljku.

Ovdje smo se dotakli jedne zanimljive stvari: dodali smo jedan neizravni poziv, a PPP je dodao točno jednu operaciju u “težini”. Mislim da ne morate imati diplomu MSTU-a da shvatite da što više neizravnih poziva imamo, to će se više operacija obaviti korištenjem PPZ-a, dok će za PPSC broj ostati nepromijenjen.

Malo je vjerojatno da će sve što je gore razmotreno biti otkriće za bilo koga, možda nismo ni provodili eksperimente - svi ovi brojevi trebali bi biti očiti većini C++ programera na prvi pogled. Istina, jedna stvar ipak zaslužuje pojašnjenje: zašto, u slučaju rvalue, PZ nema kopiju (ili drugi potez), već samo jedan potez.

Pa, pogledali smo razliku u prijenosu između PPZ i PPSC promatrajući iz prve ruke broj kopija i poteza. Iako je očito da je prednost PPZ-a nad PPSC-om čak iu ovako jednostavnim primjerima, blago rečeno, Ne Očito, još uvijek, pomalo pretenciozno, donosim sljedeći zaključak: ako ćemo i dalje kopirati argument funkcije, onda ima smisla razmotriti prosljeđivanje argumenta funkciji prema vrijednosti. Zašto sam izvukao ovaj zaključak? Za glatki prijelaz na sljedeći odjeljak.

Ako kopiramo...

Dakle, dolazimo do poslovičnog "ako". Većina argumenata na koje smo naišli nije pozivala na univerzalnu implementaciju PPP-a umjesto PPSC-a; pozivali su samo na to "ako se argument svejedno kopira." Vrijeme je da shvatimo što nije u redu s ovim argumentom.

Želim započeti s malim opisom kako pišem kod. U posljednje vrijeme moj proces kodiranja sve više liči na TDD, tj. pisanje bilo koje metode klase počinje pisanjem testa u kojem se ova metoda pojavljuje. Sukladno tome, kada počnem pisati test i kreiram metodu nakon pisanja testa, još uvijek ne znam hoću li kopirati argument. Naravno, nisu sve funkcije kreirane na ovaj način; često, čak iu procesu pisanja testa, znate točno kakva će implementacija biti. Ali to se ne događa uvijek!

Netko bi mi mogao prigovoriti da nije bitno kako je metoda izvorno napisana, možemo promijeniti način prosljeđivanja argumenta kada metoda poprimi oblik i potpuno nam je jasno što se tu događa (tj. imamo li kopiranje ili ne ). Djelomično se slažem s ovim - doista, možete to učiniti na ovaj način, ali ovo nas uključuje u neku čudnu igru ​​u kojoj moramo mijenjati sučelja samo zato što se implementacija promijenila. Što nas dovodi do sljedeće dileme.

Ispada da modificiramo (ili čak planiramo) sučelje na temelju toga kako će biti implementirano. Ne smatram se stručnjakom za OOP i druge teorijske izračune softverske arhitekture, ali takve radnje jasno proturječe osnovnim pravilima kada implementacija ne bi trebala utjecati na sučelje. Naravno, određeni detalji implementacije (bilo da su značajke jezika ili ciljne platforme) i dalje cure kroz sučelje na ovaj ili onaj način, ali trebali biste pokušati smanjiti, a ne povećati, broj takvih stvari.

Pa, Bog ga blagoslovio, idemo ovim putem i dalje mijenjamo sučelja ovisno o tome što radimo u implementaciji u smislu kopiranja argumenta. Recimo da smo napisali ovu metodu:

Void setName(Ime imena) ( m_Name = premjesti(naziv); )

i predao naše promjene u repozitorij. Kako je vrijeme prolazilo, naš programski proizvod dobivao je nove funkcionalnosti, integrirali su se novi okviri, a pojavila se i zadaća informiranja vanjskog svijeta o promjenama u našoj klasi. Oni. Dodat ćemo neki mehanizam obavijesti našoj metodi, neka to bude nešto slično Qt signalima:

Void setName(Name name) ( m_Name = move(name); emit nameChanged(m_Name); )

Postoji li problem s ovim kodom? Jesti. Za svaki poziv setName šaljemo signal, tako da će signal biti poslan čak i kada značenje m_Name nije promijenjeno. Osim problema s izvedbom, ova situacija može dovesti do beskonačne petlje zbog koda koji prima gornju obavijest da nekako pozove setName. Da biste izbjegli sve ove probleme, takve metode najčešće izgledaju ovako:

Void setName(Ime imena) ( if(ime == m_Name) return; m_Name = premjesti(ime); emitiraj nameChanged(m_Name); )

Riješili smo se gore opisanih problema, ali sada naše pravilo “ako ipak kopiramo...” nije uspjelo - više nema bezuvjetnog kopiranja argumenta, sada ga kopiramo samo ako se promijeni! Što bismo sada trebali učiniti? Promijeniti sučelje? U redu, promijenimo sučelje klase zbog ovog popravka. Što ako je naša klasa naslijedila ovu metodu iz nekog apstraktnog sučelja? Promijenimo ga i tamo! Ima li puno promjena jer se promijenila implementacija?

Opet mi mogu prigovoriti, kažu, autoru, zašto pokušavaš uštedjeti na utakmicama kad će ovo stanje funkcionirati tamo? Da, većina poziva bit će lažna! Ima li povjerenja u ovo? Gdje? I ako sam odlučio štedjeti na šibicama, nije li sama činjenica da smo koristili PPZ bila posljedica upravo takve štednje? Ja samo nastavljam “stranačku liniju” koja zagovara učinkovitost.

Konstruktori

Prijeđimo ukratko na konstruktore, pogotovo zato što postoji posebno pravilo za njih u clang-tidyju, koje još ne radi za druge metode/funkcije. Recimo da imamo klasu poput ove:

Klasa JustClass ( public: JustClass(const string& justString): m_JustString(justString) ( ) private: string m_JustString; );

Očito, parametar je kopiran, a clang-tidy će nam reći da bi bilo dobro prepisati konstruktor na ovo:

JustClass(string justString): m_JustString(move(justString)) ( )

I, iskreno govoreći, teško mi je raspravljati ovdje - na kraju krajeva, mi stvarno uvijek kopiramo. I najčešće, kada nešto provučemo kroz konstruktor, mi to kopiramo. Ali češće ne znači uvijek. Evo još jednog primjera:

Klasa TimeSpan ( public: TimeSpan(DateTime start, DateTime end) ( if(start > end) throw InvalidTimeSpan(); m_Start = move(start); m_End = move(end); ) private: DateTime m_Start; DateTime m_End; );

Ovdje ne kopiramo uvijek, već samo kada su datumi ispravno prikazani. Naravno, u velikoj većini slučajeva to će biti slučaj. Ali ne uvijek.

Možete dati još jedan primjer, ali ovaj put bez koda. Zamislite da imate klasu koja prihvaća veliki objekt. Klasa postoji već dugo, a sada je vrijeme da ažuriramo njenu implementaciju. Shvaćamo da nam od jednog velikog objekta (koji je godinama rastao) ne treba više od pola, a možda i manje. Možemo li nešto učiniti u vezi s tim prolaskom po vrijednosti? Ne, nećemo moći ništa učiniti jer će kopija ipak biti stvorena. Ali kad bismo koristili PPSC, jednostavno bismo promijenili ono što radimo iznutra dizajner. I ovo je ključna točka: pomoću PPSC-a kontroliramo što i kada se događa u implementaciji naše funkcije (konstruktora), ali ako koristimo PPZ, tada gubimo svaku kontrolu nad kopiranjem.

Što možete uzeti iz ovog odjeljka? Činjenica da je argument "ako ipak kopiramo..." vrlo kontroverzan, jer Ne znamo uvijek što ćemo kopirati, a čak i kada znamo, vrlo često nismo sigurni da će se tako nastaviti i u budućnosti.

Selidba je jeftina

Od samog trenutka kada se pojavila semantika kretanja, počela je imati ozbiljan utjecaj na način pisanja modernog C++ koda, a s vremenom se taj utjecaj samo pojačao: nije ni čudo, jer kretanje je tako jeftino u usporedbi s kopiranjem. Ali je li? Je li istina da je kretanje Stalno jeftina operacija? To je ono što ćemo pokušati shvatiti u ovom odjeljku.

Binarni veliki objekt

Počnimo s trivijalnim primjerom, recimo da imamo sljedeću klasu:

Struct Blob (std::array podaci; );

Obični mrlja(BDO, engleski BLOB), koji se može koristiti u raznim situacijama. Pogledajmo koliko će nas koštati prolaz po referenci i po vrijednosti. Naš BDO će se koristiti otprilike ovako:

Void Storage::setBlobByRef(const Blob& blob) ( m_Blob = blob; ) void Storage::setBlobByVal(Blob blob) ( m_Blob = move(blob); )

I mi ćemo te funkcije nazvati ovako:

Const Blob blob(); skladištenje; pohrana.setBlobByRef(blob); pohrana.setBlobByVal(blob);

Kod za ostale primjere bit će identičan ovome, samo s drugačijim nazivima i vrstama, pa ga neću navoditi za preostale primjere - sve je u repozitoriju.

Prije nego što prijeđemo na mjerenja, pokušajmo predvidjeti rezultat. Dakle, imamo 4 KB std::array koji želimo pohraniti u objekt klase Storage. Kako smo ranije saznali, za PPSC ćemo imati jedan primjerak, dok ćemo za PPZ imati jedan primjerak i jedan potez. Na temelju činjenice da je nemoguće premjestiti niz, postojat će 2 kopije za PPZ, nasuprot jednoj za PPSC. Oni. možemo očekivati ​​dvostruku superiornost u performansama za PPSC.

Sada pogledajmo rezultate testa:

Ovaj i svi sljedeći testovi pokrenuti su na istom stroju koristeći MSVS 2017 (15.7.2) i oznaku /O2.

Praksa se poklopila s pretpostavkom - prolaz po vrijednosti je 2 puta skuplji, jer je za niz premještanje potpuno jednako kopiranju.

Crta

Pogledajmo još jedan primjer, regularni std::string. Što možemo očekivati? Znamo (o tome sam raspravljao u članku) da moderne implementacije razlikuju dvije vrste nizova: kratke (oko 16 znakova) i duge (one koje su duže od kratkih). Za kratke se koristi interni međuspremnik, koji je regularni C-niz char, ali dugi će već biti postavljeni na hrpu. Ne zanimaju nas kratke linije, jer... rezultat će tamo biti isti kao kod BDO-a, stoga se usredotočimo na duge linije.

Dakle, imajući dugačak niz, očito je da bi njegovo pomicanje trebalo biti prilično jeftino (samo pomaknite pokazivač), tako da možete računati na to da pomicanje niza ne bi trebalo uopće utjecati na rezultate, a PPZ bi trebao dati rezultat ništa lošije od PPSC-a. Provjerimo to u praksi i dobićemo sljedeće rezultate:

Prijeći ćemo na objašnjenje ovog "fenomena". Dakle, što se događa kada kopiramo postojeći niz u već postojeći niz? Pogledajmo trivijalni primjer:

Prvi niz (64, "C"); niz drugi(64, "N"); //... drugi = prvi;

Imamo dva niza od 64 znaka, tako da interni međuspremnik nije dovoljan kada ih kreiramo, što rezultira time da su oba niza alocirana na gomilu. Sada kopiramo prvo u drugo. Jer naše veličine redaka su iste, očito ima dovoljno prostora dodijeljenog u drugom za smještaj svih podataka iz prvog, tako da drugi = prvi; bit će banalni memcpy, ništa više. Ali ako pogledamo malo modificirani primjer:

Prvi niz (64, "C"); niz drugi = prvi;

tada više neće biti poziva operator= , već će se pozivati ​​konstruktor kopiranja. Jer Budući da imamo posla s konstruktorom, u njemu nema postojeće memorije. Prvo ga je potrebno odabrati i tek onda prvo kopirati. Oni. ovo je dodjela memorije, a zatim memcpy. Kao što vi i ja znamo, dodjeljivanje memorije na globalnoj gomili obično je skupa operacija, tako da će kopiranje iz drugog primjera biti skuplje od kopiranja iz prvog. Skuplje po heap raspodjeli memorije.

Kakve to veze ima s našom temom? Najizravniji, jer prvi primjer pokazuje točno što se događa s PPSC-om, a drugi pokazuje što se događa s PPZ-om: za PPZ se uvijek kreira novi red, dok se za PPSC ponovno koristi postojeći. Već ste vidjeli razliku u vremenu izvršenja, tako da se tu nema što dodati.

Ovdje se ponovno suočavamo s činjenicom da pri korištenju JPP-a radimo izvan konteksta, te stoga ne možemo iskoristiti sve prednosti koje on može pružiti. I ako smo ranije razmišljali u smislu teoretskih budućih promjena, ovdje promatramo vrlo konkretan neuspjeh u produktivnosti.

Naravno, netko bi mi mogao prigovoriti da niz stoji odvojeno, a većina tipova ne funkcionira na taj način. Na što mogu odgovoriti sljedeće: sve što je ranije opisano bit će istinito za bilo koji spremnik koji alocira memoriju u gomili odmah za paket elemenata. Također, tko zna koje se druge optimizacije osjetljive na kontekst koriste u drugim vrstama?

Što biste trebali ukloniti iz ovog odjeljka? Činjenica da čak i ako je premještanje stvarno jeftino ne znači da će zamjena kopiranja s kopiranjem+premještanjem uvijek dati rezultat usporediv u smislu izvedbe.

Složen tip

Na kraju, pogledajmo tip koji će se sastojati od više objekata. Neka ovo bude klasa Osoba koja se sastoji od podataka svojstvenih osobi. Obično je to vaše ime, prezime, poštanski broj itd. Sve ovo možete predstaviti kao nizove i pretpostaviti da će nizovi koje ste stavili u polja klase Person vjerojatno biti kratki. Iako vjerujem da će u stvarnom životu mjerenje kratkih žica biti najkorisnije, ipak ćemo pogledati žice različitih veličina kako bismo dobili potpuniju sliku.

Također ću koristiti osobu s 10 polja, ali za to neću kreirati 10 polja izravno u tijelu klase. Implementacija Persona skriva spremnik u svojim dubinama - to čini praktičnijim mijenjanje parametara testa, praktički bez odstupanja od onoga kako bi to funkcioniralo da je Person stvarna klasa. Međutim, implementacija je dostupna i uvijek možete provjeriti kod i reći mi jesam li napravio nešto krivo.

Dakle, krenimo: Osoba s 10 polja tipa string, koja prenosimo pomoću PPSC i PPZ u Storage:

Kao što vidite, imamo veliku razliku u performansama, što ne bi trebalo biti iznenađenje za čitatelje nakon prethodnih odjeljaka. Također vjerujem da je klasa Person dovoljno "stvarna" da takvi rezultati neće biti odbačeni kao apstraktni.

Usput, kada sam pripremao ovaj članak, pripremio sam još jedan primjer: klasu koja koristi nekoliko std::function objekata. Po mojoj zamisli trebao je pokazati i prednost u izvedbi PPSC nad PPZ, ali pokazalo se upravo suprotno! Ali ne navodim ovaj primjer ovdje ne zato što mi se nisu svidjeli rezultati, već zato što nisam imao vremena shvatiti zašto su takvi rezultati dobiveni. Unatoč tome, postoji kod u repozitoriju (Pisači), testovi - također, ako netko želi shvatiti, bilo bi mi drago čuti rezultate istraživanja. Kasnije se planiram vratiti na ovaj primjer, a ako nitko ne objavi te rezultate prije mene, onda ću ih razmotriti u posebnom članku.

Rezultati

Pa smo pogledali razne prednosti i nedostatke prosljeđivanja po vrijednosti i prosljeđivanja po referenci na konstantu. Pogledali smo neke primjere i pogledali izvedbu obje metode u ovim primjerima. Naravno, ovaj članak ne može i nije iscrpan, ali, po mom mišljenju, sadrži dovoljno informacija za donošenje neovisne i informirane odluke o tome koju metodu je najbolje koristiti. Netko može prigovoriti: "Zašto koristiti jednu metodu, krenimo od zadatka!" Dok se općenito slažem s ovom tezom, u ovoj situaciji se s njom ne slažem. Vjerujem da može postojati samo jedan način prenošenja argumenata u jeziku, koji se koristi prema zadanim postavkama.

Što znači default? To znači da kada pišem funkciju, ne razmišljam o tome kako bih trebao proslijediti argument, samo koristim "default". Jezik C++ prilično je složen jezik koji mnogi ljudi izbjegavaju. I po mom mišljenju, složenost nije uzrokovana toliko složenošću jezičnih konstrukata koji postoje u jeziku (tipični programer se možda nikad neće susresti s njima), već činjenicom da vas jezik tjera da mnogo razmišljate: jesam li se oslobodio up memory, je li skupo koristiti ovu funkciju ovdje? i tako dalje.

Mnogi programeri (C, C++ i drugi) nepovjerljivi su i boje se C++ koji se počeo pojavljivati ​​nakon 2011. godine. Čuo sam mnogo kritika da jezik postaje sve složeniji, da sada samo "gurui" mogu pisati na njemu itd. Osobno vjerujem da to nije tako - naprotiv, povjerenstvo posvećuje puno vremena tome da jezik učini što ugodnijim za početnike i da programeri moraju manje razmišljati o značajkama jezika. Uostalom, ako se ne moramo mučiti s jezikom, onda imamo vremena razmišljati o zadatku. Ova pojednostavljenja uključuju pametne pokazivače, lambda funkcije i još mnogo toga što se pojavilo u jeziku. Pritom, ne poričem činjenicu da sada treba više učiti, ali što je loše u učenju? Ili se ne događaju promjene u drugim popularnim jezicima koje treba naučiti?

Nadalje, ne sumnjam da će biti snobova koji mogu odgovoriti: "Ne želite razmišljati? Zatim idite pisati u PHP-u.” Takvima ne želim ni odgovarati. Navest ću samo primjer iz realityja igre: u prvom dijelu Starcrafta, kada se novi radnik stvori u zgradi, da bi počeo vaditi minerale (ili plin), trebalo ga je ručno poslati tamo. Štoviše, svaki paket minerala imao je granicu, nakon čijeg dostizanja je povećanje broja radnika bilo beskorisno, pa su čak mogli ometati jedni druge, pogoršavajući proizvodnju. Ovo je promijenjeno u Starcraftu 2: radnici automatski počinju rudariti minerale (ili plin), a također pokazuje koliko radnika trenutno rudari i koliki je limit ovog ležišta. Ovo je uvelike pojednostavilo igračevu interakciju s bazom, dopuštajući mu da se usredotoči na važnije aspekte igre: izgradnju baze, gomilanje trupa i uništavanje neprijatelja. Čini se da je ovo samo velika inovacija, ali ono što je počelo na Internetu! Ljudi (tko su oni?) počeli su vrištati da je igra "zajebana" i "ubili su Starcraft". Očito su takve poruke mogle doći samo od “čuvara tajnih znanja” i “adepta visokog APM-a” koji su voljeli biti u nekom “elitnom” klubu.

Dakle, vraćajući se našoj temi, što manje trebam razmišljati o tome kako napisati kod, to više vremena imam za razmišljanje o rješavanju trenutnog problema. Razmišljanje o tome koju bih metodu trebao upotrijebiti - PPSC ili PPZ - ne dovodi me ni trunku bliže rješenju problema, tako da jednostavno odbijam razmišljati o takvim stvarima i biram jednu opciju: prenošenje reference na konstantu. Zašto? Zato što ne vidim nikakve prednosti za JPP u općim slučajevima, a posebne slučajeve treba razmotriti zasebno.

To je poseban slučaj, samo što sam primijetio da se u nekoj metodi PPSC pokazao kao usko grlo, a promjenom prijenosa u PPZ dobit ćemo značajno povećanje performansi, ne oklijevam koristiti PPZ. Ali prema zadanim postavkama koristit ću PPSC i u uobičajenim funkcijama i u konstruktorima. I ako je moguće, promovirat ću ovu posebnu metodu gdje god je to moguće. Zašto? Zato što mislim da je praksa promicanja JPP-a opaka zbog činjenice da lavovski dio programera nema previše znanja (bilo u načelu, ili jednostavno još nisu ušli u stvar) i jednostavno slijede savjete. Osim toga, ako ima više proturječnih savjeta, izaberu onaj koji je jednostavniji, a to dovodi do pesimizma koda jednostavno zato što je netko negdje nešto čuo. O da, ovaj netko također može dati link na Abrahamsov članak da dokaže da je u pravu. I onda sjedite, čitate kod i razmišljate: je li činjenica da se ovdje parametar prosljeđuje po vrijednosti zato što je programer koji je ovo napisao došao s Jave, samo je pročitao puno "pametnih" članaka ili stvarno postoji potreba za tehničke karakteristike?

PPSC je mnogo lakši za čitanje: osoba jasno poznaje "dobru formu" C++ i idemo dalje - pogled se ne zadržava. Praksa korištenja PPSC-a podučavala se C++ programerima godinama, koji je razlog da ga se napusti? To me navodi na još jedan zaključak: ako sučelje metode koristi PPP, onda bi trebao postojati i komentar zašto je to tako. U drugim slučajevima mora se primijeniti PPSC. Naravno, postoje tipovi izuzetaka, ali ih ne spominjem ovdje samo zato što se podrazumijevaju: string_view, initializer_list, razni iteratori, itd. Ali to su iznimke, čiji se popis može proširiti ovisno o tome koje se vrste koriste u projektu. Ali bit ostaje ista od C++98: prema zadanim postavkama uvijek koristimo PPCS.

Za std::string najvjerojatnije neće biti razlike na malim nizovima, o tome ćemo kasnije.

Unaprijed se ispričavam zbog pretenciozne napomene o "postavljanju bodova", ali moramo vas nekako namamiti u članak)) Sa svoje strane, pokušat ću osigurati da sažetak i dalje ispunjava vaša očekivanja.

Ukratko o čemu pričamo

Svi to već znaju, ali na početku ću vas podsjetiti kako se parametri metode mogu proslijediti u 1C. Mogu se proslijediti "po referenci" ili "po vrijednosti". U prvom slučaju metodi prosljeđujemo istu vrijednost kao na mjestu poziva, au drugom njezinu kopiju.

Prema zadanim postavkama, u 1C, argumenti se prosljeđuju referencom, a promjene parametra unutar metode bit će vidljive izvan metode. Ovdje daljnje razumijevanje pitanja ovisi o tome što točno podrazumijevate pod riječju "promjena parametra". Dakle, ovo znači preraspodjelu i ništa više. Štoviše, dodjela može biti implicitna, na primjer, pozivanje metode platforme koja vraća nešto u izlaznom parametru.

Ali ako ne želimo da se naš parametar prosljeđuje referencom, tada možemo navesti ključnu riječ prije parametra Značenje

Procedura ByValue(parametar vrijednosti) Parametar = 2; Parametar EndProcedure = 1; PoVrijednosti(Parametar); Izvješće (parametar); // ispisat će 1

Sve radi kako je obećano - promjena (ili bolje rečeno "zamjena") vrijednosti parametra ne mijenja vrijednost izvan metode.

Pa, koja je šala?

Zanimljivi trenuci počinju kada počnemo prosljeđivati ​​ne primitivne tipove (stringove, brojeve, datume itd.) kao parametre, već objekte. Ovo je mjesto gdje koncepti poput "plitke" i "duboke" kopije objekta dolaze u igru, kao i pokazivači (ne u C++ terminima, već kao apstraktne ručke).

Kada prosljeđujemo objekt (na primjer, tablicu vrijednosti) po referenci, prosljeđujemo samu vrijednost pokazivača (određeni handle), koji "drži" objekt u memoriji platforme. Kada se proslijedi po vrijednosti, platforma će napraviti kopiju ovog pokazivača.

Drugim riječima, ako, prosljeđujući objekt referencom, u metodi parametru dodijelimo vrijednost "Array", tada ćemo na mjestu poziva primiti niz. Ponovno dodjeljivanje vrijednosti proslijeđene referencom vidljivo je iz lokacije poziva.

Procedura ProcessValue(Parametar) Parametar = Novi niz; Tablica EndProcedure = Nova tablica vrijednosti; ProcesValue(Tablica); Izvješće(Vrstavrijednosti(Tablica)); // će ispisati polje

Ako proslijedimo objekt prema vrijednosti, tada na točki poziva naša tablica vrijednosti neće biti izgubljena.

Sadržaj i stanje objekta

Prilikom prolaska po vrijednosti, ne kopira se cijeli objekt, već samo njegov pokazivač. Instanca objekta ostaje ista. Nije važno kako prosljeđujete objekt, po referenci ili po vrijednosti - brisanjem tablice vrijednosti izbrisat ćete i samu tablicu. Ovo čišćenje će biti vidljivo posvuda, jer... postojao je samo jedan objekt i nije bilo važno kako je točno proslijeđen metodi.

Procedura ProcessValue(Parameter) Parameter.Clear(); Tablica EndProcedure = Nova tablica vrijednosti; Tablica.Dodaj(); ProcesValue(Tablica); Izvješće(Tablica.Količina()); // će ispisati 0

Prilikom prosljeđivanja objekata metodama, platforma radi s pokazivačima (uvjetnim, a ne izravnim analogijama iz C++). Ako se objekt proslijedi referencom, tada memorijska ćelija 1C virtualnog stroja u kojoj se objekt nalazi može biti prebrisana drugim objektom. Ako se objekt proslijedi prema vrijednosti, tada se pokazivač kopira i prepisivanje objekta ne rezultira prepisivanjem memorijske lokacije s izvornim objektom.

U isto vrijeme, svaka promjena država objekt (čišćenje, dodavanje svojstava, itd.) mijenja sam objekt, i nema nikakve veze s time kako i gdje je objekt prenesen. Stanje instance objekta se promijenilo; može postojati hrpa "referenci" i "vrijednosti" na njega, ali instanca je uvijek ista. Prosljeđivanjem objekta metodi ne stvaramo kopiju cijelog objekta.

I to je uvijek istina, osim...

Interakcija klijent-poslužitelj

Platforma implementira pozive poslužitelja vrlo transparentno. Jednostavno pozovemo metodu, a ispod haube platforma serijalizira (pretvara u niz) sve parametre metode, prosljeđuje ih poslužitelju, a zatim vraća izlazne parametre natrag klijentu, gdje se deserializiraju i žive kao ako nikada nisu bili ni na jednom poslužitelju.

Kao što znate, ne mogu se serijalizirati svi objekti platforme. Ovdje raste ograničenje: ne mogu se svi objekti proslijediti metodi poslužitelja s klijenta. Ako proslijedite objekt koji se ne može serijalizirati, platforma će početi koristiti ružne riječi.

  • Eksplicitna izjava programerovih namjera. Gledajući potpis metode, možete jasno reći koji su parametri ulazni, a koji izlazni. Ovaj kod je lakši za čitanje i održavanje
  • Kako bi promjena parametra “by reference” na poslužitelju bila vidljiva na pozivnoj točki na klijentu, p Sama platforma nužno će klijentu vratiti parametre proslijeđene poslužitelju putem veze kako bi osigurala ponašanje opisano na početku članka. Ako parametar ne treba vratiti, doći će do prekoračenja prometa. Za optimizaciju razmjene podataka, parametre čije nam vrijednosti nisu potrebne na izlazu treba označiti riječju Vrijednost.

Ovdje je vrijedna pažnje druga točka. Za optimizaciju prometa, platforma neće vratiti vrijednost parametra klijentu ako je parametar označen riječju Vrijednost. Sve je to super, ali dovodi do zanimljivog učinka.

Kao što sam već rekao, kada se objekt prenese na poslužitelj, dolazi do serijalizacije, tj. vrši se "duboka" kopija objekta. I ako postoji riječ Značenje objekt neće putovati od poslužitelja natrag do klijenta. Zbrojimo ove dvije činjenice i dobijemo sljedeće:

&OnServerProcedureByLink(Parameter) Parameter.Clear(); EndProcedure &OnServerProcedureByValue(parametar vrijednosti) Parameter.Clear(); EndProcedure &OnClient Procedure ByValueClient(parametar vrijednosti) Parameter.Clear(); EndProcedure &OnClient Procedure CheckValue() List1= Nove ListValues; List1.Add("zdravo"); Lista2 = Lista1.Kopiraj(); Lista3 = Lista1.Kopiraj(); // objekt se u potpunosti kopira, // prenosi na poslužitelj, a zatim vraća. // brisanje popisa vidljivo je na pozivnoj točki ByRef(List1); // objekt se u potpunosti kopira, // prenosi na poslužitelj. Ne vraća se. // Brisanje popisa NIJE VIDLJIVO u trenutku pozivanja ByValue(List2); // kopira se samo pokazivač objekta // brisanje popisa vidljivo je u točki poziva ByValueClient(List3); Izvješće(List1.Količina()); Izvješće(List2.Količina()); Izvješće(List3.Količina()); Kraj postupka

Sažetak

Ukratko, može se sažeti na sljedeći način:

  • Prijenos po referenci omogućuje vam da "prebrišete" objekt s potpuno drugačijim objektom
  • Prolaz po vrijednosti ne dopušta vam da "prebrišete" objekt, ali će promjene u unutarnjem stanju objekta biti vidljive, jer radimo s istom instancom objekta
  • Prilikom poziva poslužitelja radi se s RAZLIČITIM instancama objekta jer Izvedena je duboka kopija. Ključna riječ Značenje spriječit će instancu poslužitelja da se kopira natrag u instancu klijenta, a promjena internog stanja objekta na poslužitelju neće dovesti do slične promjene na klijentu.

Nadam se da će vam ovaj jednostavan popis pravila olakšati rješavanje sporova s ​​kolegama u vezi s prosljeđivanjem parametara "po vrijednosti" i "po referenci"