Portál AbcLinuxu, 2. května 2025 05:47
Na začátek bych se chtěl omluvit, že seriál nějakou dobu neběžel. To bylo z různých důvodů, hlavně tedy různé věci mimo PC. Nyní ale pokračujeme, dnes dokončíme ty funkce, co jsme načali, přidáme si nějaké ty delegáty, pak cykly, podmínky, a přijde řada i na pole a práci s nimi a také trochu pointery, které jsou s poli těsně spjaty.
Příště se chci podívat na asociativní pole, základní problematiku objektů (struktury a třídy, nebudeme jim věnovat příliš velkou pozornost jako např. v Javě, C# nebo C++, protože budeme pracovat hodně i s jinými paradigmaty, ale probrat je je třeba), modulů a kondicionální kompilaci. Dále se pak zaměříme na const/immutable, string mixiny a přejdeme plynule na nějaké ty templates a generické programování. Pak ještě budeme řešit správu chyb a několik jiných věcí v jazyce, a nakonec přejdeme na psaní nějaké reálné, celkem velké aplikace, kde využijeme už naučených technik a probereme pár nových.
V minulém díle jsme načali problematiku funkcí. Teď už bychom měli umět nějakou tu základní funkci vytvořit, předat jí argumenty a vrátit hodnotu. Stačí to ale? Co když chceme zevnitř funkce modifikovat nějakou neglobální proměnnou, nebo ji jen přečíst? Jak spravovat u funkcí chyby? Co je to takový function overloading? Nebo funkce s variabilním počtem argumentů? Na to všechno se dnes podíváme.
Design kontraktem je jedna z nejlepších věcí v D. Jedná se v podstatě o systém podmínek, které se dají aplikovat na funkce a také na třídy (o tom v kapitole o třídách). Podobně lze programovat i C++, ale výsledek je často neohrabaný. D přišlo se speciální syntaxí. Základní nápad za kontrakty je jednoduchý. Kontrakt je v podstatě pouze výraz. Takový výraz se musí vyhodnotit jako pravda, jinak program obsahuje chybu a ta se musí opravit.
Assert je základním kontraktem. Je přítomen v mnoha jazycích, včetně C/C++. D není žádnou výjimkou. Assert je to, o čem jsem se zmínil nahoře – výraz, který se musí vyhodnotit jako pravda.
Použije se např. takto:
void foo(bool x) { assert(!x); }
To zařídí, aby když hodnota předaná funkci foo
je pravdivá, tak se
jedná o chybu v programu, a běh programu se tudíž ukončí. Je možné
předat i zprávu, která se vypíše na výstup při chybě:
assert(false, "Sem by se program vůbec neměl dostat.");
Kromě toho existuje ještě tzv. static assert. Ten se oproti normálnímu assertu vyhodnocuje při kompilaci. To umožňuje některé chyby zachytit už před tím, než se program zkompiluje. Syntaxe je stejná, jen se přidá slovíčko static:
static assert(x == 5);
Oproti Cčku, které prostě ukončí program, v D se dá chybný assert chytit jako výjimka. Design kontraktem v D ale nespočívá pouze v assertu.
Pre a post kontrakty jsou bloky kódu, které se vyhodnocují jako podmínky před spuštěním funkce a po spuštění funkce. Zapisují se nějak takto:
in { .... pre-kontrakt .... } out (result) { .... post-kontrakt .... } body { .... tělo funkce .... }
Příkladem si můžeme uvést funkci, která shodí program, pokud první argument je menší než 4, druhý argument menší než 16, nebo pokud návratová hodnota je větší než 65536.
int foo(int a, int b) in { assert(a >= 4); assert(b >= 16); } out (result) { assert(result <= 65536); } body { return a * b; }
Pokud funkce nevrací nic a chceme post-kontrakt, pak není třeba
uvádět (result), a syntaxe je totožná s in
. Na kontraktech není nic
složitého, je to primitivní koncept, a přitom je velice užitečný v
praktickém programování.
Často chceme mít několik variant té samé funkce s různými argumenty. V Cčku je nutné každou takovou funkci pojmenovat jinak. To je často dosti nepraktické, ale v C nutné. V D je to jednoduché:
void foo() { } void foo(int x) { writeln(x); }
V takovém případě si D při volání foo
zjistí, jestli některá z
funkcí
sedí. Pokud ne, program se nezkompiluje. Pokud jich sedí více, také se
nezkompiluje.
Je jednoduché:
void foo(ret function(args) x) { x(...); }
Např.
void foo(int function(float) x) { writeln(x(3.14f)); } foo(function int(float y) { writeln(y); return 5; });
V tomto příkladě jsem de facto předal literál funkce. Samozřejmě je možné předat jakoukoliv funkci, která je dostupná v aktuálním kontextu.
Tady se poprvé seznámíme s problematikou templates, nebo česky šablon. Už název napovídá, že template funkce bude „předloha“ k vygenerování nějaké reálné funkce. Ti, co mají nějakou předchozí zkušenost např. s C++, už ví; oproti C++ ale D přináší mnohem lepší syntaxi, která se o moc neliší od syntaxe normálních funkcí.
Template funkci zapíšeme jako:
návratový_typ název(template argumenty)(argumenty) { .... }
Co jsou to ty template argumenty? Ty v podstatě specifikují argumenty, které se vyhodnocují při kompilaci. Většinou u nich nespecifikujeme typ. V takovém případě se nejedná o hodnotu, ale o typ samotný. Template argumenty se dají využít jako typy reálných argumentů:
void print(T)(T arg) { writeln(arg); }
Když takovou funkci zavoláme s jakýmkoliv typem, který je podporován
funkcí writeln
, bude to fungovat. Při zavolání takové template funkce
se z předané hodnoty zjistí typ T
, a na jeho základě se vygeneruje
funkce, která se pak teprve volá.
Pokud není možné typ vydedukovat z argumentů, pak je nutné jej explicitně specifikovat. Například:
T a(T)() { return T.init; }
Vlastnost .init už byla zmíněna dříve, specifikuje výchozí hodnotu takového typu. Takovou funkci pak zavoláme s pomocí vykřičníku:
auto x = a!int();
O auto
jsme se ale ještě nezmínili. Jedná se o jednu z možností,
kterou poskytuje typová inference v D. Pokud je možno typ proměnné
zjistit z hodnoty, pak auto
se automaticky nahradí tím pravým typem.
Znamená to, že
auto x = 5;
fungovat bude, ale jen
auto x;
už ne. Každopádně to byla jen taková odbočka. Pokud specifikujeme více template argumentů, které se nedají zjistit, dáme je do závorek:
x!(int, float)();
Template argumenty nemusí být jenom typy. Mohou to být i celá čísla jakéhokoliv typu, imutabilní řetězce a literály jakéhokoliv typu. Příklad s čísly:
void foo(int T)() {} foo!5();
To se hodí např. při různých výpočtech při kompilaci. Imutabilní řetězce:
int foo(string code)() { mixin(code); } foo!"return 5;"();
Takový výraz bude mít hodnotu 5. Mixin je funkce, která umožňuje řetězce známé při kompilaci „zabudovat“ do kódu. To se hodí např. pro tvorbu domain-specific jazyků. Zatím se jimi ale zabývat nebudeme.
Pokud template argument má typ alias
, jedná se o literál jakékoliv
hodnoty. To se hodí u lambda funkcí. Za chvíli o nich bude řeč.
Template argumenty je možné specializovat. Například:
void foo(T )() { ... } /* pro všechno ostatní */ void foo(T : int)() { ... } /* specializace pro int */
Ještě bych se měl zmínit o tzv. variadických
templates. Pokud jako poslední template argument zapíšeme
NÁZEV..., pak to znamená tzv. template parameter tuple. Ten nemá
explicitně specifikovaný typ, musí být striktně poslední a může být jen
jeden. Může reprezentovat jak typy, tak hodnoty. Obsahuje pak všechny
template parametry, které nebyly explicitně specifikovány předtím. Dá
se s ním pracovat jako s polem (přistupovat na jednotlivé prvky přes [index]
, řezat apod. – viz pozdější kapitola o polích).
Jinak o templates nebude asi nějaká zvláštní kapitola. Místo toho je budu zmiňovat průběžně tak, aby se je programátor naučil používat přirozeně. Templates nemusí být jen funkce. Mohou být i template struktury, třídy, nebo klidně i templates jako samotné.
Občas se hodí funkcím předat blíže nespecifikovaný počet argumentů. K tomu slouží tzv. variadické funkce. D je podporuje ve čtyřech formách.
Tato cesta se hodí, pokud tvoříme interface k C v D. Má stejné nedostatky, jako v C, tzn:
extern (C)
(takže nemůže mít overloady)Argumenty jsou přístupné přes pointer _argptr. Pokud znáte C, nemělo by
toto dělat problém. Pokud ne, zatím nebude důvod tuto cestu použít.
Modul core.vararg
poskytuje template funkci va_arg, umožňující získat
hodnotu.
Příklad:
import std.stdio, core.vararg; extern (C) void print_all_as_int(int n, ...) { /* Cyklus foreach - probereme v další lekci, tady v podstatě pouze spustí writeln pro každou hodnotu od 0 do n, kde n není zahrnuto */ foreach (i; 0 .. n) writeln(va_arg!int(_argptr)); }
Jedná se o podobnou cestu. Také je přítomen pointer _argptr. Řeší ale některé nedostatky:
extern (C)
, takže může mít overloadyK _argptr přidává ještě další skrytou proměnnou _arguments, která je v podstatě polem obsahující informaci o počtu argumentů a typ každého.
import std.stdio, core.vararg; void print_float_int_or_error(...) { /* .length vrátí délku pole */ foreach (i; 0 .. _arguments.length) { if (_arguments[i] == typeid(int)) writeln(va_arg!int(_argptr)); else if (_arguments[i] == typeid(float)) writeln(va_arg!float(_argptr)); else assert(false); } }
Tato cesta se hodí, když všechny argumenty jsou stejného typu, tzn. mohou vytvořit pole. Například:
void print_ints(int[] args ...) { writeln(args); }
V podstatě se všechny tyto argumenty přetransformují do jednoho pole.
Zdaleka nejelegantnější cesta. Hodí se kdykoliv, kdy je třeba pracovat s mnoha typy, a tyto typy jsou známy při kompilaci. Dá se použít skoro vždycky a nemá žádné další nároky např. na paměť, hodně se toho děje při kompilaci. Využíváme zde template funkcí, které jsme probrali nahoře, přesněji parameter tuple.
Například:
void print_all_args(T...)(T args) { foreach(arg; args) writeln(arg); }
Vzhledem k tomu, že parameter tuple obsahuje všechno, co se předá, použití jej jako pravých argumentů funkce vyústí v to, že se dá předat funkci kolik pravých argumentů chceme. Funguje to stejně, jako kdybychom všechny argumenty předali explicitně. Díky manipulaci template parameter tuples jako s poli lze přes toto iterovat pomocí už zmíněného cyklu foreach; cykly budeme probírat za chvíli. Taková funkce vypíše všechny argumenty předané funkci. Přes args přistupujeme k hodnotám; přes T přistupujeme k typům.
Teď se podíváme na delegáty. Kromě delegátu se dá použít i název closure, popř. v užším slova smyslu lambda funkce. Jedná se o tzv. fat pointer. Skládá se ze dvou částí, pointeru na funkci samotnou a pointeru na kontext (stack frame). Kontext obsahuje lokální proměnné scope, která takový delegát uzavírá. V praxi to znamená, že je možné z delegátu modifikovat a číst proměnné z vnějšího kontextu. D implementuje pravé closures; pokud starý kontext zanikne, je zkopírován a uložen v delegátu. To umožňuje delegáty různě vracet apod.
Nejjednodušší případ delegátů. Tyto funkce jsou delegáty z logických důvodů; programátor může očekávat, že bude moci přistupovat k vnějším proměnným.
void foo() { int i = 5; void bar() { ++i; } bar(); assert(i == 6); }
Vnořené funkce jako delegáty mají výhodu i v tom, že se z nich dají dělat template delegáty:
void foo() { int i = 5; void bar(T)(T arg) { writeln(arg); ++i; } bar(5); bar("hello"); bar(3.14f); assert(i == 8); }
Pokud chce programátor opravdu, aby taková funkce nebyla delegátem,
stačí před návratový typ přidat slovo static
:
void foo() { static void bar() { writeln("not a delegate!"); } bar(); }
Lambda funkce jsou takové delegáty, které většinou vyhodnocují výraz, ale mohou i složitější věci. D si zjistí, jestli taková lambda funkce přistupuje ke kontextu, a podle toho ji udělá buď delegátem, nebo obyčejnou funkcí. Pokud jsme si jisti, že je delegátem, pak ji můžeme předat podobně, jako funkci:
int i; void foo(int delegate(int, int) x) { writeln(x(5, 10)); } foo((a, b) { ++i; return a + b; });
Jak lze vidět, nespecifikoval jsem při volání typy. To není ani
nutné,
protože D v tomto případě využije tzv. typovou inferenci k dedukci typu
z deklarace/definice funkce foo. Z ukázky lze vidět další věc – při
předávání delegátů jako argumentů platí stejná pravidla jako u funkcí,
včetně literálů, které se dají zapsat dlouze jako delegate ret(...) {
... }
.
Tento kód má bohužel jednu nevýhodu, a to dost zásadní. Pokud D vyhodnotí, že se nejedná o delegát, kompilace selže (pokud se nepoužije dlouhý zápis zmíněný v předchozím odstavci). Proto je zde lepší využít template funkcí:
void foo(T)(T x) { writeln(x(5, 10)); } foo((int a, int b) { return a + b; });
V tomto případě se ale objevuje další nevýhoda – není možné využít typovou inferenci. Místo toho zkusme něco takového:
void foo(alias x)() { writeln(x(5, 10)); } foo!((a, b) { return a + b; })();
Co že toto dělá? Stejně jak jakékoliv T, U nebo cokoliv v template
argumentech specifikuje typ, alias
specifikuje literál jakékoliv
hodnoty. V našem případě je to literál funkce/delegátu. Protože alias
se v tomto případě vyhodnocuje až při volání, není nutné znát typy
argumentů a tudíž je možné využít typovou inferenci. Ke zjištění typů
pak dojde z hodnot. Template argumenty předáváme, jak už jsem dříve
zmínil, přes !(...)
. Template funkce, která bere lambda funkci jako
alias, musí být globální, ne součást nějakého běžícího kontextu.
Jedná se o stejný princip, ale vzhledem k tomu, že klasické lambda funkce jsou dosti dlouhé, je přítomna v D alternativní syntaxe.
Vezmeme předchozí případ s aliasem. Jak můžeme nahradit to škaredé
volání foo!((a, b) { return a + b; })();
? Snadno:
foo!((a, b) => a + b)();
Zápis
(argumenty) => návratová_hodnota
je zkrácený zápis lambda funkce. Výrazy jako:
(a) => a * a;popř.
(a, b) => a * b;
se dají přímo přeložit do
(a) { return a * a; }, popř.
(a, b) { return a * b; }
int
a float
a vrací
jejich násobek. Pre-kontrakt bude ověřovat, zda int
argument je větší
než 10. Post-kontrakt bude ověřovat, zda návratová hodnota je větší než
50.alias
a vrací
jeho návratovou hodnotu..length
pole
argumentů)Co je nám platné, pokud známe funkce a proměnné bez znalosti cyklů a
podmínek? Tyto konstrukce jsou fundamentálními prvky v procedurálním
programování. Bez nich nemůžeme snadno řídit průběh kódu. Také se
podíváme na enumerace. Tak jdeme na to
Jak už název naznačuje, podmínky větví běh kódu. Bez žádného dalšího okecávání, napíšeme si podmínku:
if (VÝRAZ) { ....pravda.... } else { ....nepravda.... }
Pokud VÝRAZ
platí, spustí se blok kódu pro „pravda“. Pokud ne,
spustí se blok ve větvi else
. Pro jednoduché jednořádkové podmínky
nejsou třeba složené závorky:
if (x == 5) foo(); else bar();
Větev „else“ není vždy potřeba. Tudíž je možné zapsat:
if (y == 10) x();
Binární operátor ==
porovnává dvě hodnoty. Lze použít samozřejmě už
dříve použité operátory:
int y = 5; if (y + 5 == 10) writeln("pravda!");
Opak operátoru ==
je operátor !=
. Větvit můžeme dále, pomocí else
if
. Například:
if (x == 5) { ... } else if (x == 10) { ... } else { ... }
Pro porovnávání hodnot ještě používáme operátory <
(menší), >
(větší), <=
(menší nebo rovno), >=
(větší nebo rovno). Např.
if (x < 5) writeln("x je menší než 5");
Můžeme testovat několik podmínek pomocí operátorů &&
(AND) a ||
(OR). Příklad:
if (x == 5 && y == 10) ... /* Pokud x je rovno 5 a zároveň y 10 */ if (x == 5 || y == 10) ... /* Pokud alespoň jedna z podmínek platí */
Unární operátor !
převrací hodnotu výrazu. Např:
bool x = true; if (!x) ....
Uvnitř podmínek můžeme používat i přiřazování, pokud dáme výraz do dalšího páru závorek.
if ((x += 10) == 20) ...
Speciální podmínka static if
testuje při kompilaci. Hodí se to při
programování s templates.
void foo(int i)() { static if (i == 5) .... else static if (i == 10) .... else .... }
Ternární operátor umožňuje krátký zápis jednoduchých podmínek:
(x == 5) ? pravda : nepravda
Takové výrazy je možné psát např. i ve volání funkcí, což se hodí při psaní kompaktního kódu:
void foo(int x) { ... } foo(y ? 1 : 5);
V souvislosti s podmínkami bych měl uvést ještě is()
. To umožňuje
kontrolovat typy. Např.
void foo(T)() { if (is(T == int)) .... else .... }
Samozřejmostí je kombinace se static if
. Výraz is(T == int)
kontroluje přesně typ. Nahrazením ==
dvojtečkou lze kontrolovat
implicitně konvertovatelné typy na tento typ:
is(T : int)
Výrazů s is()
je možné využít pro specializaci template funkcí:
void foo(T)() if (is(T == int)) { ... }
Speciálním případem podmínek je switch. Ten umožňuje efektivně testovat na mnoho hodnot bez toho, abychom museli psát spoustu if větví. Syntaxe:
switch (výraz)
{
case HODNOTA:.... kód ....
break;
case NĚCO:
.... kód ....
break;
default:
break;
}
je ekvivalentní s
if (výraz == HODNOTA) ...
else if (výraz == NĚCO) ...
Na konci každé case
většinou musíme dát break;,
jinak se přejde
na další case. Toho se dá využít pro testování několika hodnot se
stejným kódem:
int i = 10; switch (i) { case 5: case 10: writeln("5 nebo 10"); break; case 15: break; default: break; }
Kód v default
je ekvivalentní s větví else
u standardní
podmínky. D nabízí kromě toho ještě např. goto case
, který přejde na
nějaký case
.
case 5: goto case 10; break; case 10: break;
Oproti C, C++ apod., D umožňuje testovat nejen integrální hodnoty, ale i např. řetězce:
string x = "hello"; switch (x) { case "hello": writeln("hello!"); break; default: break; }
Case mohou obsahovat i několik hodnot:
case 5, 6, 7, 8, 9, 10: break; /* od 5 do 10 */
Mohou být použity i hodnoty proměnných.
int i = 50; .... case i: ... break;
Kromě toho existuje ještě final switch
, který má několik omezení:
default
není povolencase
musí být literálem.Podmínky samotné většinou nestačí. Často potřebujeme něco opakovat. K tomu má D několik cyklů.
Nejjednodušší je cyklus while
. Opakuje, dokud je splněna podmínka.
Například:
int i = 5; while (i < 150) ++i;
Pokud v cyklu while použijeme break;
, vystoupíme z cyklu. Např.
tímto dosáhneme podobné funkcionality.
int i = 5; while (true) { if (i == 150) break; ++i; }
Další slovíčko continue;
přeskočí na další iteraci. Např.
int i = 5; while (true) { if (i == 150) { ++i; continue; } ++i; writeln(i); }
Tento příklad vypisuje čísla od 6 až do nekonečna, kromě 150, která nebude vypsána.
Cyklus
do { .... } while (výraz)
je identický s while
, s jedním rozdílem; kód v bloku se spustí
alespoň jednou, až pak se začne testovat podmínka.
Cyklus for
je takový vylepšený while
. Jeho syntaxe je taková:
for (inicializace; podmínka; post-iterace) { ... }
Část „inicializace“ se spustí první. Potom se cyklí, dokud je splněna podmínka. V části „post-iterace“ se provede něco, co má být spuštěno po každém cyklu. Například
for (int i = 5; i < 150; ++i) writeln(i);
vypíše čísla od 5 do 149. Break
a continue
fungují s jedním
rozdílem; část post-iterace se spustí i při break
nebo continue
. To
znamená že
for (int i = 5; i < 150; ++i) if (i == 100) continue; else writeln(i);
sice nevypíše 100, ale provede správně inkrementaci. Cyklus for
se v
D nepoužívá tak často, protože cyklus foreach
je pro většinu případů
vhodnější.
Cyklus foreach
je přídavek jazyka D ke klasickým C nebo C++ cyklům.
Umožňuje iterovat přes číselný rozsah, pole, asociativní pole, template
argument tuple i uživatelské iterovatelné struktury.
Ekvivalent for (int i = 0; i < 10; ++i)
:
foreach (i; 0 .. 10) { ... }
Typová inference se stará o typ i
. Samozřejmě je možné jej i
explicitně specifikovat. Iterace přes pole:
int[] x = [ 5, 10, 15, 20 ]; foreach (v; x) writeln(v); /* vypíše každý prvek pole */
Pole jsme sice ještě nedělali, ale tento příklad by měl dávat smysl. Iterovat je možné i s indexem:
int[] x = [ 5, 10, 15, 20 ]; foreach (idx, val; x) writefln("x[%d] == %d", idx, val);
V případě asociativních polí by hodnota idx
byla klíčem aktuálního
prvku. Jinak je indexem (0 až délka-1).
Cyklus foreach jsme vlastně zmínili už v našem příkladě s variadickou funkcí. To funguje na stejném principu, jako pole.
Tím bych cykly zakončil; budeme je samozřejmě dále využívat v praxi. Cílem nyní není popsat úplně každou funkci, to bychom tu byli týden a ještě by tento díl nebyl stále u konce; cílem je popsat základní ideu a dále rozšiřovat při praktickém použití.
Může se zdát trochu divné je dělat zde, ale budou velice užitečné a proto je chci probrat hned. Enumerace je de facto kolekce konstant. V C, C++ (před C++11) jsou pouze číselné. V D mohou být jakéhokoliv typu. Základní enumeraci definujeme
enum Jméno { A, B, C, D }
Potom můžeme dělat instance enumerací:
Jméno x = Jméno.C;
Hodnota A bude 0, B 1, C 2, D 3. Explicitně specifikovat hodnoty je možné, dobrou konvencí je specifikovat první prvek jako 0. Následující prvky budou mít vždy o 1 větší hodnotu.
enum X { A = 5, B, C, D }
D umožňuje, jak už jsem řekl, dělat enumerace mnoha typů. Např. pro řetězce
enum X: string { A = "foo", B = "bar", C = "baz" }
Takové enumerace přirozeně potřebují explicitně specifikované hodnoty. Výchozím typem pro enumerace je int. Enumerace je možné přirozeně předávat jako argumenty a castovat z/do původních typů, ze kterých vycházejí. K čemu je toto dobré? Většinou se využívají pro set vlastností, které ovlivňují hodnotu něčeho. V praktických úlohách na ně dojde.
foreach
, podmínky a výrazu is()
).Znalost funkcí a výrazů pro kontrolu průběhu programu je hezká věc a tvoří už celkem solidní znalostní základ. Teď se naučíme pracovat s poli. D to dělá pro uživatele relativně jednoduché. Asociativní pole probereme později.
Pole můžeme rozdělit na dva typy. Prvním typem jsou statická pole, druhým dynamická pole. Statická pole mají fixní velikost a ta musí být známa už při kompilaci. Statická pole jsou na stacku a jsou analogická se statickými poli v ANSI C (ne C99).
Deklarují se:
typ[velikost] pole;
například:
char[512] buf;
Velikost statického pole nesmí přesáhnout 16 MB. Je to hodnotový typ, předává se také vždy po hodnotě a lze jej vracet z funkcí. V tom je rozdíl od C. V C se statická pole předávají po pointeru.
Alternativní způsob deklarace jakéhokoliv pole je
typ pole[velikost];
stejně jako v C, ale nepoužívejte tuto možnost, je to spíš jen pro snadnější migraci.
Dynamická pole jsou alokována dynamicky, tudíž většinou na heap. Deklarují se
typ[] pole;
například
int[] x;
V defaultu je paměť dynamických polí spravována automaticky garbage collectorem, stejně jako všechno ostatní v D. Dynamická pole jsou předávána referencí.
Pole inicializujeme většinou pomocí literálu, nebo vůbec:
int[] x = [ 5, 10, 15 ];
Takové pole bude mít délku 3. U statických polí jsou všechny
neinicializované prvky nastaveny na nulu. Dynamická pole můžeme
inicializovat i na void
, pak je obsah nedefinován, ale to je nebezpečná
optimalizace.
Před práci s poli je nutné ujasnit si dva pojmy. V D se rozlišuje
pole a slice (řez). Pole je buď statické pole přímo, nebo dynamicky
alokované pole bez uložené délky, stejně jako v C (např. při alokaci
pomocí calloc
). To, s čím uživatel bude v naprosté většině případů
pracovat, je slice. Slice je reprezentován pointerem na první element
pole a délkovou informací, ke vždy na stacku.
typ[] x;
je de facto slice. Ukazuje na nějaký pointer (v tomto případě nulový pointer) a délkovou informaci má nastavenou v defaultu na nulu. Jak už název napovídá, slices se budou moci dále řezat na kratší kousky. Pokud máme např. pole, které je reprezentováno prvky
0-1-2-3-4-5-6-7
a chceme slice od 3 do 5, pointer se nastaví na pointer elementu 3 a délková informace taky na 3.
Inicializujme si pole z literálu, např.
int[] arr = [ 5, 10, 15, 20, 25, 30, 35, 40, 45, 50 ];
Dojde k alokaci paměti pro pole, vytvoření slice nad polem a to je
ten samotný int[]
. To neplatí pro statická pole, kde literály jsou
samozřejmě na stacku. Když si pole vypíšeme pomocí
writeln(arr);
tak by se mělo správně vytisknout.
Teď chceme nastavit druhý prvek na 11. To můžeme udělat snadno
arr[1] = 11;
Když si zjistíme délku pole:
writeln(arr.length);
mělo by se vypsat 10. Pokud chceme rozšířit pole na délku dejmetomu 15, můžeme tuto vlastnost modifikovat:
arr.length = 15;
Pak si můžeme nastavit hodnoty jednotlivých prvků. Pokud nová délka
je menší, nedojde k realokaci, je to ekvivalentní se slice [0 ..
nová_délka]
. Teď dejmetomu chceme získat podpole, které začíná prvkem s
hodnotou 15 a končí 35. To je snadné:
int[] sub = arr[2 .. 6];
Nedojde k žádnému kopírování. Tento řez polem bude sdílet paměť s původním polem. To znamená, že změna hodnoty v původním poli se projeví i v řezu. Teď chceme z tohoto podpole vyřezat první prvek. Uděláme:
sub = sub[1 .. $];
$
se substituuje za délku pole. Je ekvivalentní s sub.length.
int[] x = arr;
jen vytvoří nový slice původního arr, tudíž nedojde ke kopii a x
bude jen referencí původního pole. Pomocí []
lze vytvořit slice celého
pole:
int[] x = arr[];
což je ekvivalentní s arr[0 .. $]
. Teď chceme nastavit několik prvků
arr na stejnou hodnotu. Provedeme
arr[0 .. 4] = 42;
Teď chceme nastavit první dva prvky na různé hodnoty. Provedením
arr[0 .. 2] = [ 3, 5 ];
se nastaví první dva prvky na 3 a 5. Co když chceme připojit k
našemu arr
jiné pole? Snadné:
arr ~= [ 150, 140, 130 ];
Když slice je jen další reference na pole, občas potřebujeme i
duplikovat původní pole. To se dá udělat vlastnosti .dup
.
int[] arr2 = arr.dup;
Teď chceme obrátit pořadí prvků v arr. Slices mají vlastnost .reverse. Tudíž můžeme udělat:
arr[] = arr.reverse;
Kromě .dup
existuje i .idup
, který vytvoří duplikát s imutabilními
prvky, ale o tom později. Existuje i .sort
, který pole seřadí, ale
plánuje se jej zbavit, protože knihovna většinou bude mít lepší
výsledky. Proto to nebudeme zde řešit. .sizeof
vrátí délku vynásobenou
velikostí jednoho prvku (což je .sizeof
vlastnost toho prvku).
Vlastnost .init
vrátí pro dynamická pole null (jakožto nulový pointer)
a pro statická pole literál výchozích hodnot prvků.
Slices umožňují kontrolu polí, takže pokud se zjistí při kompilaci změna mimo rozsah pole, program se nezkompiluje, a pokud se zjistí za běhu, vytvoří výjimku. O výjimkách se budeme bavit později, zatím nejsou potřeba. Aby se nesnižoval výkon, tato kontrola je povolena pouze, když program není zkompilován jako release.
Na závěr velmi krátká (a vcelku neefektivní – chtělo by to lepší pivot apod. :)) implementace algoritmu quicksort pomocí slices:
import std.stdio: writeln; auto filter(alias pred, T)(T[] arr) { T[] r; foreach (v; arr) if (pred(v)) r ~= v; return r; } void main() { T[] qsort(T)(T[] xs) { return (!xs.length ? [] : qsort(filter!(x => x <xs[0])(xs[1..$])) ~ xs[0..1] ~ qsort(filter!(x => x>=xs[0])(xs[1..$]))); }; auto x = [ 4, 150, 3, 8, 163, 141, 98, 56, 99, 108 ]; writeln(qsort(x)); }
Podobné implementace lze najít např. pro Haskell a jiné funkcionální jazyky.
Pointery, česky ukazatele, jsou de facto pouze čísly, jejich
velikost je v 32 nebo 64 bitů, dle architektury (D nepodporuje 16bitové
architektury). Jejich hodnotou je určitá adresa v paměti. Nulový
pointer ukazuje na nulu, to znamená na nic, v D se zapisuje jako null
(aby se předešlo konfliktům s nulou, jako v C++). Pointery jsou v C
použity na spoustu věcí, např. k předávání „po referenci“, k práci s
poli – v tom případě pointer ukazuje na první prvek pole apod. Pointery
jsou samozřejmě i v D. Pokud máme pole, např.
int[] x = [ 5, 10, 15, 20, 25 ];
pak pointer na 5 získáme
int* y = x.ptr;
Takový pointer pak můžeme předat třeba nějaké C funkci. Získáním pointeru se vzdáme délkové informace. Kromě získávání pointerů ze slices je možný i opačný postup.
int[] z = y[0 .. 5];
To vytvoří v podstatě identický slice jako v případě x. Vzhledem k tomu, že neznáme délku, musíme ji manuálně specifikovat. Takové chování umožňuje alokovat dynamická pole klasickými metodami jako v C, např.
import core.stdc.stdlib; int[] arr = (cast(int*)calloc(5, int.sizeof))[0 .. 5];
Samozřejmě, u takového pole nelze měnit .length vlastnost a musíme se jej manuálně zbavit:
free(arr.ptr);
K použití pointerů by ve většině případů v D neměl být důvod. Jazyk poskytuje dost funkcí, které mohou pro high level programování pointery úplně nahradit, ale vzhledem k tomu, že D je také systémovým jazykem, jsou pointery přítomny (a i z důvodu kompatibility s C knihovnami). Teď se jimi moc zabývat nebudeme, to ani není cílem. Jen bych chtěl upozornit, že kód v C:
int *x, *y, *z;
se v D zapíše
int* x, y, z;
Pointerová aritmetika apod. v D funguje naprosto stejně, jako v C.
Pracovat s pointery jako s poli se také dá, identicky s C. U pointerů
na struktury neexistuje ->
pro přístup k jednotlivým prvkům, používá
se .
, stejně jako u normálních instancí:
Foo *bar = new Foo(); /* writeln(bar->x); špatně */ writeln(bar.x); /* dobře */
V souvislosti s pointery, D má ještě pole typu void[]
, které má
pointer typu void*
. Stejně jak není možné provést dereferenci void*
pointeru, tak není možné vzít jeden prvek takového pole.
Dva a vícedimenzionální pole v D nejsou. Místo toho vytvoříte stejně jako v C „pole polí“. Příklad:
int[5][5] foo;
Řetězce v D jsou poli znaků. Rozlišujeme tři typy řetězců v D, podle velikosti jednotky:
O jejich literálech už bylo řečeno v předchozí kapitole. Literály
jsou typu immutable(char)[]
(mutabilní pole imutabilních znaků), popř. wchar, dchar
. Pro lepší čitelnost má immutable(char)[]
alias nazvaný string
. Pro práci s řetězci platí stejná pravidla, jako pro pole,
dají se spojovat, duplikovat, řezat apod.
Oproti C nejsou řetězce v D zakončeny nulou, protože se slices to nemá význam. Literály jsou ale zakončeny, pro kompatibilitu s C funkcemi.
Všechny pointery v D (kromě function pointerů a delegátů) se dají převést na void*
. Statická pole T[x]
se
dají implicitně převést na T[]
, const(T)[]
a void[]
. Dynamická pole se
dají převést na const(T)[]
a void[]
. Z toho vyplývá, že const(T)[]
se
dá použít jako např. typ argumentu tam, kde fungují jak imutabilní
řetězce, tak mutabilní řetězce. V takových případech se ale místo const
doporučuje použít inout(T)[]
.
std.string
funkci indexOf
).for (inicializace; podmínka; post-iterace) { ... }Část „inicializace“ se spustí první. Potom se cyklí, dokud je splněna podmínka. Opravdu?
->
by se muslo použít něco jako smartptr.get().fooBar()
a to zavání Javou boost::stdint
a podobné... Nicméně C99 a C++11 toto řeší tím, že tento přístup standardizují (viz), což si myslim, že je dobré...
3) Řeší C++11 - nullptr
Řekl jsem snad, že C++ je pomalejší? Řekl jsem, že D umí být stejně rychlé, a to je rozdíl.proč tedy D co se týče neohrabanosti daleko lépe řešeno a přitom v některých případech výkonnostně C++ převyšuje?
Vícenásobná dědičnost není plně nahraditelná ani interfacy ani mixiny ani jejich kombinací.Například kdy?
v D mám nad programem stejnou kontrolu, jako v C++, možná ještě o něco vyšší (díky lepšímu metaprogrammingu, traits apod.)Tak to chci vidět, jak naimplementujete v D třístavovou logiku, kterou používá třeba SQL, pro objekt, který nemá defaultní konstruktor, pokud teda nechcete všechno cpát na haldu jak Java. V C++ buď sáhnete po Boost.Optional nebo si něco podobného napíšete za pár minut.
Já mám pocit, že D představuje to, kam se C++ mohlo dostat, kdyby se na něm opravdu makalo. Standardizace C++ mi připomíná standardizaci OpenGL - trvá to dlouho a je to pak zklamání.Souhlasím, nicméně u toho D bych zas viděl jako potenciální problém velký "rozmach" tohoto jazyka. Co jsem tak koukal, oproti C++ má D brutální množství speciálních výrazů, hlavně teda klíčových slov, případně různých speciálních položek objektů a jejich členů. Například množství klíčových slov pro parametr funkce. Nebo všechny ty
Object.op*
při operator overloadingu. A tak dále. Přijde mi hodně náročný tohle všechno zvládnout.
Autoři D v mnoha ohledech kritzují C++ pro přílišnou složitost, nerozumím tedy tomu, proč, ač na jedné straně složitost snižují, na straně druhé sami přidávají hafo dosti složitých featur, u kterých není jisté, jestli je někdo někdy vůbec bude používat.
... pokud se něco ve standardní knihovně nahradí, stará se označí jako "deprecated" a autoři programů mají několik měsíců čas si svůj program zaktualizovat.Tohle bohužel v žádném případě není řešení problému. Zpětná kompatibilita. Zpětná kompatibilita. Zpětná kompatibilita. Jazyk, který ji nerespektuje, nemá nejmenší šanci se pro sadit jako něco více než jazyk pro hobby projekty. I teď po letech se smutkem sleduju, jak se ještě stále nevyřešilo "schizma" mezi Python2 a Python3 částí pythoního světa. Ten jazyk mám opravdu moc rád, ale vždycky, když v něm chci něco začít něco psát, tak mě tahle jeho bipolarita odrazuje.
To není. Neznám jediný jazyk, který by se nekompatibilně měnil mezi major verzemi a přežil by střednědobý horizont.PHP.
Zato si nezapomněli přidat, že znak 0x00, nebo znak 0x1A, kdekoli se vyskytne ve zdrojáku, ukončí parsing. Opravdu velmi čistý parsing. Skvělý. (Sarkasmus)Tohle má PHP taky, akorát lepší: __halt_compiler().
Jak by řekli v Big Bang Theory: Is that relevant factor?Pro programátora to přeci také znamená usnadnění. Když porovnám šablony v C++ a v D, je to obrovský rozdíl co se týče snadnosti čtení.
Jsou tam dobré věci, ale nalepené bez rozmyslu.Ne. Ve skutečnosti tam nic takového není a všechno má svůj smysl. V konferencích se vedou diskuze o všem, co se přidává/mění a vše musí být odůvodněné podstatnými argumenty. Když jsem četl knihu od Andreie Alexandrescu, měl jsem silný pocit osvícení, jelikož vysvětluje prakticky všechno, kde se to vzalo a proč. D tak na mě naopak působí jako úžasně konzistentní jazyk, kde nic není jen tak pro dobrý pocit a nad vším se přemýšlelo.
Nicméně D znám, a před několika lety jsem ho testoval, zda se nevybodnout na C++ a nepřejít.D před pár lety a D dnes je docela podstatný rozdíl.
Mimochodem, D je asi první jazyk, který nemá nic jako rozdělení řádky typu \Jak jsem to četl, napadlo mě že je to fakt nevýhoda, ale když jsem se pak snažil vymyslet nějaký příklad kde je to nutné použít, tak jsem na nic nepřišel, takže se rád nechám osvítit.
Když na tom trváte, já Vám to tedy řeknu: Na to abyste jeden řádek mohl rozdělit do více řádků.To mě samozřejmě také napadlo, ale jaký je konkrétní příklad? String můžu rozdělit následovně:![]()
string a = "dloooouhy string......................" "pokracovani, ktere se mi neveslo na predchozi radek";Ostatní výrazy lze oddělit prostě pokračováním na dalším řádku. Možná u názvu superdlouhé funkce? Ale to by byla zase úžasná prasárna. Tak fakt nevím.
#define METHOD(params) \ doSomething(params, __FILE__, __LINE__)
__FILE__
je na kompilátoru, není standardizována (může tam být cesta absolutní, relativní,... je to dost nahouby makro)... Pokud člověk používá například cmake, je lepší něco jako
foreach(i ${my_sources})
set_property(SOURCE ${i} PROPERTY COMPILE_DEFINITIONS MY_FILE="${i}")
endforeach(i)
a v ostatních systémech obdobně... To jen tak naokraj.
Kompilaci spíše zdržují includy, a to díky diskovým přístupům, ale to není věcí toho zda je gramatika závislá na kontextu.A ty řeší precompiled headers, což je možné právě díky té neexistenci standardizovaného ABI.
D je syntakticky daleko čistší jazyk než C++No zrovna Déčkové operátory jsou zářný příklad, jak se to dělat nemá.
Jak už dole Bystroushaak zmínil, např. při template metaprogrammingu, což je z velké části dáno i jednoznačně definovanou syntaxí.V C++ mají šablony taky jednoznačně danou syntaxi
Od C++11 lze v C++ zapsat if (x<a<b>>>y<c<d>>) .. to je podle mě dvojznačné. Kulaté závorky jsou OK. Co se týče specializace - vždy se to dá udělat, i když trochu jinak. Vzhledem k tomu, že templates jsou založené na parosvání textu, je to v kombinaci s moduly jasné, že to nejde stejně, jako v C++. Ale věci, jako template mixins apod., tomu dost pomáhají.
ISSN 1214-1267, (c) 1999-2007 Stickfish s.r.o.