Procházení postupně vraceným polem

Školení, která pořádám

Někdy se můžeme setkat s funkcí, která vrací jen omezený počet záznamů s tím, že k dalším se můžeme dostat předáním parametru, který určuje pořadí záznamu, od kterého nás data zajímají. Nejčastěji to budou data z externích zdrojů, může jít ale i o data z databáze, pokud je dat hodně a my jimi nechceme zabrat celou paměť a zároveň nechceme použít funkci mysql_unbuffered_query. Pro ilustraci definuji funkci vracející data jako jednoduchou:

<?php
function f($params, $offset = 0) {
    return ($offset ? array(1000) : range(0, 999));
}
?>

Při nulovém parametru $offset vrátí funkce 1000 záznamů, jinak vrátí jeden.

Jak lze postupovat, pokud chceme projít všechny záznamy?

Ruční zvyšování ofsetu

<?php
for ($offset=0; !$offset || count($f) == 1000; $offset += 1000) {
    $f = f($params, $offset);
    foreach ($f as $row) {
        // kód
    }
}
?>

Tento kód je nepraktický, pokud funkci potřebujeme volat na více místech s různým vnitřním kódem. Potom musíme všude opakovat poměrně netriviální iteraci.

Zpětné zavolání vlastní funkce

Uvedený problém se dá vyřešit tak, že cyklus uzavřeme do další funkce, které jako parametr předáme funkci, která má zpracovat kód:

<?php
function f_callback($params, $callback) {
    for ($offset=0; !$offset || count($f) == 1000; $offset += 1000) {
        $f = f($params, $offset);
        foreach ($f as $row) {
            $callback($row);
        }
    }
}
f_callback($params, 'zpracovani');
?>

Tento přístup je nepraktický v tom, že sebejednodušší kód musíme uzavírat do funkce, která navíc pracuje s vlastní sadou proměnných – to může být výhoda, ale obvykle je to spíš nevýhoda.

Spojení do jednoho velkého pole

<?php
function f_merge($params) {
    $return = array();
    for ($offset=0; !$offset || count($f) == 1000; $offset += 1000) {
        $f = f($params, $offset);
        $return = array_merge($return, $f);
    }
    return $return;
}
foreach (f_merge($params) as $row) {
    // kód
}
?>

Tím, že všechny výsledky volání funkce spojíme do jednoho pole, získáme jednoduchou iteraci, ale zabereme tím hodně paměti. Pokud je výsledků opravdu velké množství, může to být významný problém.

Vlastní iterační funkce

<?php
class F {
    private $f, $params, $offset = 0;
    function __construct($params) {
        $this->params = $params;
        $this->f = f($params);
    }
    function each() {
        $return = each($this->f);
        if (!$return && count($this->f) == 1000) {
            $this->offset += 1000;
            $this->f = f($this->params, $this->offset);
            $return = each($this->f);
        }
        return $return;
    }
}
$f = new F($params);
while (list(, $row) = $f->each()) {
    // kód
}
?>

Vytvoříme si vlastní iterační funkci, která v případě, že dojde na konec aktuálního pole, sama zvýší ofset a zavolá funkci pro vrácení dat. Nevýhodou je nemožnost používání konstrukce foreach.

Iterátor

<?php
class F implements Iterator {
    private $f, $params, $offset = 0;
    function __construct($params) {
        $this->params = $params;
    }
     function current() {
        return current($this->f);
    }
     function key() {
        return key($this->f);
    }
     function next() {
        return next($this->f);
    }
     function rewind() {
        $this->f = f($this->params);
    }
     function valid() {
        if (!is_null(key($this->f))) {
            return true;
        } elseif (count($this->f) == 1000) {
            $this->offset += 1000;
            $this->f = f($this->params, $this->offset);
            return !is_null(key($this->f));
        }
    }
}
foreach (new F($params) as $row) {
    // kód
}
?>

Rozhraní Iterator z rozšíření SPL dovoluje nadefinovat vlastní způsob procházení objektů. Abychom toho dosáhli, musíme ale nadefinovat celkem pět funkcí.

Závěr

Osobně používám spojení dílčích výsledků do jednoho velkého pole, trápí mě ale paměťové nároky. Proto jsem hledal jiné řešení. Žádné, které bych považoval za dostatečně elegantní, jsem ale bohužel nenašel.

Přijďte si o tomto tématu popovídat na školení Výkonnost webových aplikací.

Jakub Vrána, Dobře míněné rady, 14.12.2007, diskuse: 4 (nové: 0)

Diskuse

ikona dgx:

Jen bacha na to 'if (!$return && count($this->f) == 1000)', iterační funkce by za konec bufferu pokládala i prázdný řetězec nebo nulu. Lepší by asi bylo, stejně jako v příkladě s implementací Iteratoru, testovat key($this->f) === NULL.

ikona Jakub Vrána OpenID:

each() vrací pole klíče a hodnoty. Takže ani prázdná hodnota nezpůsobí prázdnost $return.

dgx:

pravda, omlouvám se.

Ondrej Ivanic:

Myslim ze kazde slusne API navrhovane v dnesnych casoch by malo vratit objekt ktory implementuje Iterator (pripadne este pridat ArrayAccess).

Iterator vie jednodocho drzat vnutorny stav a starat sa o vsetky neprijemnosti:

function f($params) { ... }

foreach($value = f($params)) { ... }

Diskuse je zrušena z důvodu spamu.

avatar © 2005-2024 Jakub Vrána. Publikované texty můžete přetiskovat pouze se svolením autora. Ukázky kódu smíte používat s uvedením autora a URL tohoto webu bez dalších omezení Creative Commons. Můžeme si tykat. Skripty předpokládají nastavení: magic_quotes_gpc=Off, magic_quotes_runtime=Off, error_reporting=E_ALL & ~E_NOTICE a očekávají předchozí zavolání mysql_set_charset. Skripty by měly být funkční v PHP >= 4.3 a PHP >= 5.0.