Portál AbcLinuxu, 9. května 2024 04:05

Programování v jazyce D (4): Funkce a delegáty – pokračování, podmínky, cykly, pole, pointery

22. 3. 2012 | Daniel Kolesa
Články - Programování v jazyce D (4): Funkce a delegáty – pokračování, podmínky, cykly, pole, pointery  

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.

Obsah

Lekce 6 – funkce a delegáty – pokračování

link

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

link

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

link

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

link

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í.

Overloading funkcí

link

Č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.

Předávání funkcí jako argumenty

link

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.

Template funkce

link

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é.

Funkce s variabilním počtem argumentů

link

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.

Jako v C

link

Tato cesta se hodí, pokud tvoříme interface k C v D. Má stejné nedostatky, jako v C, tzn:

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

Jako v D

link

Jedná se o podobnou cestu. Také je přítomen pointer _argptr. Řeší ale některé nedostatky:

K _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);
    }
}

Typově bezpečně

link

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.

Typově bezpečně se všemi typy

link

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.

Delegáty

link

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.

Vnořené funkce

link

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 s dlouhým zápisem

link

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.

Lambda funkce s krátkým zápisem

link

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; }

Domácí úkol

link
  1. Napište funkci, která přijímá argumenty typů 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.
  2. Napište template funkci, která bere delegát jako alias a vrací jeho návratovou hodnotu.
  3. Napište variadickou funkci, která selže při kompilaci, pokud počet předaných argumentů přesáhne 10 (Použijte vlastnost .length pole argumentů)

Lekce 7 – podmínky, cykly, enumerace

link

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 :-)

Podmínky

link

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)) { ... }

Switch

link

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í:

Cykly

link

Podmínky samotné většinou nestačí. Často potřebujeme něco opakovat. K tomu má D několik cyklů.

While

link

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.

Do .. while

link

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.

For

link

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ší.

Foreach

link

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í.

Enumerace

link

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.

Domácí úkol

link
  1. Napište program, který vytiskne pomocí cyklu sudá čísla od 2 do 160.
  2. Napište variadickou funkci, která vypíše všechny číselné argumenty jí předané, nečíselné ignoruje, a ke každému číselnému přidá 2 (využijte foreach, podmínky a výrazu is()).
  3. Přepište úkol 1 do ostatních typů cyklů.

Lekce 8 – pole a práce s nimi

link

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.

Typy polí

link

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í.

Inicializace polí

link

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.

Slices

link

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.

Práce s poli

link

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

link

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.

Matice

link

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

link

Ř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.

Implicitní konverze

link

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)[].

Domácí úkol

link
  1. Vytvořte program, který pomocí slicingu rozdělí jakýkoliv vstupní řetězec na slova (pro nalezení mezery použijte z modulu std.string funkci indexOf).
  2. Vytvořte variadickou funkci, která spojí všechna pole daná argumenty do jednoho a vrátí jej.
  3. Vytvořte funkci, která obrátí druhou polovinu pole.

Seriál Programování v jazyce D (dílů: 4)

První díl: Programování v jazyce D: Úvod a první kroky (1), poslední díl: Programování v jazyce D (4): Funkce a delegáty – pokračování, podmínky, cykly, pole, pointery.
Předchozí díl: Programování v jazyce D (3): Typy, proměnné, práce s čísly, literály a funkce

Další články z této rubriky

LLVM a Clang – více než dobrá náhrada za GCC
Ze 4 s na 0,9 s – programovací jazyk Vala v praxi
Reverzujeme ovladače pro USB HID zařízení
Linux: systémové volání splice()
Programování v jazyce Vala - základní prvky jazyka

ISSN 1214-1267, (c) 1999-2007 Stickfish s.r.o.