Kuinka syöttää satunnaisluku C:hen. Satunnaislukugeneraattori

Terveisiä kaikille, jotka pysähtyivät. Tämä lyhyt huomautus sisältää muutaman sanan näennäissatunnaisten lukujen luomisesta C/C++:ssa. Erityisesti siitä, kuinka työskennellä yksinkertaisimman sukupolven funktion - rand() - kanssa.

rand()-funktio

Löytyy C++-standardikirjastosta (stdlib.h). Luo ja palauttaa näennäissatunnaisen luvun välillä 0 - RAND_MAX. Tämä vakio voi vaihdella kääntäjän tai prosessorin arkkitehtuurista riippuen, mutta se on yleensä etumerkittömän int-tietotyypin maksimiarvo. Se ei hyväksy parametreja eikä ole koskaan hyväksynyt.

Jotta sukupolvi tapahtuisi, sinun on asetettava siemen käyttämällä aputoimintoa samasta kirjastosta - srand(). Se ottaa luvun ja asettaa sen satunnaisluvun luomisen aloituspisteeksi. Jos siementä ei ole asetettu, saamme aina ohjelman käynnistyessä sama satunnaisia ​​numeroita. Ilmeisin ratkaisu on lähettää nykyinen järjestelmäaika sinne. Tämä voidaan tehdä käyttämällä aika(NULL)-funktiota. Tämä toiminto on time.h-kirjastossa.

Olemme selvittäneet teorian, löytäneet kaikki satunnaislukujen generointifunktiot, nyt generoidaan ne.

Esimerkki satunnaislukujen generointifunktion rand() käytöstä

#sisältää #sisältää #sisältää käyttäen nimiavaruutta std; int main() ( srand(aika(NULL)); for(int i = 0; i< 10; i++) { cout << rand() << endl; } return 0; }

Rand loi meille 10 satunnaislukua. Voit varmistaa, että ne ovat satunnaisia ​​suorittamalla ohjelman uudelleen ja uudelleen. Mutta tämä ei riitä, joten meidän on puhuttava etäisyydestä.

Aseta alueen rajat arvolle rand()

Luodaksesi luvun välillä A–B, sinun on kirjoitettava tämä:

A + rand() % ((B + 1) - A);

Raja-arvot voivat olla myös negatiivisia, minkä avulla voit luoda negatiivisen satunnaisluvun, katsotaanpa esimerkkiä.

#sisältää #sisältää #sisältää käyttäen nimiavaruutta std; int main() ( srand(aika(NULL)); int A = -2; int B = 8; for(int i = 0; i< 100; i++) { cout << A + rand() % ((B + 1) - A) << endl; } return 0; }

Artikkelin käännös Jon Skeeten satunnaiset luvut, jotka tunnetaan laajalti kapeissa piireissä. Pysähdyin tähän artikkeliin, koska törmäsin itsekin siinä kuvattuun ongelmaan.

Selataan aiheita mukaan .NETTO Ja C# StackOverflow-verkkosivustolla voit nähdä lukemattomia kysymyksiä, joissa mainitaan sana "random", jotka itse asiassa herättävät saman ikuisen ja "tuhoutumattoman" kysymyksen: miksi System.Random satunnaislukugeneraattori "ei toimi" ja miten " korjaa se"" Tämä artikkeli on omistettu tämän ongelman pohtimiseen ja sen ratkaisemiseen.

Ongelman muotoilu

StackOverflow:ssa, uutisryhmissä ja postituslistoissa, kaikki "satunnaista"-aihetta koskevat kysymykset kuulostavat tältä:
Käytän Random.Nextiä useiden satunnaislukujen luomiseen, mutta menetelmä palauttaa saman numeron, kun sitä kutsutaan useita kertoja. Numero muuttuu aina, kun sovellus käynnistetään, mutta yhden ohjelman suorituksen aikana se on vakio.

Esimerkkikoodi on jotain tämän kaltaista:
// Huono koodi! Älä käytä! for (int i = 0; i< 100; i++) { Console.WriteLine(GenerateDigit()); } ... static int GenerateDigit() { Random rng = new Random(); // Предположим, что здесь много логики return rng.Next(10); }
Joten mikä tässä on vialla?

Selitys

Random-luokka ei ole todellinen satunnaislukugeneraattori, se sisältää generaattorin pseudo satunnaisia ​​numeroita. Jokainen Random-luokan esiintymä sisältää jonkin sisäisen tilan, ja kun Next (tai NextDouble tai NextBytes) -metodia kutsutaan, menetelmä käyttää tätä tilaa palauttaakseen satunnaisena näkyvän luvun. Sisäistä tilaa muutetaan sitten niin, että seuraavan kerran, kun Next kutsutaan, se palauttaa erilaisen näennäisesti satunnaisen luvun kuin aiemmin palautettu.

Kaikki Random-luokan "sisäiset". täysin deterministinen. Tämä tarkoittaa, että jos otat useita esiintymiä Random-luokasta samalla alkutilalla, joka määritetään konstruktoriparametrin kautta siemen, ja kutsu jokaiselle tapaukselle tiettyjä menetelmiä samassa järjestyksessä ja samoilla parametreilla, niin lopulta saat samat tulokset.

Joten mikä vika yllä olevassa koodissa on? Huono asia on, että käytämme uutta Random-luokan esiintymää silmukan jokaisessa iteraatiossa. Random-konstruktori, joka ei ota parametreja, ottaa siemenensä nykyisen päivämäärän ja kellonajan. Iteraatiot silmukassa "vierivät" niin nopeasti, että järjestelmän aika "ei ehdi muuttua" niiden valmistuttua; siten kaikki Random-instanssit saavat saman arvon kuin niiden alkutila ja palauttavat siksi saman näennäissatunnaisen luvun.

Kuinka korjata se?

Ongelmaan on monia ratkaisuja, joista jokaisella on omat hyvät ja huonot puolensa. Tarkastellaan muutamia niistä.
Kryptografisen satunnaislukugeneraattorin käyttäminen
.NET sisältää abstraktin luokan RandomNumberGenerator, josta kaikkien kryptografisten satunnaislukugeneraattoreiden (jäljempänä kryptoRNG:t) on perittävä. .NET sisältää myös yhden näistä toteutuksista - täytä RNGCryptoServiceProvider-luokka. Krypto-RNG:n ideana on, että vaikka se on edelleen näennäissatunnaislukugeneraattori, se tarjoaa melko vahvan tulosten arvaamattomuuden. RNGCryptoServiceProvider käyttää useita entropialähteitä, jotka ovat pohjimmiltaan "kohinaa" tietokoneessasi, ja sen luomaa numerosarjaa on erittäin vaikea ennustaa. Lisäksi "tietokoneen sisäistä" kohinaa voidaan käyttää ei vain alkutilana, vaan myös seuraaviin satunnaisnumeroihin soitettujen puhelujen välillä; Näin ollen vaikka luokan nykyinen tila olisi tiedossa, ei riitä laskemaan sekä seuraavat tulevaisuudessa syntyneet että aiemmin luodut luvut. Itse asiassa tarkka käyttäytyminen riippuu toteutuksesta. Lisäksi Windows voi käyttää erikoislaitteistoa, joka on "todellisen satunnaisuuden" lähde (esimerkiksi radioaktiivisen isotoopin hajoamisanturi) luodakseen entistä turvallisempia ja luotettavampia satunnaislukuja.

Verrataan tätä aiemmin käsiteltyyn Random-luokkaan. Oletetaan, että soitit Random.Next(100) kymmenen kertaa ja tallensit tulokset. Jos sinulla on tarpeeksi laskentatehoa, voit pelkästään näiden tulosten perusteella laskea alkutilan (siemen), jolla Random-instanssi luotiin, ennustaa Random.Next(100)-kutsun seuraavat tulokset ja jopa laskea tulokset aikaisemmat menetelmäkutsut. Tämä käyttäytyminen on erittäin mahdotonta hyväksyä, jos käytät satunnaisia ​​lukuja turvallisuuteen, taloudellisiin tarkoituksiin jne. Crypto RNG:t toimivat huomattavasti hitaammin kuin Random-luokka, mutta ne luovat numerosarjan, joista jokainen on riippumattomampi ja arvaamattomampi muiden arvoista.

Useimmissa tapauksissa huono suorituskyky ei ole sopimusten katkaisija - huono API on. RandomNumberGenerator on suunniteltu luomaan tavuja - siinä kaikki. Vertaa tätä Random-luokan menetelmiin, joissa on mahdollista saada satunnainen kokonaisluku, murtoluku ja myös joukko tavuja. Toinen hyödyllinen ominaisuus on kyky saada satunnaisluku tietyllä alueella. Vertaa näitä mahdollisuuksia RandomNumberGeneratorin tuottamaan satunnaisten tavujen joukkoon. Voit korjata tilanteen luomalla oman kääreen (wrapper) RandomNumberGeneratorin ympärille, joka muuntaa satunnaiset tavut "käteväksi" tulokseksi, mutta tämä ratkaisu ei ole triviaali.

Useimmissa tapauksissa Random-luokan "heikkous" on kuitenkin hyvä, jos pystyt ratkaisemaan artikkelin alussa kuvatun ongelman. Katsotaan mitä voimme tehdä täällä.

Käytä yhtä Random-luokan esiintymää useisiin puheluihin
Tässä se on, ongelman ratkaisun juuri on käyttää vain yhtä Random-instanssia luotaessa useita satunnaislukuja käyttämällä Random.Next. Ja se on hyvin yksinkertaista - katso, kuinka voit muuttaa yllä olevan koodin:
// Tämä koodi on parempi Random rng = new Random(); for (int i = 0; i< 100; i++) { Console.WriteLine(GenerateDigit(rng)); } ... static int GenerateDigit(Random rng) { // Предположим, что здесь много логики return rng.Next(10); }
Nyt jokaisella iteraatiolla on eri numerot... mutta siinä ei vielä kaikki. Mitä tapahtuu, jos kutsumme tätä koodilohkoa kahdesti peräkkäin? Aivan oikein, luomme kaksi satunnaista esiintymää samalla siemenellä ja saamme kaksi identtistä satunnaislukujoukkoa. Jokaisen sarjan numerot ovat erilaisia, mutta nämä joukot ovat keskenään samansuuruisia.

On kaksi tapaa ratkaista ongelma. Ensinnäkin emme voi käyttää ilmentymää, vaan staattista kenttää, joka sisältää sattumanvaraisen esiintymän, ja sitten yllä oleva koodinpätkä luo vain yhden ilmentymän ja käyttää sitä kutsuen sitä niin monta kertaa kuin on tarpeen. Toiseksi, voimme kokonaan poistaa satunnaisen ilmentymän luomisen sieltä siirtämällä sen "korkeammaksi", mieluiten ohjelman "huipulle", jossa luodaan yksi satunnainen ilmentymä, jonka jälkeen se lähetetään kaikkiin paikkoihin. jossa tarvitaan satunnaislukuja. Tämä on hieno idea, joka ilmaistaan ​​kauniisti riippuvuuksilla, mutta se toimii niin kauan kuin käytämme vain yhtä säiettä.

Langan turvallisuus

Random-luokka ei ole lankaturvallinen. Ottaen huomioon, kuinka paljon haluamme luoda yksittäistä ilmentymää ja käyttää sitä läpi ohjelman koko sen suoritusajan (singleton, hei!), lankojen turvallisuuden puutteesta tulee todellinen tuska. Loppujen lopuksi, jos käytämme yhtä esiintymää samanaikaisesti useissa säikeissä, on mahdollista, että sen sisäinen tila nollataan, ja jos näin tapahtuu, siitä hetkestä lähtien ilmentymä tulee hyödyttömäksi.

Jälleen on kaksi tapaa ratkaista ongelma. Ensimmäinen polku sisältää edelleen yhden esiintymän käytön, mutta tällä kertaa resurssien lukituksen näytön kautta. Tätä varten sinun on luotava satunnaisen ympärille kääre, joka kääriä menetelmiensä kutsut lukituslauseeseen, mikä takaa soittajalle eksklusiivisen pääsyn esiintymään. Tämä polku on huono, koska se heikentää suorituskykyä säikeintensiivisissä skenaarioissa.

Toinen tapa, jonka kuvailen alla, on käyttää yhtä esiintymää säiettä kohti. Ainoa asia, joka meidän on varmistettava, on, että käytämme erilaisia ​​​​siemeniä luodessasi ilmentymiä, joten emme voi käyttää oletuskonstruktoreita. Muuten tämä tie on suhteellisen suoraviivainen.

Turvallinen palveluntarjoaja

Onneksi uusi yleinen luokka ThreadLocal .NET 4:ssä käyttöön otetun palvelun avulla on erittäin helppoa kirjoittaa palveluntarjoajia, jotka tarjoavat yhden esiintymän säiettä kohti. Sinun tarvitsee vain välittää edustaja ThreadLocal-konstruktorille, joka viittaa itse instanssimme arvon saamiseen. Tässä tapauksessa päätin käyttää yhtä siemenarvoa, alustaen sen Environment.TickCountilla (tämä on täsmälleen, miten parametriton Random-konstruktori toimii). Seuraavaksi tuloksena olevaa merkkien määrää kasvatetaan aina, kun meidän on hankittava uusi satunnainen esiintymä erilliselle säikeelle.

Alla oleva luokka on täysin staattinen ja sisältää vain yhden julkisen (avoin) menetelmän GetThreadRandom. Tästä menetelmästä on tehty pikemminkin menetelmä kuin ominaisuus, lähinnä mukavuussyistä: tämä varmistaa, että kaikki luokat, jotka tarvitsevat Random-esiintymän, riippuvat Funcista (Delegaatti, joka osoittaa menetelmään, joka ei ota parametreja ja palauttaa Random-tyypin arvon), eikä itse Random-luokasta. Jos tyyppi on tarkoitettu ajamaan yhdessä säikeessä, se voi kutsua edustajaa hankkimaan yhden Random-instanssin ja käyttää sitä sitten koko ajan; jos tyypin on toimittava monisäikeisissä skenaarioissa, se voi kutsua edustajaa aina, kun se tarvitsee satunnaislukugeneraattorin. Alla oleva luokka luo niin monta Random-luokan esiintymää kuin on säiettä, ja jokainen esiintymä alkaa eri alkuarvosta. Jos joudumme käyttämään satunnaislukutoimittajaa riippuvuutena muissa tyypeissä, voimme tehdä tämän: new TypeThatNeedsRandom(RandomProvider.GetThreadRandom) . No, tässä itse koodi:
käyttämällä järjestelmää; käyttäen System.Threading; julkinen staattinen luokka RandomProvider ( yksityinen staattinen int siemen = Environment.TickCount; yksityinen staattinen ThreadLocal randomWrapper = uusi ThreadLocal (() => new Random(Interlocked.Increment(ref seed))); public static Satunnainen GetThreadRandom() ( palauttaa randomWrapper.Value; ) )
Tarpeeksi yksinkertainen, eikö? Tämä johtuu siitä, että kaikki koodi on tarkoitettu tuottamaan oikea Random-esiintymä. Kun ilmentymä on luotu ja palautettu, sillä ei ole väliä, mitä teet sille seuraavaksi: kaikki muut ilmentymien julkaisut ovat täysin riippumattomia nykyisestä. Tietysti asiakaskoodissa on porsaanreikä haitallista väärinkäyttöä varten: se voi ottaa yhden Random-instanssin ja siirtää sen muille säikeille sen sijaan, että kutsuisi RandomProvider-palveluamme näissä muissa säikeissä.

Käyttöliittymän suunnitteluongelmat

Yksi ongelma on edelleen olemassa: käytämme heikosti suojattua satunnaislukugeneraattoria. Kuten aiemmin mainittiin, RandomNumberGeneratorissa on paljon turvallisempi versio RNG:stä, jonka toteutus on RNGCryptoServiceProvider-luokassa. Sen API on kuitenkin melko vaikea käyttää vakioskenaarioissa.

Olisi erittäin mukavaa, jos RNG-palveluntarjoajilla olisi erilliset "satunnaisuuden lähteet". Tässä tapauksessa meillä voisi olla yksi, yksinkertainen ja kätevä API, jota sekä epävarma mutta nopea toteutus että turvallinen mutta hidas toteutus tukevat. No, unelmoinnista ei ole haittaa. Ehkä samanlaisia ​​toimintoja tulee näkyviin tuleviin .NET Frameworkin versioihin. Ehkä joku muu kuin Microsoft tarjoaa oman sovittimen toteutuksensa. (Valitettavasti en ole se henkilö...sellaisen oikein toteuttaminen on yllättävän monimutkaista.) Voit myös luoda oman luokan johtamalla Randomista ja ohittamalla Sample- ja NextBytes-metodit, mutta ei ole selvää miten niiden pitäisi työ tai jopa oma toteutusnäyte voi olla paljon monimutkaisempi kuin miltä näyttää. Ehkä ensi kerralla…

Keskeytä AdBlock tällä sivustolla.

Joskus voi olla tarpeen luoda satunnaislukuja. Yksinkertainen esimerkki.

Esimerkki: Voittajan määrittäminen uudelleenpostituskilpailussa.

Listassa on 53 henkilöä. Voittaja on valittava heistä. Jos valitset sen itse, sinua voidaan syyttää puolueellisuudesta. Joten päätät kirjoittaa ohjelman. Se toimii seuraavasti. Syötät osallistujamäärän N, jonka jälkeen ohjelma näyttää yhden numeron - voittajan numeron.

Tiedät jo kuinka saada numero pelaajalta. Mutta kuinka voit pakottaa tietokoneen ajattelemaan satunnaislukua? Tällä oppitunnilla opit tekemään tämän.

rand()-funktio.

Tämä funktio palauttaa satunnaisen kokonaisluvun välillä nolla - RAND_MAX. RAND_MAX on erityinen C-vakio, joka sisältää suurimman kokonaisluvun arvon, jonka rand()-funktio voi palauttaa.

Funktio rand() on määritelty stdlib.h-otsikkotiedostossa. Siksi, jos haluat käyttää randia ohjelmassasi, älä unohda sisällyttää tähän otsikkotiedostoon. RAND_MAX-vakio on myös määritelty tässä tiedostossa. Löydät tämän tiedoston tietokoneeltasi ja näet sen merkityksen.

Katsotaanpa tätä ominaisuutta toiminnassa. Suoritetaan seuraava koodi:

Listaus 1.

#sisältää // käyttääksesi printf-funktiota #include // käyttääksesi rand-funktiota int main(void) ( /* luo viisi satunnaista kokonaislukua */ printf("%d\n", rand()); printf("%d\n", rand()); printf ("%d\n", rand()); printf("%d\n", rand()); printf("%d\n", rand()); )

Sen pitäisi näyttää jotain tältä.

Kuva 1 Viisi rand-funktion generoimaa satunnaislukua

Mutta haluaisimme saada numeroita 1 - 53, emmekä kaikkea peräkkäin. Tässä on muutamia temppuja, jotka auttavat sinua rajoittamaan rand()-funktiota.

Rajoita satunnaislukuja ylhäältä.

Jokainen, joka odotti koulussa hetkeä, jolloin matematiikasta tulee apua, valmistautukaa. Hetki on koittanut. Rajataksesi satunnaislukuja ylhäältä voit käyttää operaatiota jakojäännösten saamiseksi, jonka opit viimeisellä oppitunnilla. Tiedät varmaan, että luvuilla K jaon jäännös on aina pienempi kuin luku K. Esimerkiksi jakaminen 4:llä voi johtaa jäännöksiin 0, 1, 2 ja 3. Siksi, jos haluat rajoittaa satunnaislukuja ylhäältä numeroon K, ota yksinkertaisesti K:llä jaon loppuosa. Kuten tämä:

Listaus 2.

#sisältää #sisältää int main(void) ( /* luo viisi satunnaista kokonaislukua alle 100 */ printf("%d\n", rand()%100); printf("%d\n", rand()%100); printf ("%d\n", rand()%100); printf("%d\n", rand()%100); printf("%d\n", rand()%100); )


Kuva 2 Viisi satunnaislukua alle 100

Rajoita alla olevia numeroita.

Rand-funktio palauttaa satunnaislukuja väliltä. Entä jos tarvitsemme vain lukuja, jotka ovat suurempia kuin M (esimerkiksi 1000)? Mitä minun pitäisi tehdä? Se on yksinkertaista. Lisätään vain arvomme M siihen, minkä rand-funktio palautti. Sitten jos funktio palauttaa 0, lopullinen vastaus on M, jos 2394, niin lopullinen vastaus on M + 2394. Tällä toiminnolla näytämme siirtävän kaikkia lukuja eteenpäin M yksiköllä.

Aseta rand-funktion ylä- ja alaraja.

Hanki esimerkiksi numerot väliltä 80-100. Näyttää siltä, ​​​​että sinun tarvitsee vain yhdistää kaksi yllä olevaa menetelmää. Saamme jotain tällaista:

Listaus 3.

#sisältää #sisältää int main(void) ( /* luo viisi satunnaista kokonaislukua, jotka ovat suurempia kuin 80 ja alle 100 */ printf("%d\n", 80 + rand()%100); printf("%d\n", 80 + rand ()%100); printf("%d\n", 80 + rand()%100); printf("%d\n", 80 + rand()%100); printf("%d\n" " , 80 + rand()%100);)

Kokeile suorittaa tämä ohjelma. Yllättynyt?

Kyllä, tämä menetelmä ei toimi. Suoritetaan tämä ohjelma käsin nähdäksemme, teimmekö virheen. Oletetaan, että rand() palautti luvun 143. Loput jaettuna 100:lla on 43. Sitten 80 + 43 = 123. Tämä menetelmä ei siis toimi. Samanlainen malli tuottaa numeroita 80:stä 179:ään.

Puretaan ilmaisumme askel askeleelta. rand()%100 voi palauttaa lukuja väliltä 0–99. Nuo. segmentistä.
Operation + 80 siirtää segmenttiämme 80 yksikköä oikealle. Saamme.
Kuten näette, ongelmamme on segmentin oikeassa reunassa; sitä on siirretty oikealle 79 yksikköä. Tämä on alkuperäinen numeromme 80 miinus 1. Selvitetään asiat ja siirretään oikeaa reunaa taaksepäin: 80 + rand()%(100 - 80 + 1) . Sitten kaiken pitäisi toimia niin kuin pitää.

Yleensä, jos meidän on saatava numeroita segmentistä, meidän on käytettävä seuraavaa rakennetta:
A + rand()%(B-A+1) .

Tämän kaavan mukaan kirjoitamme uudelleen viimeisimmän ohjelmamme:

Listaus 4.

#sisältää #sisältää int main(void) ( /* luo viisi satunnaista kokonaislukua segmentistä */ printf("%d\n", 80 + rand()%(100 - 80 + 1)); printf("%d\n", 80 + rand()%(100 - 79)); printf("%d\n", 80 + rand()%21); printf("%d\n", 80 + rand()%21); printf ("%d\n", 80 + rand()%21); )

Tulos:


Kuva 3 Satunnaislukuja alueelta

No, nyt voit ratkaista oppitunnin alkuperäisen ongelman. Luo luku segmentistä. Vai etkö osaa?

Mutta ensin muutama hyödyllinen tieto. Suorita viimeinen ohjelma kolme kertaa peräkkäin ja kirjoita muistiin sen luomat satunnaisluvut. Huomasitko?

Funktio srand().

Kyllä, samat numerot näkyvät joka kerta. "Niin niin generaattori!" - sinä sanot. Etkä ole täysin oikeassa. Itse asiassa samoja numeroita syntyy koko ajan. Mutta voimme vaikuttaa tähän käyttämällä srand()-funktiota, joka on myös määritelty stdlib.h-otsikkotiedostossa. Se alustaa satunnaislukugeneraattorin siemennumerolla.

Käännä ja suorita tämä ohjelma useita kertoja:

Listaus 5.

#sisältää #sisältää int main(void) ( srand(2); /* luo viisi satunnaista kokonaislukua segmentistä */ printf("%d\n", 80 + rand()%(100 - 80 + 1)); printf("% d\n", 80 + rand()%(100 - 79)); printf("%d\n", 80 + rand()%21); printf("%d\n", 80 + rand() %21); printf("%d\n", 80 + rand()%21); )

Muuta nyt srand()-funktion argumentti toiseen numeroon (toivottavasti et ole unohtanut, mikä funktion argumentti on?) ja käännä ja suorita ohjelma uudelleen. Numerosarjan on muututtava. Heti kun muutamme argumenttia srand-funktiossa, myös järjestys muuttuu. Ei kovin käytännöllistä, vai mitä? Jos haluat muuttaa järjestystä, sinun on käännettävä ohjelma uudelleen. Kunpa tämä numero lisättäisiin sinne automaattisesti.

Ja se voidaan tehdä. Käytetään esimerkiksi time()-funktiota, joka on määritelty time.h-otsikkotiedostossa. Tämä funktio palauttaa sekuntien määrän, joka on kulunut 1. tammikuuta 1970, jos NULL välitetään argumenttina. Tässä on katsaus, miten se on tehty.

Listaus 6.

#sisältää #sisältää #sisältää // käyttääksesi time()-funktiota int main(void) ( srand(time(NULL)); /* luo viisi satunnaista kokonaislukua segmentistä */ printf("%d\n", 80 + rand()%( 100 - 80 + 1)); printf("%d\n", 80 + rand()%(100 - 79)); printf("%d\n", 80 + rand()%21); printf( " %d\n", 80 + rand()%21); printf("%d\n", 80 + rand()%21); )

Saatat kysyä, mikä on NULL? Asiallinen kysymys. Sillä välin kerron sinulle, mikä tämä erityinen varattu sana on. Voin myös sanoa mitä nollaosoitin tarkoittaa, mutta... Tämä ei anna sinulle mitään tietoa, joten suosittelen olemaan ajattelematta sitä tällä hetkellä. Ja muista se vain eräänlaisena fiksuna temppuna. Tulevilla tunneilla tarkastelemme tätä asiaa tarkemmin.

Artikkelin käännös Jon Skeeten satunnaiset luvut, jotka tunnetaan laajalti kapeissa piireissä. Pysähdyin tähän artikkeliin, koska törmäsin itsekin siinä kuvattuun ongelmaan.

Selataan aiheita mukaan .NETTO Ja C# StackOverflow-verkkosivustolla voit nähdä lukemattomia kysymyksiä, joissa mainitaan sana "random", jotka itse asiassa herättävät saman ikuisen ja "tuhoutumattoman" kysymyksen: miksi System.Random satunnaislukugeneraattori "ei toimi" ja miten " korjaa se"" Tämä artikkeli on omistettu tämän ongelman pohtimiseen ja sen ratkaisemiseen.

Ongelman muotoilu

StackOverflow:ssa, uutisryhmissä ja postituslistoissa, kaikki "satunnaista"-aihetta koskevat kysymykset kuulostavat tältä:
Käytän Random.Nextiä useiden satunnaislukujen luomiseen, mutta menetelmä palauttaa saman numeron, kun sitä kutsutaan useita kertoja. Numero muuttuu aina, kun sovellus käynnistetään, mutta yhden ohjelman suorituksen aikana se on vakio.

Esimerkkikoodi on jotain tämän kaltaista:
// Huono koodi! Älä käytä! for (int i = 0; i< 100; i++) { Console.WriteLine(GenerateDigit()); } ... static int GenerateDigit() { Random rng = new Random(); // Предположим, что здесь много логики return rng.Next(10); }
Joten mikä tässä on vialla?

Selitys

Random-luokka ei ole todellinen satunnaislukugeneraattori, se sisältää generaattorin pseudo satunnaisia ​​numeroita. Jokainen Random-luokan esiintymä sisältää jonkin sisäisen tilan, ja kun Next (tai NextDouble tai NextBytes) -metodia kutsutaan, menetelmä käyttää tätä tilaa palauttaakseen satunnaisena näkyvän luvun. Sisäistä tilaa muutetaan sitten niin, että seuraavan kerran, kun Next kutsutaan, se palauttaa erilaisen näennäisesti satunnaisen luvun kuin aiemmin palautettu.

Kaikki Random-luokan "sisäiset". täysin deterministinen. Tämä tarkoittaa, että jos otat useita esiintymiä Random-luokasta samalla alkutilalla, joka määritetään konstruktoriparametrin kautta siemen, ja kutsu jokaiselle tapaukselle tiettyjä menetelmiä samassa järjestyksessä ja samoilla parametreilla, niin lopulta saat samat tulokset.

Joten mikä vika yllä olevassa koodissa on? Huono asia on, että käytämme uutta Random-luokan esiintymää silmukan jokaisessa iteraatiossa. Random-konstruktori, joka ei ota parametreja, ottaa siemenensä nykyisen päivämäärän ja kellonajan. Iteraatiot silmukassa "vierivät" niin nopeasti, että järjestelmän aika "ei ehdi muuttua" niiden valmistuttua; siten kaikki Random-instanssit saavat saman arvon kuin niiden alkutila ja palauttavat siksi saman näennäissatunnaisen luvun.

Kuinka korjata se?

Ongelmaan on monia ratkaisuja, joista jokaisella on omat hyvät ja huonot puolensa. Tarkastellaan muutamia niistä.
Kryptografisen satunnaislukugeneraattorin käyttäminen
.NET sisältää abstraktin luokan RandomNumberGenerator, josta kaikkien kryptografisten satunnaislukugeneraattoreiden (jäljempänä kryptoRNG:t) on perittävä. .NET sisältää myös yhden näistä toteutuksista - täytä RNGCryptoServiceProvider-luokka. Krypto-RNG:n ideana on, että vaikka se on edelleen näennäissatunnaislukugeneraattori, se tarjoaa melko vahvan tulosten arvaamattomuuden. RNGCryptoServiceProvider käyttää useita entropialähteitä, jotka ovat pohjimmiltaan "kohinaa" tietokoneessasi, ja sen luomaa numerosarjaa on erittäin vaikea ennustaa. Lisäksi "tietokoneen sisäistä" kohinaa voidaan käyttää ei vain alkutilana, vaan myös seuraaviin satunnaisnumeroihin soitettujen puhelujen välillä; Näin ollen vaikka luokan nykyinen tila olisi tiedossa, ei riitä laskemaan sekä seuraavat tulevaisuudessa syntyneet että aiemmin luodut luvut. Itse asiassa tarkka käyttäytyminen riippuu toteutuksesta. Lisäksi Windows voi käyttää erikoislaitteistoa, joka on "todellisen satunnaisuuden" lähde (esimerkiksi radioaktiivisen isotoopin hajoamisanturi) luodakseen entistä turvallisempia ja luotettavampia satunnaislukuja.

Verrataan tätä aiemmin käsiteltyyn Random-luokkaan. Oletetaan, että soitit Random.Next(100) kymmenen kertaa ja tallensit tulokset. Jos sinulla on tarpeeksi laskentatehoa, voit pelkästään näiden tulosten perusteella laskea alkutilan (siemen), jolla Random-instanssi luotiin, ennustaa Random.Next(100)-kutsun seuraavat tulokset ja jopa laskea tulokset aikaisemmat menetelmäkutsut. Tämä käyttäytyminen on erittäin mahdotonta hyväksyä, jos käytät satunnaisia ​​lukuja turvallisuuteen, taloudellisiin tarkoituksiin jne. Crypto RNG:t toimivat huomattavasti hitaammin kuin Random-luokka, mutta ne luovat numerosarjan, joista jokainen on riippumattomampi ja arvaamattomampi muiden arvoista.

Useimmissa tapauksissa huono suorituskyky ei ole sopimusten katkaisija - huono API on. RandomNumberGenerator on suunniteltu luomaan tavuja - siinä kaikki. Vertaa tätä Random-luokan menetelmiin, joissa on mahdollista saada satunnainen kokonaisluku, murtoluku ja myös joukko tavuja. Toinen hyödyllinen ominaisuus on kyky saada satunnaisluku tietyllä alueella. Vertaa näitä mahdollisuuksia RandomNumberGeneratorin tuottamaan satunnaisten tavujen joukkoon. Voit korjata tilanteen luomalla oman kääreen (wrapper) RandomNumberGeneratorin ympärille, joka muuntaa satunnaiset tavut "käteväksi" tulokseksi, mutta tämä ratkaisu ei ole triviaali.

Useimmissa tapauksissa Random-luokan "heikkous" on kuitenkin hyvä, jos pystyt ratkaisemaan artikkelin alussa kuvatun ongelman. Katsotaan mitä voimme tehdä täällä.

Käytä yhtä Random-luokan esiintymää useisiin puheluihin
Tässä se on, ongelman ratkaisun juuri on käyttää vain yhtä Random-instanssia luotaessa useita satunnaislukuja käyttämällä Random.Next. Ja se on hyvin yksinkertaista - katso, kuinka voit muuttaa yllä olevan koodin:
// Tämä koodi on parempi Random rng = new Random(); for (int i = 0; i< 100; i++) { Console.WriteLine(GenerateDigit(rng)); } ... static int GenerateDigit(Random rng) { // Предположим, что здесь много логики return rng.Next(10); }
Nyt jokaisella iteraatiolla on eri numerot... mutta siinä ei vielä kaikki. Mitä tapahtuu, jos kutsumme tätä koodilohkoa kahdesti peräkkäin? Aivan oikein, luomme kaksi satunnaista esiintymää samalla siemenellä ja saamme kaksi identtistä satunnaislukujoukkoa. Jokaisen sarjan numerot ovat erilaisia, mutta nämä joukot ovat keskenään samansuuruisia.

On kaksi tapaa ratkaista ongelma. Ensinnäkin emme voi käyttää ilmentymää, vaan staattista kenttää, joka sisältää sattumanvaraisen esiintymän, ja sitten yllä oleva koodinpätkä luo vain yhden ilmentymän ja käyttää sitä kutsuen sitä niin monta kertaa kuin on tarpeen. Toiseksi, voimme kokonaan poistaa satunnaisen ilmentymän luomisen sieltä siirtämällä sen "korkeammaksi", mieluiten ohjelman "huipulle", jossa luodaan yksi satunnainen ilmentymä, jonka jälkeen se lähetetään kaikkiin paikkoihin. jossa tarvitaan satunnaislukuja. Tämä on hieno idea, joka ilmaistaan ​​kauniisti riippuvuuksilla, mutta se toimii niin kauan kuin käytämme vain yhtä säiettä.

Langan turvallisuus

Random-luokka ei ole lankaturvallinen. Ottaen huomioon, kuinka paljon haluamme luoda yksittäistä ilmentymää ja käyttää sitä läpi ohjelman koko sen suoritusajan (singleton, hei!), lankojen turvallisuuden puutteesta tulee todellinen tuska. Loppujen lopuksi, jos käytämme yhtä esiintymää samanaikaisesti useissa säikeissä, on mahdollista, että sen sisäinen tila nollataan, ja jos näin tapahtuu, siitä hetkestä lähtien ilmentymä tulee hyödyttömäksi.

Jälleen on kaksi tapaa ratkaista ongelma. Ensimmäinen polku sisältää edelleen yhden esiintymän käytön, mutta tällä kertaa resurssien lukituksen näytön kautta. Tätä varten sinun on luotava satunnaisen ympärille kääre, joka kääriä menetelmiensä kutsut lukituslauseeseen, mikä takaa soittajalle eksklusiivisen pääsyn esiintymään. Tämä polku on huono, koska se heikentää suorituskykyä säikeintensiivisissä skenaarioissa.

Toinen tapa, jonka kuvailen alla, on käyttää yhtä esiintymää säiettä kohti. Ainoa asia, joka meidän on varmistettava, on, että käytämme erilaisia ​​​​siemeniä luodessasi ilmentymiä, joten emme voi käyttää oletuskonstruktoreita. Muuten tämä tie on suhteellisen suoraviivainen.

Turvallinen palveluntarjoaja

Onneksi uusi yleinen luokka ThreadLocal .NET 4:ssä käyttöön otetun palvelun avulla on erittäin helppoa kirjoittaa palveluntarjoajia, jotka tarjoavat yhden esiintymän säiettä kohti. Sinun tarvitsee vain välittää edustaja ThreadLocal-konstruktorille, joka viittaa itse instanssimme arvon saamiseen. Tässä tapauksessa päätin käyttää yhtä siemenarvoa, alustaen sen Environment.TickCountilla (tämä on täsmälleen, miten parametriton Random-konstruktori toimii). Seuraavaksi tuloksena olevaa merkkien määrää kasvatetaan aina, kun meidän on hankittava uusi satunnainen esiintymä erilliselle säikeelle.

Alla oleva luokka on täysin staattinen ja sisältää vain yhden julkisen (avoin) menetelmän GetThreadRandom. Tästä menetelmästä on tehty pikemminkin menetelmä kuin ominaisuus, lähinnä mukavuussyistä: tämä varmistaa, että kaikki luokat, jotka tarvitsevat Random-esiintymän, riippuvat Funcista (Delegaatti, joka osoittaa menetelmään, joka ei ota parametreja ja palauttaa Random-tyypin arvon), eikä itse Random-luokasta. Jos tyyppi on tarkoitettu ajamaan yhdessä säikeessä, se voi kutsua edustajaa hankkimaan yhden Random-instanssin ja käyttää sitä sitten koko ajan; jos tyypin on toimittava monisäikeisissä skenaarioissa, se voi kutsua edustajaa aina, kun se tarvitsee satunnaislukugeneraattorin. Alla oleva luokka luo niin monta Random-luokan esiintymää kuin on säiettä, ja jokainen esiintymä alkaa eri alkuarvosta. Jos joudumme käyttämään satunnaislukutoimittajaa riippuvuutena muissa tyypeissä, voimme tehdä tämän: new TypeThatNeedsRandom(RandomProvider.GetThreadRandom) . No, tässä itse koodi:
käyttämällä järjestelmää; käyttäen System.Threading; julkinen staattinen luokka RandomProvider ( yksityinen staattinen int siemen = Environment.TickCount; yksityinen staattinen ThreadLocal randomWrapper = uusi ThreadLocal (() => new Random(Interlocked.Increment(ref seed))); public static Satunnainen GetThreadRandom() ( palauttaa randomWrapper.Value; ) )
Tarpeeksi yksinkertainen, eikö? Tämä johtuu siitä, että kaikki koodi on tarkoitettu tuottamaan oikea Random-esiintymä. Kun ilmentymä on luotu ja palautettu, sillä ei ole väliä, mitä teet sille seuraavaksi: kaikki muut ilmentymien julkaisut ovat täysin riippumattomia nykyisestä. Tietysti asiakaskoodissa on porsaanreikä haitallista väärinkäyttöä varten: se voi ottaa yhden Random-instanssin ja siirtää sen muille säikeille sen sijaan, että kutsuisi RandomProvider-palveluamme näissä muissa säikeissä.

Käyttöliittymän suunnitteluongelmat

Yksi ongelma on edelleen olemassa: käytämme heikosti suojattua satunnaislukugeneraattoria. Kuten aiemmin mainittiin, RandomNumberGeneratorissa on paljon turvallisempi versio RNG:stä, jonka toteutus on RNGCryptoServiceProvider-luokassa. Sen API on kuitenkin melko vaikea käyttää vakioskenaarioissa.

Olisi erittäin mukavaa, jos RNG-palveluntarjoajilla olisi erilliset "satunnaisuuden lähteet". Tässä tapauksessa meillä voisi olla yksi, yksinkertainen ja kätevä API, jota sekä epävarma mutta nopea toteutus että turvallinen mutta hidas toteutus tukevat. No, unelmoinnista ei ole haittaa. Ehkä samanlaisia ​​toimintoja tulee näkyviin tuleviin .NET Frameworkin versioihin. Ehkä joku muu kuin Microsoft tarjoaa oman sovittimen toteutuksensa. (Valitettavasti en ole se henkilö...sellaisen oikein toteuttaminen on yllättävän monimutkaista.) Voit myös luoda oman luokan johtamalla Randomista ja ohittamalla Sample- ja NextBytes-metodit, mutta ei ole selvää miten niiden pitäisi työ tai jopa oma toteutusnäyte voi olla paljon monimutkaisempi kuin miltä näyttää. Ehkä ensi kerralla…

Opetusalgoritmisissa ongelmissa tarve generoida satunnaisia ​​kokonaislukuja on melko yleistä. Tietenkin voit vastaanottaa ne käyttäjältä, mutta ongelmia voi syntyä täyttäessäsi taulukkoa 100 satunnaisluvulla.

C-kielen (ei C++) rand()-standardikirjastofunktio tulee avuksemme.

int rand(tyhjä);

Se luo näennäissatunnaisen kokonaisluvun arvoalueella 0 - RAND_MAX. Jälkimmäinen on vakio, joka vaihtelee kielen toteutuksesta riippuen, mutta useimmissa tapauksissa se on 32767.
Entä jos tarvitsemme satunnaislukuja 0-9? Tyypillinen ulospääsy tästä tilanteesta on käyttää modulo-jakotoimintoa.

Jos tarvitsemme numeroita 1 (ei 0) - 9, voimme lisätä yhden...

Ideana on tämä: generoimme satunnaisluvun 0-8, ja 1:n lisäämisen jälkeen se muuttuu satunnaisluvuksi 1-9.

Ja lopuksi surullisin asia.
Valitettavasti rand()-funktio tuottaa näennäissatunnaisia ​​lukuja, ts. numeroita, jotka vaikuttavat satunnaisilta, mutta ovat itse asiassa arvosarja, joka on laskettu älykkäällä algoritmilla, joka ottaa parametriksi niin sanotun viljan. Nuo. Rand()-funktion luomat luvut riippuvat arvosta, joka viljalla on sitä kutsuttaessa. Ja kääntäjä asettaa grain aina arvoon 1. Toisin sanoen numerosarja on näennäissatunnainen, mutta aina sama.
Ja tämä ei ole sitä, mitä me tarvitsemme.

Srand()-funktio auttaa korjaamaan tilanteen.

void srand(signed int seed);

Se asettaa rakeen yhtä suureksi kuin sen parametrin arvo, jolla sitä kutsuttiin. Ja myös numerosarja on erilainen.

Mutta ongelma pysyy. Kuinka tehdä viljasta satunnainen, koska kaikki riippuu siitä?
Tyypillinen tapa päästä tästä tilanteesta on käyttää time()-funktiota.

aika_t aika(aika_t* ajastin);

Se periytyy myös C-kielestä, ja kun sitä kutsutaan nollaosoittimella parametrina, se palauttaa sekuntien määrän, joka on kulunut 1. tammikuuta 1970 lähtien. Ei, tämä ei ole vitsi.

Nyt voimme välittää tämän funktion arvon srand()-funktioon (joka tekee implisiittisen heiton), ja saamme upean satunnaisen rakeen.
Ja numerot ovat upeita ja toistumattomia.

Jotta voit käyttää funktioita rand() ja srand(), sinun on sisällytettävä otsikkotiedosto , ja käyttääksesi time() -tiedostoa .

Tässä on täydellinen esimerkki.

#sisältää
#sisältää
#sisältää

käyttäen nimiavaruutta std;

int main()
{
cout<< "10 random numbers (1..100): " << endl;
srand(aika(NULL));
for(int i=0;i<10;i++) cout << rand() % 100 + 1 << " ";
cin.get();
paluu 0;
}