برنامه نویسی

php: انجام بازگشت با iterator(iterator)های بازگشتی

بازگشت در بین برنامه نویسان شهرت بدی دارد. پیچیده و پیچیده است و اشکال زدایی آن دشوار است، یک تفنگ پای واقعی. این کاری است که در مدرسه انجام می دهید (اگر برای این کار به مدرسه رفته اید) و اگر بتوانید از آن اجتناب کنید، دیگر هرگز آن را لمس نکنید. که یک کشیدن است، زیرا موارد استفاده زیادی برای بازگشت وجود دارد. ساختارهای داده با عمق دلخواه همه جا وجود دارد: سیستم های فایل، درختان dom، بسته های json 32 کیلوبایتی که شریک ادغام شما به تازگی در api شما قرار داده است.

در این پست به بیش از دو ویژگی php می پردازیم که به آسان تر کردن بازگشت کمک می کند: RecursiveIterator رابط، که روش‌ها و ویژگی‌هایی را در اختیار ما قرار می‌دهد که نوشتن توابع بازگشتی را آسان‌تر می‌کند و نام‌های وحشتناکی دارد. RecursiveIteratorIterator کلاسی که می‌توانیم از آن برای صاف کردن ساختارهای داده عمیق دلخواه استفاده کنیم.

تکرار کننده را با یک تکرار کننده تکرار کنند
php به توسعه دهندگان: پنج بار سریع بگویید ‘iterator’

ما با استفاده از یک تابع بازگشتی خواهیم ساخت RecursiveArrayIterator، با یک حلقه ساده شروع می شود و تا عملکرد کامل کار می کند. سپس ما به چگونگی اهرم نگاه خواهیم کرد RecursiveIteratorIterator برای شکستن آن آرایه تو در تو در یک سطح واحد تا بتوانیم با یک حلقه ساده یا یک فراخوانی پیچیده تر، اما قدرتمند، داده ها را استخراج کنیم. iterator_apply.

آرایه نمونه ما

همه نمونه‌های اینجا از آرایه نمونه زیر استفاده می‌کنند که مجموعه‌ای از آلبوم‌های رکورد را نشان می‌دهد:

$albums = [ 
    'first_name' => 'grant',
    'last_name' => 'horwood',
    'albums' => [
        [
            'title' => 'pottymouth',
            'artist' => 'bratmobile',
            'year' => 1993,
        ],  
        [   
            'title' => 'monks music',
            'artist' => 'monk, thelonious',
            'year' => 1957,
        ],  
    ],  
];
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

این آرایه شامل سه عنصر سطح بالا است که یکی از آنها (albums) آرایه ای از آرایه است. این چیز خوبی برای بازگشت است.

را RecursiveIterator رابط

در اینجا دو چیز اصلی است که باید در مورد آن بدانیم RecursiveIterator رابط:

  1. این یک رابط است: RecursiveIterator در واقع به خودی خود کاری انجام نمی دهد، فقط یک رابط است که رفتار را تعریف می کند. اگر چیزی قابل استفاده می خواهیم، ​​باید به کلاس هایی که پیاده سازی می کنند نگاه کنیم RecursiveIterator; کلاس هایی مانند RecursiveArrayIterator (برای رسیدگی به آرایه ها) و RecursiveDirectoryIterator (برای مدیریت فایل سیستم ها).
  2. در واقع هیچ بازگشتی انجام نمی دهد: کلاس هایی که اجرا می کنند RecursiveIterator برای ما بازگشت انجام نده کاری که آنها انجام می دهند این است که ابزارهایی را برای کمک به ما در ایجاد آسان توابع بازگشتی خود فراهم می کنند.

ممکن است ناامید کننده به نظر برسد، اما نباید اینطور باشد: آن ابزارهایی که RecursiveIterator ارائه مفید هستند. بیایید به سه مورد از آنها نگاه کنیم.

  • hasChildren(): یک ساختار داده با عمق دلخواه، مانند یک آرایه، می تواند مقادیری داشته باشد که خود آرایه هستند. را hasChildren روش آزمایش می کند که آیا مقدار مورد نظر ما یک آرایه است یا خیر. ما از این برای آزمایش زمانی استفاده می‌کنیم که به تابع بازگشتی خود نیاز داریم تا عملاً تکرار شود. یک بولی برمی گرداند.

  • getChildren(): اگر hasChildren برمی گرداند true در این صورت باید یک تکرار کننده با ارزش فعلی دریافت کنیم تا چیزی برای تکرار داشته باشیم. این چیزی است که getChildren میکند.

  • مکان نما: همانطور که ما روی خود حلقه می زنیم RecursiveIterator، یک مکان نما داخلی وجود دارد که مکان ما را در آرایه ردیابی می کند. این به ما اجازه می دهد تا متدهایی مانند hasChildren برای عنصر فعلی این بسیار مفید است

اگر فوراً منطقی نبود، نگران نباشید. ما قصد داریم به ساخت یک تابع بازگشتی با آن بپردازیم RecursiveIterator گام به گام، با یک حلقه نه چندان مفید شروع کنید.

یک حلقه ساده (و نه خیلی مفید) با RecursiveArrayIterator

زیرا RecursiveArrayIterator یک تکرار کننده است، می توانیم از آن در a استفاده کنیم foreach حلقه بیا این کار را انجا دهیم.

$rai = new RecursiveArrayIterator($albums);

foreach($rai as $k => $v) {
    @print "$k => $v".PHP_EOL;
}
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

این بسیار ساده است: ما یک نمونه جدید از RecursiveArrayIterator با آرایه ما و سپس یک foreach، چاپ کلیدها و مقادیر. نتایج ضعیف هستند

first_name => grant
last_name => horwood
albums => Array
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

این یک رفتار تکرارکننده مستقیم است. حلقه ما به سطح بالای آرایه رفت و تمام. برای شروع ساختن یک تابع بازگشتی که آنچه ما می خواهیم را انجام می دهد، باید از آن استفاده کنیم RecursiveIterator‘s hasChildren و getChildren مواد و روش ها.

استفاده كردن hasChildren

همانطور که از نام آن پیداست، هدف از hasChildren تعیین اینکه آیا مقدار فعلی آرایه ای است که می تواند یک تکرار کننده از آن بسازد یا خیر. بیایید اضافه کنیم hasChildren به حلقه ناامید کننده ما

$rai = new RecursiveArrayIterator($albums);

foreach($rai as $k => $v) {
    print $k;
    if($rai->hasChildren()) {
        print " has children";
    }
    print PHP_EOL;
}
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

در اینجا، ما آزمایش می کنیم که آیا مقدار عنصر فعلی یک آرایه است، به عنوان مثال. دارای تعدادی عنصر «کودک» است و در صورت وجود، «فرزند دارد» را چاپ کنید. خروجی این است:

first_name
last_name
albums has children
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید


ℹ️ نکته مهم در مورد مکان نما: در اینجا می بینیم که روش را فراخوانی می کنیم hasChildren بر $rai، RecursiveArrayIterator خود شی ما می توانیم این کار را انجام دهیم زیرا RecursiveArrayIterator دارای یک مکان نما داخلی است که به عنصر فعلی که در حال حلقه زدن آن هستیم اشاره می کند. همانطور که ما حلقه می زنیم، این مکان نما به عنصر بعدی پیش می رود به طوری که وقتی متدها را روی شی تکرار کننده فراخوانی می کنیم، همیشه برای عنصری باشد که روی آن کار می کنیم.


وقتی بچه دار شدیم، می توانیم getChildren

حالا که می دانیم albums بچه دارد، ما می توانیم جدید بگیریم RecursiveArrayIterator شی برای آن آرایه با getChildren و در مورد آن تکرار کنید. برای مثال:

$rai = new RecursiveArrayIterator($albums);

foreach($rai as $k => $v) {
    print $k." => ";
    if($rai->hasChildren()) {
        $rai2 = $rai->getChildren();
        foreach($rai2 as $k2 => $v2) {
            @print PHP_EOL."  $k2 => $v2";
        }
    }
    else {
        print $v;
    }
    print PHP_EOL;
}
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

بزرگترین تغییری که در اینجا ایجاد کرده ایم این است که چه زمانی hasChildren برمی گرداند true، ما یک جدید دریافت می کنیم RecursiveArrayIterator شیء برای آن کودکان با getChildren و سپس روی آن حلقه بزنید. خروجی این است:

first_name => grant
last_name => horwood
albums => 
  0 => Array
  1 => Array
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

اکنون ما همه ابزارها را برای ایجاد یک تابع بازگشتی داریم که کل آرایه ما را مدیریت می کند.

در نهایت: ساخت یک تابع بازگشتی که کل آرایه ما را مدیریت می کند

ارتقاء حلقه ما به یک تابع بازگشتی اکنون فقط به انجام دو کار نیاز دارد:

  1. حلقه خود را در یک تابع قرار دهید
  2. تابع ما خودش را وقتی فراخوانی کند hasChildren برگشت true

به معنای واقعی کلمه همین است. بیایید نگاه بیندازیم:

$rai = new RecursiveArrayIterator($albums);

function recurse(RecursiveArrayIterator $rai) {
    foreach($rai as $k => $v) {
        if($rai->hasChildren()) {
            recurse($rai->getChildren());
        }
        else {
            print "$k => $v".PHP_EOL;
        }
    }
}

// call our recursive function
recurse($rai);
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

تابع ما یک آرگومان را می پذیرد، a RecursiveArrayIterator هدف – شی. وقتی آن تابع را فراخوانی می کنیم، روی سطح بالای آن آرایه حلقه می زند و جفت های کلید/مقدار را چاپ می کند. وقتی با مقداری مواجه می شود که hasChildren، یک تکرار کننده جدید با getChildren و سپس خود را با آن تکرار کننده جدید به عنوان آرگومان فراخوانی می کند و در سطح بعدی آرایه به پایین حلقه می زند. این کار تا زمانی ادامه می‌یابد که حلقه اول فاقد عناصر باشد.

خروجی به این شکل است

first_name => grant
last_name => horwood
title => pottymouth
artist => bratmobile
year => 1993
title => monks music
artist => monk, thelonious
year => 1957
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

این چیز عالی است به جز اینکه ما ساختار آرایه خود را از دست داده ایم. به یک سطح “مسطح” شده است.

البته، این هنوز هم می تواند مفید باشد. بسیاری از زبان‌ها دستورات «Flattening» داخلی برای انجام این کار دارند (من مقیاس‌های زیادی می‌نویسم و ​​از flatten روش همیشه).

اما شاید بخواهیم آن ساختار را حفظ کنیم. برای آن باید یک انباشته اضافه کنیم.

meme recursion
یک بازگشت موفق

اضافه کردن یک انباشته به عملکرد ما

همانطور که از نام آن پیداست، انباشتگر متغیری است که در آن نتایج تابع بازگشتی خود را در حین اجرا جمع می کنیم. وقتی بازگشت ما انجام شد، آن را برمی گردانیم. بیایید یکی را به تابع موجود خود اضافه کنیم:

$rai = new RecursiveArrayIterator($albums);

function recurse(RecursiveArrayIterator $rai, Array $accumlator) {
    foreach($rai as $k => $v) {
        if($rai->hasChildren()) {
            // add the return of our next call to recurse to our accumulator
            $accumlator[$k] = recurse($rai->getChildren(), []);
        }
        else {
            // add the key/value pair to the accumulator
            $accumlator[$k] = $v;
        }
    }

    return $accumlator;
}

// call our recursive function with an empty accumulator
$accumlator = recurse($rai, []);

// see the results
print_r($accumlator);
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

در اینجا، انباشتگر ما یک آرایه است. وقتی تماس می گیریم از یک خالی عبور می کنیم recurseو در بدنه تابع خود به جای چاپ جفت کلید/مقدار خود، آنها را به انباشته اضافه می کنیم. وقتی تابع ما خاتمه می یابد، انباشت کننده برگردانده می شود.

با نگاه کردن به مقدار بازگشتی، متوجه می شویم که ساختار حفظ شده است. در واقع، این آرایه دقیقاً همان آرایه ای است که به تابع خود ارسال کردیم. موفقیت نوع.

ساختن تابعی که همان آرایه ای را که ارسال شده برمی گرداند، عملکردی کاملاً پیشگامانه نیست. کاری که احتمالاً می خواهیم با تابع بازگشتی خود انجام دهیم این است که برخی از آن مقادیر را تبدیل یا فیلتر کنیم.

ما می‌توانیم این کار را انجام دهیم، البته، با افزودن مقداری کد به خطی که در آن مقداری را به انباشته اختصاص می‌دهیم. به عنوان مثال، اگر بخواهیم تمام مقادیر خود را بزرگ کنیم، می توانیم اضافه کنیم strtoupper مانند:

$accumlator[$k] = strtoupper($v);
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

یا اگر می خواستیم حذف کنیم year عنصری از هر آلبوم، می‌توانیم آن را فیلتر کنیم.

if($k != 'year') {
    $accumlator[$k] = $v;
}
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

اکنون ما در حال انجام یک کار مفید هستیم!

ساخت یک راه حل عمومی تر

بنابراین، اکنون می‌توانیم کل آرایه خود را طی کنیم و فیلترها یا تبدیل‌های دلخواه خود را اعمال کنیم. این خوب است، اما اگر بتوانیم یک تابع بازگشتی عمومی بنویسیم که فیلترها و تبدیل‌ها را به عنوان آرگومان می‌گیرد، بسیار بهتر است. بیا این کار را انجا دهیم.

$rai = new RecursiveArrayIterator($albums);

/**
 * Traverse the array applying $filter to each key and $transformer to each value
 *
 * @param  RecursiveArrayIterator $rai
 * @param  Array $accumlator
 * @param  callable $filter
 * @param  callable $transformer
 * @return array
 */
function recurse(RecursiveArrayIterator $rai, Array $accumlator, callable $filter = null, callable $transformer = null): Array {

    // a default filter that removes nothing
    $filter = $filter ?? fn($n) => true;

    // a default transformer that changes nothing
    $transformer = $transformer ?? fn($n) => $n;

    foreach($rai as $k => $v) {
        if($rai->hasChildren()) {
            $accumlator[$k] = recurse($rai->getChildren(), [], $filter, $transformer);
        }
        else {
            // test the filter and apply the transformer
            if($filter($k)) {
                $accumlator[$k] = $transformer($v);
            }
        }
    }

    return $accumlator;
}

// filter to apply to each key. remove elements with key 'year'
$filter = fn($n) => $n != 'year';

// transformer to apply to each value. apply uppercase to all values
$transformer = fn($n) => strtoupper($n);

// call our recursive function with an empty accumulator
$accumlator = recurse($rai, [], $filter, $transformer);

// see the results
print_r($accumlator);
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

ما در اینجا دو آرگومان جدید به تابع خود اضافه کرده ایم:

  • $filter: این یک فراخوانی است که کلید عنصر آرایه ما را به عنوان آرگومان می پذیرد. یک تست روی کلید انجام می دهد و یک Boolean برمی گرداند. در مثال بالا، ما تست می کنیم که آیا کلید است year و برگشت false اگر باشد. این قابل فراخوانی برای هر عنصر آرایه و فقط عناصری که برمی گردند اعمال می شود true به آکومولاتور ما اضافه می شوند.
  • $transformer: یک فراخوانی که مقدار عنصر را به عنوان آرگومان می پذیرد و برخی از عملکردها را برای آن مقدار اعمال می کند. در مثال، مقدار را با حروف بزرگ می کنیم strtoupper. وقتی این کد مثال را اجرا می کنیم، مقدار تبدیل شده به انباشتگر اضافه می شود، این را دریافت می کنیم:
Array
(
    [first_name] => GRANT
    [last_name] => HORWOOD
    [albums] => Array
        (
            [0] => Array
                (
                    [title] => POTTYMOUTH
                    [artist] => BRATMOBILE
                )
            [1] => Array
                (
                    [title] => MONKS MUSIC
                    [artist] => MONK, THELONIOUS
                )
        )
)
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

تمام عناصر کلید شده با year حذف شده اند و strtoupper برای تمام مقادیر اعمال شده است. دقیقا همان چیزی که ما خواسته ایم

اگر بگذریم nullبرای هر دو است $filter یا $transformer، از فراخوان های پیش فرض استفاده می شود: بدون فیلتر، بدون تبدیل.

این تکنیک در دنیای واقعی کاربرد دارد. مدت‌ها پیش، برای کار روزانه، بسته‌ای نوشتم تا رمزهای عبور و مواردی از این قبیل را از json خودسرانه ویرایش کنم تا آن چیزهای هکر را در فایل‌های لاگ خود ذخیره نکنیم. بازگشت با فیلترها و ترانسفورماتورها راه حل بود.

استفاده كردن RecursiveIteratorIterator برای صاف کردن آرایه ها

زمانی را به یاد می آورید که اولین بازگشت را انجام دادیم و آرایه چند بعدی ما به یک سطح صاف برگشت؟

ما می توانیم با استفاده از RecursiveIteratorIterator کلاس مثلا:

$rai = new RecursiveArrayIterator($albums);

// create a RecursiveIteratorIterator object
$rii = new RecursiveIteratorIterator($rai);

// loop over the RecursiveIteratorIterator
foreach($rii as $k => $v) {
    print "$k => $v".PHP_EOL;
}
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

این حلقه جایگزین کل تابع بازگشتی ما می شود و همان نتایج، یک آرایه مسطح را بیرون می دهد:

first_name => grant
last_name => horwood
title => pottymouth
artist => bratmobile
year => 1993
title => monks music
artist => monk, thelonious
year => 1957
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

اساسا، RecursiveIteratorIterator فقط یک تکرار کننده است که روی ما حلقه می زند RecursiveArrayIteratorو همه RecursiveArrayIteratorآن را شامل می شود. این یک تکرار کننده است که بر روی یک تکرار کننده تکرار می شود و همه تکرار کننده هایی که تکرار کننده شامل، به صورت بازگشتی است. این جمله در ابتدای این پست مضحک به نظر می رسید، اما اکنون کاملاً منطقی است.

استفاده كردن iterator_apply روی همه این چیزها

صاف کردن یک آرایه می تواند مزایای زیادی داشته باشد. به عنوان مثال، اگر بخواهیم فقط تمام هنرمندان مجموعه آلبوم خود را دریافت کنیم، می توانیم یک تابع بازگشتی با RecursiveArrayIterator و آن اطلاعات را استخراج کنیم، در غیر این صورت می‌توانیم کل موضوع را با هم صاف کنیم RecursiveIteratorIterator و هر مقدار کلید شده را بگیرید artist. روش صاف کردن بسیار سریعتر و آسان تر است.

چند راه برای استخراج داده ها از یک آرایه مسطح وجود دارد، اما اولین موردی که به آن اشاره می کنم راهی است که کار نمی کند: array_filter.

در نگاه اول، array_filter برای این کار عالی به نظر می رسد ما می خواهیم آرایه خود را فیلتر کنیم تا فقط دریافت کنیم artist، گذشته از همه اینها. با این حال، array_filter کلیدها را حفظ می کند این یک مشکل است زیرا، اگر به خروجی بالا نگاه کنیم، دو عنصر با یک کلید داریم: artist. با array_filter، همه آن کلیدها به هم می ریزند و ما فقط یک مقدار را به پایان می رسانیم: آخرین. خوب نیست.

مستقیم ترین رویکرد این است که به سادگی در ما فیلتر کنیم foreach حلقه

$rai = new RecursiveArrayIterator($albums);

$rii = new RecursiveIteratorIterator($rai);

$artists = [];
foreach($rii as $k => $v) {
    if($k == "artist") {
        $artists[] = $v;
    }
}

print_r($artists);
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

در اینجا، ما بر روی خود حلقه می زنیم RecursiveIteratorIterator و اگر کلید است artist ما آن را به خود اضافه می کنیم $artists آرایه. نتیجه مجموعه ای از نام هنرمندان است. کامل.

یا می توانستیم استفاده کنیم iterator_apply.

iterator_apply برای هر عنصر در یک تکرار کننده یک تابع اعمال می کند. در این معنا به نوعی شبیه است array_map. بیایید ببینیم چگونه می‌توانیم هنرمندانمان از آن استفاده کنند:

$rai = new RecursiveArrayIterator($albums);

$rii = new RecursiveIteratorIterator($rai);

// our filtering function to extract only 'artists'
$filter = fn($n) => $n == 'artist';

$artists = [];
iterator_apply($rii,
    function($n, $filter, &$artists) {

        // test the key with the filter
        if($filter($n->key())) {
            $artists[] = $n->current();
        }

        return true;
    },
    [$rii, $filter, &$artists]);

print_r($artists);
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

نگاه كردن iterator_apply، می بینیم که سه آرگومان می گیرد. اولین آرگومان تکرار کننده ما است. بسیار سرراست

آرگومان های دوم و سوم تابعی را که می خواهیم برای هر عنصر تکرار کننده اعمال کنیم را تعریف می کند. آرگومان دو خود تابع است، یک فراخوانی. آرگومان سوم آرایه ای است که تمام آرگومان هایی را که به آن تابع ارسال می کنیم را نشان می دهد. اگر تابعی که در آرگومان دو تعریف کردیم، پنج آرگومان بگیرد، آرایه در اینجا باید پنج عنصر داشته باشد. اولین آرگومان تابع ما و به تبع آن اولین عنصر آرایه ما باید همیشه خود تکرار کننده باشد.

در مثال بالا، تابعی که به iterator_apply می‌دهیم سه آرگومان دارد: تکرارکننده، یک فراخوانی که برای فیلتر کردن عناصر موجود در iterator استفاده می‌کنیم، و یک انباشته به نام $artists. ما در حال عبور هستیم $artists توسط مرجع تا بتوانیم آن را در بدنه تابع تغییر دهیم و پس از اجرا نتایج را در دسترس خود قرار دهیم.

در بدنه تابع، کلید عنصر جاری تکرار کننده را با فراخوانی دریافت می کنیم key() روش و آن را به ما منتقل کنید $filter، قابل فراخوانی ما است که تعیین می کند آیا آن کلید وجود دارد یا خیر artists. اگر $filter مقدار true را برمی گرداند، با فراخوانی مقدار عنصر فعلی تکرار کننده را بدست می آوریم current() و آن را به آکومولاتور خود اضافه کنیم $artists.

صادقانه بگویم، این رویکرد نه آسان است و نه واضح و در نتیجه، اکثر مردم فقط از آن استفاده می کنند foreach حلقه ها ولی iterator_apply بسیار قدرتمند است، به خصوص اگر یک تابع پیچیده یا از قبل موجود داشته باشیم که می خواهیم آن را اعمال کنیم. دانستن وجود آن ارزشمند است، حتی اگر مجبور باشیم هر بار که از آن استفاده می کنیم کتابچه راهنمای آن را دوباره بخوانیم.

🔎 این پست در اصل در وبلاگ فنی گرنت هوروود نوشته شده است

نوشته های مشابه

دیدگاهتان را بنویسید

نشانی ایمیل شما منتشر نخواهد شد. بخش‌های موردنیاز علامت‌گذاری شده‌اند *

دکمه بازگشت به بالا