Prehod po vrednosti. Posredovanje parametrov po sklicu in po vrednosti. Privzete nastavitve

Torej, naj bo Factorial(n) funkcija za izračun faktoriala števila n. Potem, glede na to, da »vemo«, da je faktor 1 enak 1, lahko sestavimo naslednjo verigo:

Faktorial(4)=Faktorial(3)*4

Faktorial(3)=Faktorial(2)*3

Faktorial(2)=Faktorial(1)*2

Ampak, če ne bi imeli terminalskega pogoja, da ko je n=1 funkcija Factorial vrne 1, potem se taka teoretična veriga nikoli ne bi končala in to bi lahko bila napaka Call Stack Overflow - call stack overflow. Da bi razumeli, kaj je sklad klicev in kako se lahko prelije, si poglejmo rekurzivno izvedbo naše funkcije:

Faktoriel funkcije (n: celo število): LongInt;

Če je n=1, potem

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

Konec;

Kot lahko vidimo, je treba za pravilno delovanje verige pred vsakim naslednjim klicem funkcije vse lokalne spremenljivke nekam shraniti, da bo ob obračanju verige rezultat pravilen (izračunana vrednost faktoriela od n-1 pomnožimo z n). V našem primeru je treba vsakič, ko faktorialno funkcijo pokličemo iz same sebe, shraniti vse vrednosti spremenljivke n. Področje, v katerem so shranjene lokalne spremenljivke funkcije, ko se sama rekurzivno kliče, se imenuje sklad klicev. Seveda ta sklad ni neskončen in se lahko izčrpa, če so rekurzivni klici zgrajeni nepravilno. Končnost ponovitev našega primera je zagotovljena z dejstvom, da se pri n=1 klic funkcije ustavi.

Posredovanje parametrov po vrednosti in po sklicu

Do zdaj nismo mogli spremeniti vrednosti v podprogramu dejanski parameter(tj. parameter, ki je naveden ob klicu podprograma), pri nekaterih aplikacijskih nalogah pa bi bilo to priročno. Spomnimo se postopka Val, ki naenkrat spremeni vrednost dveh svojih dejanskih parametrov: prvi je parameter, kamor bo zapisana pretvorjena vrednost nizovne spremenljivke, drugi pa je parameter Code, kjer je število napačnih parametrov. znak se postavi v primeru napake med pretvorbo tipa. Tisti. še vedno obstaja mehanizem, s katerim lahko podprogram spremeni dejanske parametre. To je mogoče zaradi različnih načinov podajanja parametrov. Oglejmo si te metode podrobneje.

Programiranje v Pascalu

Posredovanje parametrov po vrednosti

V bistvu smo tako prenesli vse parametre v naše rutine. Mehanizem je sledeč: ko je določen dejanski parameter, se njegova vrednost prekopira v pomnilniško območje, kjer se nahaja podprogram, in potem, ko funkcija ali procedura zaključi svoje delo, se to področje počisti. V grobem povedano, medtem ko se podprogram izvaja, obstajata dve kopiji njegovih parametrov: ena v obsegu klicočega programa in druga v obsegu funkcije.

S to metodo podajanja parametrov potrebuje več časa za klic podprograma, saj je poleg samega klica potrebno kopirati vse vrednosti vseh dejanskih parametrov. Če je podprogramu posredovana velika količina podatkov (na primer matrika z velikim številom elementov), ​​je lahko čas, potreben za kopiranje podatkov v lokalno območje, precejšen, kar je treba upoštevati pri razvoju programov in iskanje ozkih grl pri njihovem delovanju.

Pri tem načinu prenosa podprogram ne more spremeniti dejanskih parametrov, saj bodo spremembe vplivale le na izolirano lokalno področje, ki bo sproščeno po zaključku funkcije ali postopka.

Posredovanje parametrov po sklicu

S to metodo se vrednosti dejanskih parametrov ne kopirajo v podprogram, ampak se prenesejo naslovi v pomnilniku (povezave do spremenljivk), kjer se nahajajo. V tem primeru podprogram že spreminja vrednosti, ki niso v lokalnem obsegu, zato bodo vse spremembe vidne klicnemu programu.

Za navedbo, da mora biti argument posredovan s sklicevanjem, je pred njegovo deklaracijo dodana ključna beseda var:

Procedure getTwoRandom(var n1, n2:Integer; obseg: Integer);

n1:=naključno(razpon);

n2:=naključno(razpon); konec ;

var rand1, rand2: Celo število;

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

Konec.

V tem primeru so sklicevanja na dve spremenljivki posredovana proceduri getTwoRandom kot dejanska parametra: rand1 in rand2. Tretji dejanski parameter (10) se posreduje po vrednosti. Postopek piše z uporabo formalnih parametrov

Metode programiranja z uporabo nizov

Namen laboratorijskega dela : spoznati metode v jeziku C#, pravila za delo z znakovnimi podatki in komponento ListBox. Napišite program za delo z nizi.

Metode

Metoda je element razreda, ki vsebuje programsko kodo. Metoda ima naslednjo strukturo:

[atributi] [specifikatorji] ime tipa ([parametri])

telo metode;

Atributi so posebna navodila prevajalniku o lastnostih metode. Atributi se redko uporabljajo.

Kvalifikatorji so ključne besede, ki služijo različnim namenom, na primer:

· Ugotavljanje razpoložljivosti metode za druge razrede:

o zasebno– metoda bo na voljo samo znotraj tega razreda

o zaščiten– metoda bo na voljo tudi za otroške razrede

o javnosti– metoda bo na voljo vsem drugim razredom, ki imajo dostop do tega razreda

Označevanje razpoložljivosti metode brez ustvarjanja razreda

· Vrsta nastavitve

Tip določa rezultat, ki ga vrne metoda: to je lahko kateri koli tip, ki je na voljo v C#, kot tudi ključna beseda void, če rezultat ni potreben.

Ime metode je identifikator, ki bo uporabljen za klic metode. Za identifikator veljajo enake zahteve kot za imena spremenljivk: sestavljeno je lahko iz črk, številk in podčrtaja, ne more pa se začeti s številko.

Parametri so seznam spremenljivk, ki jih je mogoče posredovati metodi ob klicu. Vsak parameter je sestavljen iz vrste in imena spremenljivke. Parametri so ločeni z vejicami.

Telo metode je običajna programska koda, le da ne more vsebovati definicij drugih metod, razredov, imenskih prostorov itd. Če mora metoda vrniti nek rezultat, mora biti ključna beseda return prisotna na koncu s povratno vrednostjo. . Če vračanje rezultatov ni potrebno, potem uporaba ključne besede return ni potrebna, čeprav je dovoljena.

Primer metode, ki ovrednoti izraz:

javni dvojni Calc (dvojni a, dvojni b, dvojni c)

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

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

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

Preobremenitev metode

Jezik C# omogoča ustvarjanje več metod z enakimi imeni, vendar različnimi parametri. Prevajalnik bo pri gradnji programa samodejno izbral najprimernejšo metodo. Na primer, lahko napišete dve ločeni metodi za dvig števila na potenco: en algoritem bi uporabili za cela števila, drugega pa za realna števila:

///

/// Izračunajte X na potenco Y za cela števila

///

zasebno int Pow(int X, int Y)

///

/// Izračunaj X na potenco Y za realna števila

///

zasebni dvojni Pow (dvojni X, dvojni Y)

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

sicer če (Y == 0)

Ta koda se kliče na enak način, razlika je le v parametrih - v prvem primeru bo prevajalnik poklical metodo Pow s celoštevilskimi parametri, v drugem pa z realnimi parametri:

Privzete nastavitve

Jezik C#, začenši z različico 4.0 (Visual Studio 2010), vam omogoča nastavitev privzetih vrednosti za nekatere parametre, tako da lahko pri klicanju metode nekatere parametre izpustite. Če želite to narediti, je treba pri izvajanju metode zahtevanim parametrom dodeliti vrednost neposredno na seznamu parametrov:

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

Console.WriteLine("Število: (0)", Število);

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

V tem primeru lahko metodo pokličete na naslednji način:

Pridobi podatke (10, 20);

V prvem primeru bo izbirni parameter enak 20, ker je izrecno določen, v drugem primeru pa bo enak 5, ker ni izrecno podana in prevajalnik vzame privzeto vrednost.

Privzete parametre je mogoče nastaviti le na desni strani seznama parametrov; na primer, prevajalnik ne bo sprejel takega podpisa metode:

private void GetData(int Izbirno = 5 , int število)

Ko so parametri posredovani metodi na običajen način (brez dodatnih ključnih besed ref in out), morebitne spremembe parametrov znotraj metode ne vplivajo na njeno vrednost v glavnem programu. Recimo, da imamo naslednjo metodo:

zasebni void Calc(int število)

Vidimo, da je znotraj metode spremenjena spremenljivka Number, ki je bila posredovana kot parameter. Poskusimo poklicati metodo:

Console.WriteLine(n);

Na zaslonu se prikaže številka 1, to pomeni, da se kljub spremembi spremenljivke v metodi Calc vrednost spremenljivke v glavnem programu ni spremenila. To je posledica dejstva, da ob klicu metode a kopirati spremenjena spremenljivka, to spremenljivko spremeni metoda. Ko se metoda zaključi, se vrednost kopij izgubi. Ta metoda posredovanja parametra se imenuje mimo vrednosti.

Da bi metoda spremenila spremenljivko, ki ji je bila posredovana, mora biti posredovana s ključno besedo ref – mora biti v podpisu metode in ob klicu:

zasebni void Calc (številka ref int)

Console.WriteLine(n);

V tem primeru se na zaslonu prikaže številka 10: sprememba vrednosti v metodi je vplivala tudi na glavni program. Ta metoda prenosa se imenuje mimo sklicevanja, tj. Ne prenaša se več kopija, temveč sklic na resnično spremenljivko v pomnilniku.

Če metoda uporablja spremenljivke s sklicevanjem samo za vrnitev vrednosti in ji ni vseeno, kaj je bilo v njih na začetku, potem takšnih spremenljivk ne morete inicializirati, ampak jih posredujete s ključno besedo out. Prevajalnik razume, da začetna vrednost spremenljivke ni pomembna in se ne pritožuje nad pomanjkanjem inicializacije:

private void Calc(out int Number)

int n; // Ničesar ne dodelimo!

podatkovni tip niz

Jezik C# uporablja vrsto nizov za shranjevanje nizov. Če želite deklarirati (in praviloma takoj inicializirati) spremenljivko niza, lahko napišete naslednjo kodo:

niz a = "Besedilo";

niz b = "nizi";

Na vrsticah lahko izvedete operacijo dodajanja - v tem primeru bo besedilo ene vrstice dodano besedilu druge:

niz c = a + " " + b; // Rezultat: besedilo niza

Vrsta niza je pravzaprav vzdevek za razred String, ki vam omogoča izvajanje številnih bolj zapletenih operacij na nizih. Na primer, metoda IndexOf lahko išče podniz v nizu, metoda Substring pa vrne del niza določene dolžine, ki se začne na določenem mestu:

niz a = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";

int index = a.IndexOf("OP"); // Rezultat: 14 (šteto od 0)

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

Če morate v niz dodati posebne znake, lahko to storite z uporabo ubežnih zaporedij, ki se začnejo s poševnico nazaj:

Komponenta ListBox

Komponenta ListBox je seznam, katerega elemente izberemo s tipkovnico ali miško. Seznam elementov določa lastnost Predmeti. Elementi so element, ki ima svoje lastnosti in svoje metode. Metode Dodaj, RemoveAt in Vstavi se uporabljajo za dodajanje, brisanje in vstavljanje elementov.

Predmet Predmeti shrani predmete na seznam. Objekt je lahko kateri koli razred - podatki razreda se pretvorijo za prikaz v predstavitev niza z metodo ToString. V našem primeru bodo nizi delovali kot objekti. Ker pa objekt Items shranjuje objekte, pretvorjene v tipski objekt, jih morate pred uporabo vrniti v prvotni tip, v našem primeru niz:

string a = (string)listBox1.Items;

Če želite določiti število izbranega elementa, uporabite lastnost SelectedIndex.

Ko sem začel programirati v C++ in intenzivno študiral knjige in članke, sem vedno naletel na isti nasvet: če moramo funkciji predati nek objekt, ki se v funkciji ne sme spreminjati, ga je treba vedno posredovati s sklicevanjem na konstanto(PPSK), razen v primerih, ko moramo posredovati primitivni tip ali strukturo, ki je po velikosti njim podobna. Ker V več kot 10 letih programiranja v C++ sem zelo pogosto naletel na ta nasvet (in tudi sam sem ga dal več kot enkrat), že dolgo sem ga "vsrkal" - vse argumente samodejno posredujem s sklicevanjem na konstanto . Toda čas teče in minilo je že 7 let, odkar imamo na voljo C++11 s semantiko premikanja, v zvezi s čimer slišim vse več glasov, ki dvomijo v dobro staro dogmo. Mnogi začenjajo trditi, da je prehod s sklicevanjem na konstanto preteklost in da je zdaj potreben mimo vrednosti(PPZ). Kaj je v ozadju teh pogovorov, pa tudi kakšne zaključke lahko iz vsega tega potegnemo, želim razpravljati v tem članku.

Knjižna modrost

Da bi razumeli, katerega pravila se moramo držati, predlagam, da se obrnemo na knjige. Knjige so odličen vir informacij, ki jih nismo dolžni sprejeti, a jim je vsekakor vredno prisluhniti. In začeli bomo z zgodovino, z izvorom. Ne bom ugotavljal, kdo je bil prvi opravičevalec PPSC, kot primer bom dal samo knjigo, ki je imela name osebno največji vpliv pri vprašanju uporabe PPSC.

Mayers

V redu, tukaj imamo razred, v katerem so vsi parametri posredovani s sklicevanjem, ali obstajajo težave s tem razredom? Na žalost obstaja in ta problem je na površini. V našem razredu imamo 2 funkcionalni entiteti: prva prevzame vrednost v fazi ustvarjanja objekta, druga pa vam omogoča, da spremenite predhodno nastavljeno vrednost. Imamo dve entiteti, a štiri funkcije. Zdaj si predstavljajte, da lahko nimamo 2 podobnih entitet, ampak 3, 5, 6, kaj potem? Potem se bomo soočili s hudo napihnjenostjo kode. Zato, da ne bi ustvarili množice funkcij, je bil predlog, da se povezave v parametrih popolnoma opustijo:

Predloga class Holder ( public: explicit Holder(T value): m_Value(move(value)) ( ) void setValue(T value) ( ​​​​m_Value = move(value); ) const T& value() const noexcept ( return m_Value; ) zasebno: T m_Vrednost; );

Prva prednost, ki takoj pade v oči, je bistveno manj kode. Še manj ga je kot v prvi različici, zaradi odstranitve const in & (čeprav so dodali move). Toda vedno so nas učili, da je podajanje po sklicu bolj produktivno kot podajanje po vrednosti! Tako je bilo pred C++11 in tako je še vedno, zdaj pa, če pogledamo to kodo, bomo videli, da tukaj ni več kopiranja kot v prvi različici, pod pogojem, da ima T konstruktor premika. Tisti. Sam PPSC je bil in bo hitrejši od PPZ, vendar koda nekako uporablja posredovano referenco in pogosto se ta argument kopira.

Vendar to še ni vsa zgodba. Za razliko od prve možnosti, kjer imamo samo kopiranje, tukaj dodamo še gibanje. Toda selitev je poceni operacija, kajne? Na to temo ima Mayersova knjiga, ki jo obravnavamo, tudi poglavje (»postavka 29«), ki ima naslov: »Predpostavimo, da operacije premikanja niso prisotne, niso poceni in se ne uporabljajo.« Glavna ideja bi morala biti jasna iz naslova, če pa želite podrobnosti, jo vsekakor preberite - ne bom se zadrževal na tem.

Tukaj bi bilo primerno opraviti popolno primerjalno analizo prve in zadnje metode, vendar ne bi želel odstopati od knjige, zato bomo analizo preložili na druge razdelke, tukaj pa bomo nadaljevali z upoštevanjem Scottovih argumentov. Torej, poleg dejstva, da je tretja možnost očitno krajša od druge, kaj Scott vidi kot prednost PPZ pred PPSC v sodobni kodi?

Vidi ga v tem, da v primeru podajanja vrednosti r, tj. nekateri kličejo takole: Holder holder(string("me")); , opcija s PPSC nam bo dala kopiranje, opcija s PPZ pa gibanje. Po drugi strani pa, če je prenos takšen: Imetnik imetnik (someLvalue); , potem PPZ zagotovo izgubi zaradi dejstva, da bo izvajal tako kopiranje kot premikanje, medtem ko bo v različici s PPSC samo eno kopiranje. Tisti. Izkazalo se je, da je PPZ, če upoštevamo čisto učinkovitost, nekakšen kompromis med količino kode in "polno" (prek && ) podporo za semantiko gibanja.

Zato je Scott tako skrbno ubesedil svoj nasvet in ga tako skrbno promovira. Zdelo se mi je celo, da je to izpostavil nejevoljno, kot pod pritiskom: ni si mogel kaj, da ne bi razprav o tej temi umestil v knjigo, saj... o tem se je razpravljalo precej široko in Scott je bil vedno zbiralec kolektivnih izkušenj. Poleg tega navaja zelo malo argumentov v obrambo PPZ, navaja pa veliko tistih, ki to »tehniko« postavljajo pod vprašaj. Njegove argumente proti si bomo ogledali v poznejših razdelkih, tukaj pa bomo na kratko ponovili argument, ki ga Scott navaja v obrambo javno-zasebnega partnerstva (miselno dodamo "če predmet podpira gibanje in je poceni"): omogoča, da se izognete kopiranju pri posredovanju izraza rvalue kot argumenta funkcije. Ampak dovolj mučne Meyersove knjige, pojdimo k drugi knjigi.

Mimogrede, če je kdo prebral knjigo in je presenečen, da tukaj ne vključim možnosti s tem, kar je Mayers imenoval univerzalne reference - zdaj znane kot reference za posredovanje - potem je to enostavno razložiti. Razmišljam samo o PPZ in PPSC, ker... Menim, da je slaba oblika uvajati funkcije predloge za metode, ki niso predloge, samo zaradi podpiranja podajanja obeh vrst (rvalue/lvalue) s sklicevanjem. Da ne omenjam dejstva, da se koda izkaže drugače (nič več konstantnosti) in s seboj prinese druge težave.

Josattis in družba

Zadnja knjiga, ki si jo bomo ogledali, je »C++ Templates«, ki je tudi najnovejša od vseh knjig, omenjenih v tem članku. Izšla je konec leta 2017 (v knjigi je navedeno leto 2018). Za razliko od drugih knjig je ta v celoti posvečena vzorcem in ne nasvetom (kot Mayers) ali C++ na splošno, kot Stroustrup. Zato so tukaj prednosti in slabosti obravnavane z vidika pisnih predlog.

Tej temi je posvečeno celotno 7. poglavje, ki ima zgovoren naslov »Po vrednosti ali po referenci?«. V tem poglavju avtorja precej na kratko, a jedrnato opisujeta vse načine prenosa z vsemi njihovimi prednostmi in slabostmi. Analiza učinkovitosti tukaj praktično ni podana in samoumevno je, da bo PPSC hitrejši od PPZ. Toda ob vsem tem avtorji na koncu poglavja priporočajo uporabo privzetega PPP za funkcije predloge. Zakaj? Ker se s povezavo parametri predloge prikažejo v celoti, brez povezave pa se »razpadejo«, kar blagodejno vpliva na obdelavo nizov in nizovnih literalov. Avtorji verjamejo, da če se za neko vrsto PPP izkaže, da je neučinkovit, potem lahko vedno uporabite std::ref in std::cref. To je nekaj nasvetov, če sem iskren, ste videli veliko ljudi, ki želijo uporabljati zgornje funkcije?

Kaj svetujejo glede PPSC? Svetujejo uporabo PPSC, ko je zmogljivost kritična ali obstajajo drugi tehten razlogov za neuporabo PPP. Seveda tukaj govorimo le o standardni kodi, vendar je ta nasvet v neposrednem nasprotju z vsem, kar so programerje učili desetletje. To ni le nasvet, da PPP obravnavate kot alternativo – ne, to je nasvet, da PPSC postane alternativa.

S tem smo zaključili naše potovanje po knjigah, ker... Ne poznam nobene druge knjige, v katero bi se morali posvetovati o tem vprašanju. Gremo naprej v drug medijski prostor.

Mrežna modrost

Ker Živimo v dobi interneta, zato se ne smete zanašati samo na knjižno modrost. Poleg tega mnogi avtorji, ki so nekoč pisali knjige, zdaj preprosto pišejo bloge in so knjige opustili. Eden od teh avtorjev je Herb Sutter, ki je maja 2013 na svojem blogu objavil članek »GotW #4 Solution: Class Mechanics«, ki se, čeprav ni povsem posvečen problematiki, ki jo obravnavamo, vendarle dotika.

Torej, v prvotni različici članka je Sutter preprosto ponovil staro modrost: "predaj parametre s sklicevanjem na konstanto," vendar te različice članka ne bomo več videli, ker Članek vsebuje nasprotni nasvet: " če parameter bo še vedno kopiran, nato pa ga posredujte po vrednosti.« Spet razvpiti "če". Zakaj je Sutter spremenil članek in kako sem vedel za to? Iz komentarjev. Preberite komentarje na njegov članek, mimogrede, bolj zanimivi in ​​uporabni so kot sam članek. Res je, po pisanju članka je Sutter končno spremenil svoje mnenje in takih nasvetov ne daje več. Spremembo mnenja lahko najdemo v njegovem govoru na CppConu leta 2014: »Nazaj k osnovam! Osnove sodobnega sloga C++". Bodite prepričani, da pogledate, prešli bomo na naslednjo internetno povezavo.

In naslednji je glavni programski vir 21. stoletja: StackOverflow. Oziroma odgovor, pri čemer je število pozitivnih odzivov v času pisanja tega članka preseglo 1700. Vprašanje je: kaj je idiom kopiraj in zamenjaj? , in kot bi moral povedati naslov, ni ravno v zvezi s temo, ki jo obravnavamo. Toda v odgovoru na to vprašanje se avtor dotakne tudi teme, ki nas zanima. Svetuje tudi uporabo PPZ, "če bo argument vseeno kopiran" (čas je, da tudi za to uvedemo okrajšavo, bognedaj). In na splošno se zdi ta nasvet povsem primeren, v okviru njegovega odgovora in tam obravnavanega operaterja, vendar si avtor dopušča, da tak nasvet poda širše in ne le v tem konkretnem primeru. Še več, gre dlje od vseh nasvetov, o katerih smo prej razpravljali, in poziva k temu, da to počnemo tudi v kodi C++03! Kaj je avtorja spodbudilo k takšnim sklepom?

Očitno je avtor odgovora črpal glavni navdih iz članka drugega avtorja knjige in honorarnega razvijalca Boost.MPL - Dave Abrahams. Članek se imenuje “Želite hitrost? Pass by Value.” , objavljena pa je bila že avgusta 2009, tj. 2 leti pred sprejetjem C++11 in uvedbo semantike premika. Kot v prejšnjih primerih priporočam, da bralec sam prebere članek, vendar bom navedel glavne argumente (pravzaprav je samo en argument), ki jih Dave navaja v prid PPZ: morate uporabiti PPZ , ker z njim dobro deluje optimizacija »preskok kopije« (copy elision), ki je v PPSC ni. Če preberete komentarje k članku, lahko vidite, da nasveti, ki jih promovira, niso univerzalni, kar potrjuje tudi avtor sam, ko odgovarja na kritike komentatorjev. Vseeno pa članek vsebuje izrecen nasvet (smernico) za uporabo PPP, če bo argument vseeno kopiran. Mimogrede, če koga zanima, si lahko prebere članek »Želite hitrost? Ne (vedno) mimo vrednosti.« . Kot bi moral povedati naslov, je ta članek odgovor na Daveov članek, tako da če ste prebrali prvega, ne pozabite prebrati tudi tega!

Na žalost (na srečo nekaterih) takšni članki in (še bolj) priljubljeni odgovori na priljubljenih straneh povzročajo množično uporabo dvomljivih tehnik (banalen primer) preprosto zato, ker to zahteva manj pisanja in stara dogma ni več neomajna - Vedno se lahko obrnete na »ta priljubljen nasvet«, če ste potisnjeni ob zid. Zdaj predlagam, da se seznanite s tem, kaj nam ponujajo različni viri s priporočili za pisanje kode.

Ker Ker so različni standardi in priporočila zdaj objavljeni tudi na spletu, sem se odločil, da ta razdelek uvrstim med »omrežne modrosti«. Torej, tukaj bi rad govoril o dveh virih, katerih namen je izboljšati kodo programerjev C++ tako, da jim zagotovi nasvete (smernice), kako napisati to kodo.

Prvi niz pravil, ki jih želim upoštevati, je bila zadnja kaplja, ki me je prisilila, da sem se lotil tega članka. Ta niz je del pripomočka clang-tidy in ne obstaja zunaj njega. Kot vse, kar je povezano s clangom, je ta pripomoček zelo priljubljen in je že prejel integracijo s CLion in Resharper C++ (tako sem naletel nanj). Torej, clang-tydy vsebuje pravilo modernize-pass-by-value, ki deluje na konstruktorjih, ki sprejemajo argumente prek PPSC. To pravilo predlaga, da PPSC zamenjamo s PPZ. Poleg tega v času pisanja članka opis tega pravila vsebuje opombo, da to pravilo adijo deluje samo za konstruktorje, vendar bodo ti (kdo so oni?) z veseljem sprejeli pomoč tistih, ki to pravilo razširijo na druge subjekte. Tam je v opisu povezava do Daveovega članka - jasno je, od kod prihajajo noge.

Za zaključek tega pregleda modrosti in verodostojnih mnenj drugih ljudi predlagam, da si ogledate uradne smernice za pisanje kode C++: C++ Core Guidelines, katerih glavna urednika sta Herb Sutter in Bjarne Stroustrup (ni slabo, kajne?). Ta priporočila torej vsebujejo naslednje pravilo: »Za parametre »in« posredujte ceneno kopirane tipe po vrednosti in druge po sklicevanju na const«, ki popolnoma ponavlja staro modrost: PPSK povsod in PPP za majhne objekte. Ta nasvet opisuje več možnosti, ki jih je treba upoštevati. v primeru, da posredovanje argumentov potrebuje optimizacijo. A PPZ ni na seznamu alternativ!

Ker nimam drugih virov, vrednih pozornosti, predlagam, da preidem na neposredno analizo obeh načinov prenosa.

Analiza

Celotno prejšnje besedilo je napisano zame nekoliko nenavadno: predstavljam tuja mnenja in se celo trudim, da ne izražam svojega (vem, da se slabo izide). Predvsem zaradi dejstva, da so mnenja drugih, in moj cilj je bil, da jih na kratko pregledam, sem odložil podrobno obravnavo nekaterih trditev, ki sem jih našel pri drugih avtorjih. V tem delu se ne bom skliceval na avtoritete in podajal mnenj, tukaj si bomo ogledali nekaj objektivnih prednosti in slabosti PPSC in PPZ, ki bodo začinjene z mojo subjektivno percepcijo. Seveda se bo nekaj od tega, o čemer smo razpravljali prej, ponovilo, toda, žal, to je struktura tega članka.

Ali ima JZP prednost?

Torej, preden preučimo argumente za in proti, predlagam, da pogledamo, kaj in v katerih primerih nam daje prednost mimo vrednosti. Recimo, da imamo razred, kot je ta:

Razred 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); ) );

Čeprav nas za namene tega članka zanimata le prvi dve funkciji, sem vključil štiri možnosti samo zato, da jih uporabim kot kontrast.

Razred Accounter je preprost razred, ki šteje, kolikokrat je bil kopiran/premaknjen. In v razredu CopyMover smo implementirali funkcije, ki nam omogočajo, da upoštevamo naslednje možnosti:

    premikanje prestal argument.

    Predaj po vrednosti, ki ji sledi kopiranje prestal argument.

Zdaj, če vsaki od teh funkcij posredujemo lvalue, na primer takole:

Računovodja byRefer; Računovodja po vrednosti; Accounter byValuerAndNotMover; CopyMover copyMover; copyMover.setByRefer(byRefer); copyMover.setByValuer(byValuer); copyMover.setByValuerAndNotMover(byValuerAndNotMover);

potem dobimo naslednje rezultate:

Očitni zmagovalec je PPSC, ker... daje samo en izvod, PPZ pa en izvod in eno potezo.

Zdaj pa poskusimo posredovati rvalue:

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

Dobimo naslednje:

Tukaj ni jasnega zmagovalca, ker ... tako PPZ kot PPSK imata po eno operacijo, a glede na to, da PPZ uporablja gibanje, PPSK pa kopiranje, lahko prepustimo zmago PPZ.

Toda naši poskusi se tu ne končajo; dodajmo naslednje funkcije za simulacijo posrednega klica (z naknadnim posredovanjem argumentov):

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

Uporabili jih bomo popolnoma enako kot brez njih, zato kode ne bom ponavljal (po potrebi poglejte v repozitorij). Torej za lvalue bi bili rezultati takšni:

Upoštevajte, da PPSC poveča razkorak s PPZ in ostane pri eni sami kopiji, medtem ko ima PPZ že kar 3 operacije (eno gibanje več)!

Zdaj posredujemo rvalue in dobimo naslednje rezultate:

Zdaj ima PPZ 2 gibanja, PPSC pa še vedno en izvod. Ali je zdaj možno predlagati PPZ za zmagovalca? Ne, ker če ena poteza ne bi smela biti vsaj nič slabša od ene kopije, tega ne moremo reči za 2 potezi. Zato v tem primeru ne bo zmagovalca.

Lahko mi ugovarjajo: »Avtor, imate pristransko mnenje in vlečete tisto, kar vam koristi. Tudi 2 potezi bosta cenejši od kopiranja!« S to trditvijo se ne morem strinjati Glede na vse, Ker Koliko je selitev hitrejša od kopiranja, je odvisno od posameznega razreda, vendar si bomo "poceni" selitev ogledali v ločenem razdelku.

Tukaj smo se dotaknili zanimivosti: dodali smo en posredni klic, PPP pa je dodal natanko eno operacijo v "teži". Mislim, da vam ni treba imeti diplome MSTU, da razumete, da več kot imamo posrednih klicev, več operacij bo opravljenih pri uporabi PPZ, pri PPSC pa bo številka ostala nespremenjena.

Vse zgoraj obravnavano verjetno ni bilo razodetje za nikogar, morda niti nismo izvedli poskusov - vse te številke bi morale biti večini programerjev C++ očitne na prvi pogled. Res je, ena točka si še vedno zasluži pojasnilo: zakaj v primeru rvalue PZ nima kopije (ali druge poteze), ampak samo eno potezo.

No, pogledali smo razliko v prenosu med PPZ in PPSC tako, da smo iz prve roke opazovali število kopij in potez. Čeprav je očitno, da je prednost PPZ pred PPSC tudi v tako preprostih primerih milo rečeno ne Očitno še vedno, malce pretenciozno, sklepam: če bomo še vedno kopirali argument funkcije, potem je smiselno razmisliti o posredovanju argumenta funkciji po vrednosti. Zakaj sem prišel do tega zaključka? Za gladek prehod na naslednji razdelek.

Če kopiramo ...

Tako smo prišli do pregovornega "če". Večina argumentov, na katere smo naleteli, ni zahtevala univerzalne implementacije PPP namesto PPSC; pozvali so le k temu, "če je argument vseeno kopiran." Čas je, da ugotovimo, kaj je narobe s tem argumentom.

Začeti želim z majhnim opisom, kako pišem kodo. V zadnjem času je moj proces kodiranja vedno bolj podoben TDD, tj. pisanje katere koli metode razreda se začne s pisanjem testa, v katerem se ta metoda pojavi. V skladu s tem, ko začnem pisati test in ustvarim metodo po pisanju testa, še vedno ne vem, ali bom kopiral argument. Seveda niso vse funkcije ustvarjene na ta način, pogosto že med pisanjem testa točno veš, kakšna implementacija bo. Vendar se to ne zgodi vedno!

Morda mi bo kdo ugovarjal, da ni pomembno, kako je bila metoda prvotno napisana, lahko spremenimo način podajanja argumenta, ko je metoda že oblikovana in nam je popolnoma jasno, kaj se tam dogaja (tj. ali imamo kopiranje oz. ne ). Delno se strinjam s tem - res lahko to storite na ta način, vendar nas to vključuje v nekakšno čudno igro, kjer moramo spremeniti vmesnike samo zato, ker se je implementacija spremenila. Kar nas pripelje do naslednje dileme.

Izkazalo se je, da spreminjamo (ali celo načrtujemo) vmesnik glede na to, kako bo implementiran. Nimam se za strokovnjaka za OOP in druge teoretične izračune programske arhitekture, vendar so takšna dejanja očitno v nasprotju z osnovnimi pravili, ko implementacija ne bi smela vplivati ​​na vmesnik. Seveda nekatere izvedbene podrobnosti (ne glede na to, ali gre za značilnosti jezika ali ciljne platforme) tako ali drugače še vedno uhajajo skozi vmesnik, vendar morate poskušati zmanjšati, ne povečati števila takih stvari.

No, Bog ga blagoslovi, pojdimo po tej poti in še vedno spreminjamo vmesnike glede na to, kaj počnemo v implementaciji v smislu kopiranja argumenta. Recimo, da smo napisali to metodo:

Void setName(Ime imena) ( m_Name = premakni(ime); )

in potrdil naše spremembe v repozitorij. Sčasoma je naš programski izdelek pridobil nove funkcionalnosti, nova ogrodja so bila integrirana in pojavila se je naloga obveščanja zunanjega sveta o spremembah v našem razredu. Tisti. Naši metodi bomo dodali nekaj mehanizma obveščanja, naj bo nekaj podobnega signalom Qt:

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

Ali obstaja težava s to kodo? Jejte. Za vsak klic setName pošljemo signal, tako da bo signal poslan tudi, ko pomen m_Name se ni spremenilo. Poleg težav z zmogljivostjo lahko ta situacija privede do neskončne zanke zaradi kode, ki prejme zgornje obvestilo, nekako pokliče setName. Da bi se izognili vsem tem težavam, takšne metode najpogosteje izgledajo nekako takole:

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

Zgoraj opisanih težav smo se znebili, zdaj pa je naše pravilo “če vseeno kopiramo ...” odpovedalo - ni več brezpogojnega kopiranja argumenta, zdaj ga kopiramo le, če se spremeni! Torej, kaj naj storimo zdaj? Spremeniti vmesnik? V redu, spremenimo vmesnik razreda zaradi tega popravka. Kaj pa, če bi naš razred to metodo podedoval iz nekega abstraktnega vmesnika? Spremenimo ga tudi tam! Ali je veliko sprememb, ker se je spremenila izvedba?

Spet mi lahko ugovarjajo, pravijo, avtor, zakaj poskušate prihraniti denar na tekmah, ko bo ta pogoj deloval tam? Da, večina klicev bo lažnih! Je kaj zaupanja v to? Kje? In če sem se odločil varčevati pri vžigalicah, ali ni bilo dejstvo, da smo uporabljali PPZ, posledica ravno takšnega varčevanja? Samo nadaljujem »strankarsko linijo«, ki zagovarja učinkovitost.

Konstruktorji

Na kratko preletimo konstruktorje, še posebej, ker zanje obstaja posebno pravilo v clang-tidy, ki za druge metode/funkcije še ne deluje. Recimo, da imamo razred, kot je ta:

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

Očitno je parameter kopiran in clang-tidy nam bo povedal, da bi bilo dobro prepisati konstruktor na to:

JustClass(niz justString): m_JustString(premakni(justString)) ( )

In, odkrito povedano, tukaj mi je težko trditi - navsezadnje res vedno kopiramo. In največkrat, ko nekaj predamo skozi konstruktor, to kopiramo. Vendar pogosteje ne pomeni vedno. Tu je še en primer:

Razred TimeSpan (javno: TimeSpan(DateTime začetek, DateTime end) ( if(start > end) throw InvalidTimeSpan(); m_Start = move(start); m_End = move(end); ) private: DateTime m_Start; DateTime m_End; );

Tukaj ne prepisujemo vedno, ampak samo takrat, ko so datumi pravilno predstavljeni. Seveda bo v veliki večini primerov tako. Ampak ni vedno.

Lahko navedete še en primer, vendar tokrat brez kode. Predstavljajte si, da imate razred, ki sprejme velik predmet. Razred obstaja že dolgo časa in zdaj je čas, da posodobimo njegovo izvedbo. Zavedamo se, da ne potrebujemo več kot polovico velikega objekta (ki je z leti zrasel), morda celo manj. Ali lahko kaj storimo glede tega s prehodom po vrednosti? Ne, ne bomo mogli storiti ničesar, ker bo še vedno ustvarjena kopija. Toda če bi uporabili PPSC, bi preprosto spremenili, kar počnemo znotraj oblikovalec. In to je ključno: z uporabo PPSC nadziramo, kaj in kdaj se zgodi v implementaciji naše funkcije (konstruktorja), če pa uporabimo PPZ, potem izgubimo kakršen koli nadzor nad kopiranjem.

Kaj lahko odnesete iz tega razdelka? Dejstvo, da je argument "če vseeno kopiramo ..." zelo sporen, ker Ne vemo vedno, kaj bomo kopirali, in tudi ko vemo, pogosto nismo prepričani, da se bo to nadaljevalo tudi v prihodnosti.

Selitev je poceni

Že od trenutka, ko se je pojavila semantika gibanja, je začela resno vplivati ​​na način pisanja sodobne kode C++ in sčasoma se je ta vpliv samo še stopnjeval: ni čudno, saj je gibanje tako poceni v primerjavi s kopiranjem. Ampak ali je? Ali je res, da je gibanje Nenehno poceni operacija? To bomo poskušali ugotoviti v tem razdelku.

Binarni veliki objekt

Začnimo s trivialnim primerom, recimo, da imamo naslednji razred:

Struct Blob (std::array podatki; );

Vsakdanji madež(BDO, angleško BLOB), ki se lahko uporablja v različnih situacijah. Poglejmo, koliko nas bo stalo prehod po sklicu in vrednosti. Naš BDO bo uporabljen nekako takole:

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

In te funkcije bomo imenovali takole:

Const Blob blob(); shranjevanje; shranjevanje.setBlobByRef(blob); shranjevanje.setBlobByVal(blob);

Koda za druge primere bo enaka tej, le z drugačnimi imeni in vrstami, zato je ne bom navedel za preostale primere - vse je v repozitoriju.

Preden preidemo na meritve, poskusimo napovedati rezultat. Torej imamo 4 KB veliko std::matriko, ki jo želimo shraniti v objekt razreda Storage. Kot smo že izvedeli, bomo za PPSC imeli en izvod, za PPZ pa en izvod in eno potezo. Glede na dejstvo, da je matrike nemogoče premakniti, bosta 2 kopiji za PPZ in ena za PPSC. Tisti. lahko pričakujemo dvakratno superiornost v zmogljivosti za PPSC.

Zdaj pa si oglejmo rezultate testa:

Ta in vsi nadaljnji preizkusi so bili izvedeni na istem računalniku z uporabo MSVS 2017 (15.7.2) in zastavice /O2.

Praksa je sovpadala s predpostavko - prehod po vrednosti je 2-krat dražji, ker je za matriko premikanje popolnoma enakovredno kopiranju.

Linija

Poglejmo še en primer, običajni std::string. Kaj lahko pričakujemo? Vemo (o tem sem razpravljal v članku), da sodobne izvedbe razlikujejo med dvema vrstama nizov: kratkimi (približno 16 znakov) in dolgimi (tistimi, ki so daljši od kratkih). Za kratke se uporablja notranji medpomnilnik, ki je običajna C-matrika char, dolgi pa bodo že postavljeni na kopico. Kratke vrstice nas ne zanimajo, ker... tam bo rezultat enak kot pri BDO, zato se osredotočimo na dolge vrstice.

Torej, če imate dolgo vrvico, je očitno, da bi moralo biti premikanje precej poceni (samo premaknite kazalec), tako da lahko računate na dejstvo, da premikanje vrvice sploh ne bi smelo vplivati ​​na rezultate, PPZ pa bi moral dati rezultat nič slabši od PPSC. Preverimo v praksi in dobimo naslednje rezultate:

Prešli bomo na razlago tega "fenomena". Kaj se torej zgodi, ko kopiramo obstoječi niz v že obstoječi niz? Poglejmo trivialni primer:

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

Imamo dva 64-mestna niza, zato notranji medpomnilnik pri njunem ustvarjanju ni zadosten, zaradi česar sta oba niza dodeljena na kopico. Zdaj kopiramo prvo na drugo. Ker naše velikosti vrstic so enake, očitno je v drugi dovolj prostora za vse podatke iz prve, tako da je druga = prva; bo banalen memcpy, nič drugega. Če pa pogledamo nekoliko spremenjen primer:

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

potem ne bo več klica operator= , ampak bo poklican konstruktor kopiranja. Ker Ker imamo opravka s konstruktorjem, v njem ni obstoječega pomnilnika. Najprej ga je treba izbrati in šele nato najprej kopirati. Tisti. to je dodelitev pomnilnika in nato memcpy. Kot vi in ​​jaz vemo, je dodeljevanje pomnilnika na globalnem kupu običajno draga operacija, zato bo kopiranje iz drugega primera dražje od kopiranja iz prvega. Dražja dodelitev pomnilnika kopice.

Kaj ima to opraviti z našo temo? Najbolj neposreden, saj prvi primer natančno prikazuje, kaj se zgodi s PPSC, drugi pa, kaj se zgodi s PPZ: za PPZ se vedno ustvari nova vrstica, medtem ko se za PPSC ponovno uporabi obstoječa. Razliko v času izvajanja ste že videli, zato tukaj ni kaj dodati.

Tudi tu se soočamo z dejstvom, da pri uporabi javno-zasebnega partnerstva delujemo izven konteksta in zato ne moremo izkoristiti vseh prednosti, ki jih lahko nudi. In če smo prej razmišljali v smislu teoretičnih prihodnjih sprememb, tukaj opažamo zelo konkreten neuspeh v produktivnosti.

Seveda bi mi lahko kdo ugovarjal, da niz stoji ločeno, večina tipov pa ne deluje tako. Na kar lahko odgovorim naslednje: vse prej opisano bo veljalo za vsak vsebnik, ki pomnilnik v kupu dodeli takoj za paket elementov. Poleg tega, kdo ve, katere druge kontekstno občutljive optimizacije se uporabljajo v drugih vrstah?

Kaj bi morali vzeti iz tega razdelka? Dejstvo, da tudi če je premikanje res poceni, ne pomeni, da bo zamenjava kopiranja s kopiranjem+premiskanjem vedno dala rezultat, ki je primerljiv glede na zmogljivost.

Kompleksni tip

Nazadnje si poglejmo tip, ki bo sestavljen iz več predmetov. Naj bo to razred Oseba, ki je sestavljen iz podatkov, ki so lastni osebi. Običajno je to vaše ime, priimek, poštna številka itd. Vse to lahko predstavite kot nize in domnevate, da bodo nizi, ki jih vnesete v polja razreda Person, verjetno kratki. Čeprav verjamem, da bo v resničnem življenju najbolj uporabno merjenje kratkih vrvic, si bomo vseeno ogledali vrvice različnih velikosti, da bomo dobili popolnejšo sliko.

Uporabil bom tudi Osebo z 10 polji, vendar za to ne bom ustvaril 10 polj neposredno v telesu razreda. Izvedba Person skriva vsebnik v svojih globinah - zaradi tega je bolj priročno spreminjati testne parametre, praktično brez odstopanja od tega, kako bi delovalo, če bi bil Person pravi razred. Vendar je izvedba na voljo in vedno lahko preverite kodo in mi poveste, če sem naredil kaj narobe.

Pa pojdimo: Oseba z 10 polji tipa string , ki jih s pomočjo PPSC in PPZ prenesemo v Storage :

Kot lahko vidite, imamo ogromno razliko v zmogljivosti, kar bralcev po prejšnjih razdelkih ne bi smelo presenetiti. Prav tako verjamem, da je razred Oseba dovolj "resničen", da takih rezultatov ne bomo zavrgli kot abstraktne.

Mimogrede, ko sem pripravljal ta članek, sem pripravil še en primer: razred, ki uporablja več objektov std::function. Po moji zamisli naj bi pokazal tudi prednost v izvedbi PPSC pred PPZ, a se je izkazalo ravno obratno! Vendar tega primera tukaj ne navajam zato, ker mi rezultati niso bili všeč, ampak zato, ker nisem imel časa ugotoviti, zakaj so bili doseženi takšni rezultati. Kljub temu je koda v repozitoriju (Tiskalniki), testi - tudi, če se kdo želi pozanimati, bom vesel rezultatov raziskave. K temu primeru se nameravam vrniti pozneje in če teh rezultatov nihče ne bo objavil pred mano, jih bom obravnaval v ločenem članku.

Rezultati

Tako smo si ogledali različne prednosti in slabosti podajanja po vrednosti in sklicevanja na konstanto. Ogledali smo si nekaj primerov in pogledali uspešnost obeh metod v teh primerih. Seveda ta članek ne more in ni izčrpen, vendar po mojem mnenju vsebuje dovolj informacij za neodvisno in informirano odločitev o tem, katero metodo je najbolje uporabiti. Nekdo lahko ugovarja: "zakaj uporabljati eno metodo, začnimo z nalogo!" Čeprav se na splošno strinjam s to tezo, se v tej situaciji z njo ne strinjam. Verjamem, da lahko obstaja samo en način posredovanja argumentov v jeziku, ki se uporablja privzeto.

Kaj pomeni privzeto? To pomeni, da ko pišem funkcijo, ne razmišljam o tem, kako naj posredujem argument, ampak samo uporabim "privzeto". Jezik C++ je precej zapleten jezik, ki se ga mnogi izogibajo. In po mojem mnenju zapletenost ne povzroča toliko kompleksnost jezikovnih konstruktov, ki obstajajo v jeziku (tipičen programer se morda nikoli ne sreča z njimi), temveč dejstvo, da jezik povzroči veliko razmišljanje: ali sem osvobodil up memory, ali je drago uporabljati to funkcijo tukaj? in tako naprej.

Mnogi programerji (C, C++ in drugi) so nezaupljivi in ​​se bojijo C++, ki se je začel pojavljati po letu 2011. Slišal sem veliko kritik, da jezik postaja vse bolj zapleten, da lahko zdaj v njem pišejo samo "guruji" itd. Osebno menim, da temu ni tako – ravno nasprotno, komisija veliko časa posveča temu, da bi bil jezik bolj prijazen začetnikom in da bi morali programerji manj razmišljati o lastnostih jezika. Konec koncev, če se nam ni treba boriti z jezikom, potem imamo čas za razmislek o nalogi. Te poenostavitve vključujejo pametne kazalce, lambda funkcije in še veliko več, kar se je pojavilo v jeziku. Hkrati ne zanikam dejstva, da se moramo zdaj več učiti, ampak kaj je narobe s študijem? Ali pa se v drugih priljubljenih jezikih ne dogajajo spremembe, ki bi se jih morali naučiti?

Poleg tega ne dvomim, da se bodo našli snobi, ki bodo lahko odgovorili: »Nočeš razmišljati? Potem pojdi pisati v PHP.” Takim ljudem niti ne želim odgovarjati. Navedel bom samo primer iz resničnosti igre: v prvem delu Starcrafta, ko je bil nov delavec ustvarjen v zgradbi, je bilo treba, da je začel pridobivati ​​minerale (ali plin), tja poslati ročno. Poleg tega je imela vsaka embalaža mineralov mejo, ob doseganju katere je bilo povečanje delavcev neuporabno in so lahko celo posegali drug v drugega in poslabšali proizvodnjo. To je bilo spremenjeno v Starcraftu 2: delavci samodejno začnejo rudariti minerale (ali plin), poleg tega pa je prikazano, koliko delavcev trenutno rudari in kolikšna je omejitev tega nahajališča. To je zelo poenostavilo igralčevo interakcijo z bazo in mu omogočilo, da se je osredotočil na pomembnejše vidike igre: gradnjo baze, kopičenje vojakov in uničenje sovražnika. Zdi se, da je to le odlična inovacija, toda kaj se je začelo na internetu! Ljudje (kdo so oni?) so začeli kričati, da je igra "zajebana" in "ubili so Starcraft." Očitno so takšna sporočila lahko prišla le od »varuhov skrivnega znanja« in »adepov visokega APM«, ki so bili radi v kakšnem »elitnem« klubu.

Torej, če se vrnem k naši temi, manj ko moram razmišljati o tem, kako napisati kodo, več časa imam za razmišljanje o rešitvi takojšnje težave. Razmišljanje o tem, katero metodo naj uporabim - PPSC ali PPZ - me niti za kanček ne približa rešitvi problema, zato preprosto nočem razmišljati o takšnih stvareh in izberem eno možnost: prehod s sklicevanjem na konstanto. Zakaj? Ker v splošnih primerih za JZP ne vidim prednosti, posebne primere pa je treba obravnavati posebej.

Gre za poseben primer, ker sem opazil, da se pri neki metodi PPSC izkaže za ozko grlo in da bomo s spremembo prenosa v PPZ dosegli pomembno povečanje zmogljivosti, ne oklevam uporabiti PPZ. Toda privzeto bom uporabil PPSC tako v običajnih funkcijah kot v konstruktorjih. In če bo mogoče, bom promoviral to metodo, kjer koli bo to mogoče. Zakaj? Ker se mi zdi praksa promocije javno-zasebnega partnerstva zlobna zaradi dejstva, da levji delež programerjev nima velikega znanja (bodisi načeloma ali pa preprosto še ni prišel v bistvo) in enostavno sledi nasvetom. Poleg tega, če je več nasprotujočih si nasvetov, izberejo tistega, ki je enostavnejši, in to vodi v pesimizem v kodeksu preprosto zato, ker je nekdo nekje nekaj slišal. Aja, ta nekdo lahko da tudi povezavo do Abrahamsovega članka, da dokaže, da ima prav. In potem sediš, bereš kodo in razmišljaš: ali je dejstvo, da je parameter tukaj posredovan po vrednosti, zato, ker je programer, ki je to napisal, prišel iz Jave, samo prebral veliko "pametnih" člankov, ali je res potrebna tehnična specifikacija?

PPSC je veliko lažje brati: oseba jasno pozna "dobro obliko" C++ in gremo naprej - pogled se ne zadržuje. Programerje C++ so programerje uporabe PPSC učili že leta. Kaj je razlog, da so ga opustili? To me pripelje do drugega zaključka: če vmesnik metode uporablja PPP, potem mora obstajati tudi komentar, zakaj je temu tako. V drugih primerih je treba uporabiti PPSC. Seveda obstajajo vrste izjem, vendar jih tukaj ne omenjam preprosto zato, ker so implicirane: string_view, initializer_list, različni iteratorji itd. Toda to so izjeme, katerih seznam se lahko razširi glede na vrste, ki se uporabljajo v projektu. Toda bistvo ostaja enako od C++98: privzeto vedno uporabljamo PPCS.

Za std::string najverjetneje ne bo nobene razlike pri majhnih nizih, o tem bomo govorili kasneje.

Vnaprej se opravičujem za pretenciozno opombo o "postavljanju točk", vendar vas moramo nekako zvabiti v članek)) Z moje strani bom poskušal zagotoviti, da povzetek še vedno izpolnjuje vaša pričakovanja.

Na kratko o čem govorimo

To že vsi vedo, a na začetku vas bom spomnil, kako je mogoče posredovati parametre metode v 1C. Lahko se posredujejo "po sklicu" ali "po vrednosti". V prvem primeru posredujemo metodi enako vrednost kot na točki klica, v drugem pa njeno kopijo.

Privzeto se v 1C argumenti posredujejo po sklicu, spremembe parametra znotraj metode pa bodo vidne zunaj metode. Tu je nadaljnje razumevanje vprašanja odvisno od tega, kaj točno razumete pod besedo "sprememba parametra". To torej pomeni prerazporeditev in nič drugega. Poleg tega je lahko dodelitev implicitna, na primer klic metode platforme, ki vrne nekaj v izhodnem parametru.

Če pa ne želimo, da se naš parameter posreduje po sklicu, lahko pred parametrom podamo ključno besedo Pomen

Procedura ByValue(parameter vrednosti) Parameter = 2; Parameter EndProcedure = 1; PoVrednosti(Parameter); Poročilo (parameter); // bo natisnil 1

Vse deluje kot obljubljeno - spreminjanje (ali bolje rečeno "zamenjava") vrednosti parametra ne spremeni vrednosti zunaj metode.

No, kaj je hec?

Zanimivi trenutki se začnejo, ko začnemo posredovati ne primitivne tipe (nize, številke, datume itd.) kot parametre, ampak objekte. Tu pridejo v poštev koncepti, kot so "plitve" in "globoke" kopije predmeta, pa tudi kazalci (ne v izrazih C++, ampak kot abstraktni ročaji).

Ko posredujemo predmet (na primer tabelo vrednosti) po sklicu, posredujemo samo vrednost kazalca (določen ročaj), ki "drži" predmet v pomnilniku platforme. Ko bo podana po vrednosti, bo platforma naredila kopijo tega kazalca.

Z drugimi besedami, če pri posredovanju objekta po sklicu v metodi parametru dodelimo vrednost »Matrika«, potem bomo na točki klica prejeli matriko. Ponovna dodelitev vrednosti, posredovane s sklicevanjem, je vidna z lokacije klica.

Procedura ProcessValue(Parameter) Parameter = Nova matrika; Tabela EndProcedure = Nova tabela vrednosti; ProcesValue(Tabela); Poročilo(VrstaVrednosti(Tabela)); // bo izpisal Array

Če podamo predmet po vrednosti, potem na točki klica naša tabela vrednosti ne bo izgubljena.

Vsebina in stanje objekta

Pri podajanju po vrednosti se ne kopira celoten objekt, temveč le njegov kazalec. Primerek predmeta ostane enak. Ni pomembno, kako posredujete objekt, po sklicu ali po vrednosti – s čiščenjem tabele vrednosti se počisti tabela sama. To čiščenje bo vidno povsod, saj... obstajal je samo en objekt in ni bilo pomembno, kako natančno je bil posredovan metodi.

Procedura ProcessValue(Parameter) Parameter.Clear(); Tabela EndProcedure = Nova tabela vrednosti; Table.Add(); ProcesValue(Tabela); Poročilo(Tabela.Količina()); // bo izpisal 0

Pri posredovanju objektov metodam platforma deluje s kazalci (pogojni, ne neposredni analogi iz C++). Če se predmet posreduje po sklicu, lahko pomnilniško celico virtualnega stroja 1C, v kateri je predmet, prepiše drug objekt. Če je predmet posredovan z vrednostjo, se kazalec kopira in prepisovanje objekta ne povzroči prepisovanja pomnilniške lokacije z izvirnim objektom.

Hkrati pa vsaka sprememba država predmet (čiščenje, dodajanje lastnosti itd.) spremeni sam objekt in nima prav nobene zveze s tem, kako in kam je bil predmet prenesen. Stanje primerka objekta se je spremenilo; nanj je lahko kup "referenc" in "vrednosti", vendar je primerek vedno isti. S posredovanjem predmeta metodi ne ustvarimo kopije celotnega objekta.

In to je vedno res, razen ...

Interakcija med odjemalcem in strežnikom

Platforma izvaja strežniške klice zelo pregledno. Preprosto pokličemo metodo in pod pokrovom platforma serializira (pretvori v niz) vse parametre metode, jih posreduje strežniku in nato vrne izhodne parametre nazaj odjemalcu, kjer so deserializirani in živijo kot če nikoli niso bili na nobenem strežniku.

Kot veste, vseh predmetov platforme ni mogoče serializirati. Tukaj raste omejitev: vseh objektov ni mogoče posredovati metodi strežnika iz odjemalca. Če posredujete predmet, ki ga ni mogoče serializirati, bo platforma začela uporabljati slabe besede.

  • Izrecna izjava programerjevih namenov. Če pogledate podpis metode, lahko jasno ugotovite, kateri parametri so vhodni in kateri izhodni. To kodo je lažje brati in vzdrževati
  • Da bi bila sprememba parametra »by reference« na strežniku vidna na klicni točki na odjemalcu, p Sama platforma bo odjemalcu nujno vrnila parametre, posredovane strežniku prek povezave, da bi zagotovila vedenje, opisano na začetku članka. Če parametra ni treba vrniti, bo prišlo do prekoračitve prometa. Za optimizacijo izmenjave podatkov je treba parametre, katerih vrednosti ne potrebujemo na izhodu, označiti z besedo Vrednost.

Tu je pomembna druga točka. Za optimizacijo prometa platforma odjemalcu ne bo vrnila vrednosti parametra, če je parameter označen z besedo Vrednost. Vse to je super, vendar vodi do zanimivega učinka.

Kot sem že rekel, ko se objekt prenese na strežnik, pride do serializacije, tj. izvede se "globoka" kopija objekta. In če obstaja beseda Pomen objekt ne bo potoval od strežnika nazaj do odjemalca. Seštejemo ti dve dejstvi in ​​dobimo naslednje:

&OnServerProcedureByLink(Parameter) Parameter.Clear(); EndProcedure &OnServerProcedureByValue(parameter vrednosti) Parameter.Clear(); EndProcedure &OnClient Procedure ByValueClient(parameter vrednosti) Parameter.Clear(); EndProcedure &OnClient Procedure CheckValue() List1= Nove vrednosti seznama; List1.Add("zdravo"); Seznam2 = Seznam1.Kopiraj(); Seznam3 = Seznam1.Kopiraj(); // predmet je v celoti kopiran, // prenesen na strežnik in nato vrnjen. // čiščenje seznama je vidno na klicni točki ByRef(List1); // predmet je v celoti kopiran, // prenesen na strežnik. Ne vrne se. // Čiščenje seznama NI VIDNO na točki klica ByValue(List2); // kopira se samo kazalec objekta // čiščenje seznama je vidno na točki klica ByValueClient(List3); Poročilo(Seznam1.Količina()); Poročilo(Seznam2.Količina()); Poročilo(Seznam3.Količina()); Konec postopka

Povzetek

Na kratko lahko povzamemo takole:

  • Prenos po sklicu vam omogoča, da predmet "prepišete" s popolnoma drugačnim objektom
  • Prehod po vrednosti vam ne omogoča "prepisovanja" predmeta, vendar bodo vidne spremembe v notranjem stanju predmeta, ker delamo z istim primerkom predmeta
  • Pri klicu strežnika se delo izvaja z RAZLIČNIMI primerki objekta, ker Izvedena je bila globoka kopija. Ključna beseda Pomen bo preprečil, da bi se instanca strežnika kopirala nazaj v instanco odjemalca, sprememba notranjega stanja objekta na strežniku pa ne bo povzročila podobne spremembe na odjemalcu.

Upam, da vam bo ta preprost seznam pravil olajšal reševanje sporov s kolegi glede podajanja parametrov "po vrednosti" in "po sklicu"