مدیریت حافظه درPHP‌ - دستور Foreach


بسم الله الرحمن الرحیم
برخی توسعه‌دهنده گان، و یا حتی توسعه‌دهنده گان با تجربه نیز از نحوه مدیریت آرایه ها در حلقه های foreach توسط php‌ بطور دقیق اطلاعی ندارن! / شایدم دارن حالا :) / بطور پیش‌فرض تو foreach ، خود php یه کپی از آرایه ای که در حلقه استفاده می شه می گیره و کپی گرفته شده هم به محق اینکه عملیات foreach تموم می شه از بین می رهکه این موضوع در عملیات ساده در حلقه foreach به وضوع روشنه.

به عنوان مثال:

$set = array("apple", "banana", "coconut");
foreach ( $set AS $item )
{
echo "{$item}\n";
}

و خروجی:

apple
banana
coconut



حتی با وجود اینکه از آرایه کپی شده، توسعه‌دهنده گاهی به این موضوع توجه نمی کنه، برای اینکه به آرایه اصلی نه در بین حلقه (عملیاتو نه بعد از اتمام حلقه ها اشاره‌ای نمی شههرچند که وقتی شما سعی می‌کنید عناصر آرایه رو بین حلقه ها ویرایش کنین و بعد از اتمام عملیات متوجه می شین که تغییری در آرایه اصلی ایجاد نشده:

$set = array("apple", "banana", "coconut");
foreach ( $set AS $item )
{
$item = strrev ($item);
}
print_r($set);

و خروجی:
Array
(
[0] => apple
[1] => banana
[2] => coconut
)

هیچ تغییری تو آرایه اصلی اعمال نشدهحتی با وجود اینکه بطور صریح مقداری رو به $item اختصاص دادیمدلیل این موضوع هم به این خاطره که، عملیاتی که روی $item داریم انجام می دیم، در حقیقت این عملیات روی کپی گرفته شده از آرایه اصلی داره اتفاق می افتهکپی ای که php خودش بطور پیش‌فرض از آرایه مون گرفته.

برای نقض این مسأله که بتونیم عناصر آرایه اصلی رو تغییر بدیم، می تونیم $item رو بوسیله رفرنس (reference) بکار ببریم:

$set = array("apple", "banana", "coconut");
foreach ( $set AS &$item )
{
$item = strrev($item);
}
print_r($set);

و خروجی:
Array
(
[0] => elppa
[1] => ananab
[2] => tunococ
)

همونطور که می بینین، وقتی عملیاتی روی $item که بوسیله رفرنس صدا زده شده، اعمال می شه، تغییرات اعمال شده به $item مستقیم روی عنصر مربوطه در آرایه اصلی اعمال می شههمچنین استفاده از $item بوسیله رفرنس از کپی گرفته شدن آرایه توسط php جلوگیری می کنهبرای تست صحت این موضوع، ابتدا یه اسکریپتی برای به نمایش درآوردن آرایه کپی شده به شما نشون می دیم:

$set = array("apple", "banana", "coconut");
foreach ( $set AS $item )
{
$set[] = ucfirst($item);
}
print_r($set);

و خروجی:
Array
(
[0] => apple
[1] => banana
[2] => coconut
[3] => Apple
[4] => Banana
[5] => Coconut
)

در این مثال، php از آرایه $set کپی گرفته تا عملیات رو روی کپی انجام بده، اما وقتی که $set داخل حلقه استفاده شده، php عناصر جدیدی رو به آرایه اصلی اضافه کرده و نه به آرایه ای که ازش کپی گرفته بود.
بطور ساده تر، php فقط از آرایه کپی شده برای اجرای حلقه ها و اعمال تغییرات به $item استفاده می کنهبه این خاطر حلقه فوق، فقط برای ۳ بار اجرا می شه و هر بار عنصر دیگری رو به آخر آرایه اصلی اضافه می کنه، که باعث می شه آرایه اصلی دارای تا عنصر بشه ولی هیچ وقت داخل حلقه بی نهایت نمی افته.

حالا مسئله اینه که، چطور می‌شد اگه $item رو بوسیله رفرنس استفاده کرده بودیم؟
برای جواب این سوال، کاراکتر رو به مثال فوق اضافه می‌کنیم و ببینیم نتیجه چه خواهد بود:

$set = array("apple", "banana", "coconut");
foreach ( $set AS &$item )
{
$set[] = ucfirst($item);
}
print_r($set);

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

ini_set("memory_limit","1M");

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

اما موضوع فقط به همینجا ختم نمی شهدر صورتی که آرایه مون رو بصورت رفرنس در جایی از اسکریپت استفاده کرده باشیم PHP‌ نمی تونه از آرایه کپی بگیرهطبق سناریویی که در بالا داشتیم، می دونیم که استفاده از $item بصورت رفرنس باعث حلقه نامحدود می شه، اما اگه $set در جای دیگه ای از اسکریپت مون بصورت رفرنس استفاده شده باشه، حتی در صورتی که از رفرنس در خود foreach استفاده نشده باشه هم باعث شکست می شه و با مشکل روبرو می شیم:

$set = array("apple", "banana", "coconut");
$a = &$set;
foreach ( $set AS $item )
{
$set[] = ucfirst($item);
}

که باعث می شه تو حلقه نامحدودی بیفته، حتی بازم اگه $item بوسیله رفرنس استفاد نشده باشهاستفاده از $a بجای $set در foreach هم همین نتایج یکسانی رو در بر خواهد داشت و فرقی نمی کنه.

این به این معنی نیست که بگیم $item بطور غیرمستقیم توسط رفرنس استفاده شده وقتی که $set جای دیگه ای بصورت رفرنس استفاده شده باشهبرای اثبات این قضیه، مثال زیر رو ببینید:

$set = array("apple", "banana", "coconut");
$a = &$set;
foreach ( $a AS $item )
{
$item = ucfirst($item);
}
print_r($set);

و خروجی:
Array
(
[0] => apple
[1] => banana
[2] => coconut
)

همونطور که می‌بینید مقدار اولیه عنصرهای آرایه $set تغییر نکرده، به این خاطر که حتی با وجود اینکه $set توسط رفرنس استفاده شده است، و $set کپی هم نشده، به $item فقط محدوده lexical یا همون (lexical scope) در رابطه با حلقه ای که توش هست داده شده و تغییرات اعمال شده رو به $set برگشت نخواهد دادو هنوزم در صورتی که می خوایید تغییرات بر روی آرایه اصلی اعمال بشه باید از رفرنس استفاده کنید.
به مثال زیر دقت کنین:

$set = array("apple", "banana", "coconut");
$a = &$set;
foreach ( $a AS &$item )
{
$item = strrev($item);
}
print_r($set);

و خروجی زیر رو خواهم داشت:
Array
(
[0] => elppa
[1] => ananab
[2] => tunococ
)

همه این مثال‌های فوق در آرایه های انجمنی هم در foreach‌ به سینتکس ($set AS $key => $item) صادقن و کار می کننهمچنین $key هیچ وقت نمی تونه توسط رفرنس استفاده بشه و در صورتی که استفاده بشه با خطای Fatal رو برو خواهیم شددر نتیجه این تکنیک هایی که برای ویرایش مقدار عناصر آرایه (مقادیر و نه کلیدهااستفاده شدن، برای ویرایش کلید های عناصر آرایه جواب نخواهد دادهر چند که می تونید کلید های جدیدی در آرایه ایجاد کرده و یا کلید های موجود در آرایه رو مثل مثال زیر حذف کنین:

$set = array("apple"=>"red","banana"=>"yellow","coconut"=>"brown");
foreach ( $set AS $key => $item )
{
$set[ucfirst($key)] = $item;
unset($set[$key]);
}
print_r($set);

و خروجی هم:
Array
(
[Apple] => red
[Banana] => yellow
[Coconut] => brown
)

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

$set = array("apple"=>"red","banana"=>"yellow","coconut"=>"brown");
$a = &$set;
foreach ( $set AS $key => $item )
{
$set[ucfirst($key)] = $item;
unset($set[$key]);
}
print_r($set);

و خروجی:
Array
(
)

برای اینکه قبلاً به آرایه اصلی رفرنسی شده بود و ازش در foreach کپی هم نشده است، و در نتیجه وقتی سعی در تغییر ساختار فیزیکی آرایه می‌گیرید با نتایج پیش‌بینی نشده‌ای روبرو می شید، بخصوص هنگام استفاده از تابع unset().

بدون استفاده از تابع unset() در این مثال، شما در‌ واقع عملیات رو بر روی آرایه اصلی انجام می‌دید و حلقه ها هم از روی آرایه اصلی و به همون تعداد خواهد بود پس در نتیجه با حلقه بی نهایت روبرو خواهید شد، اما از اونجایی که برای $set کلید ($key) رو مشخص می‌کنیم پس در نتیجه تو حلقه بی نهایت هم نخواهد افتاد.

به مثال زیر توجه کنید:

$set = array("apple"=>"red","banana"=>"yellow","coconut"=>"brown");
$a = &$set;
foreach ( $set AS $key => $item )
{
$set[ucfirst($key)] = $item;
}
print_r($set);

و خروجی مون هم:
Array
(
[apple] => red
[banana] => yellow
[coconut] => brown
[Apple] => red
[Banana] => yellow
[Coconut] => brown
)

برای اینکه ثابت کنیم هنوزم امکان افتادن تو حلقه نامحدود از همین تکه کد بالا وجود داره، تکه کد $set[] رو داخل حلقه مون اضافه می کنیم:

$set = array("apple"=>"red","banana"=>"yellow","coconut"=>"brown");
$a = &$set;
foreach ( $set AS $key => $item )
{
$set[ucfirst($key)] = $item;
$set[] = $item;
}
print_r($set);

که باعث ایجاد حلقه بی نهایت می شه.

از کاراهای جالبی که می شه توسط همین سینتکس $key => $item زمانی که آرایه کپی شده باشه و بدون ترس از اینکه مشکلی پیش بیاد انجام داد، تغییر ساختار آرایه اصلی هست:

$set = array("apple"=>"red","banana"=>"yellow","coconut"=>"brown");
foreach ( $set AS $key => $item )
{
$set[] = ucfirst($item);
unset($set[$key]);
}
print_r($set);

و خروجی زیر رو خواهیم داشت:
Array
(
[0] => Red
[1] => Yellow
[2] => Brown
)

همانطور که در این مثال می بینید، از آرایه اصلی، برای استفاده در حلقه کپی گرفته شده بودو $set های داخل حلقه به نسخه خارجی یا همون آرایه اصلی خارج از حلقه اشاره داره، در نتیجه استفاده از تابع unset() و تکه کد $set[] برای افزودن روی آرایه اصلی به ما آرایه جدید شامل عناصر با مقادیر ای که با حروف بزرگ شروع شده‌اند بدون کلید هاشون تحویل می ده.

و البته با کمی تغییر در کد فوق می تونیم کلید ها رو هم حفط کنیم:

$set = array("apple"=>"red","banana"=>"yellow","coconut"=>"brown");
foreach ( $set AS $key => $item ) {
unset($set[$key]);
$set[$key] = ucfirst($item);
}
print_r($set);

خروجی هم:
Array
(
[apple] => Red
[banana] => Yellow
[coconut] => Brown
)

این اطلاعات بیشتر برای توسعه دهندگانی مفید می تونه باشه که قصد دارن حفره های مربوط به حافظه و موجود تو اسکریپت های نوشته شده با PHP رو بگیرناگر شما از دستور foreach برای آرایه ای از آبجکت ها که می تونن حجم حدود 50 مگ رو داشته باشن استفاده می کنید، در حقیقت بدون دلیل دارید کپی ای از همون ساختار تو حافظه ایجاد می کنیداگه تو حلقه ها قصد تغییر ساختار آرایه رو ندارید و یا عنصر جدیدی بهش اضافه نخواهید کرد، بهتر خواهد بود که از تکه کد $a = &$array; استفاده کنید تا کپی ای بدون دلیل از آرایه گرفته نشه و بدون دلیل هم منابع سیستم استفاده نشه.

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


نکته‌ دیگه ای که در مورد آرایه ها با حجم زیاد هست، می تونیم از کلاس‌های استاندارد خود PHP هم مثل arrayIterator‌ استفاده کنیم+

منبع مطلب+

امیدوارم مفید بوده باشه :)

پست‌های معروف از این وبلاگ

آپدیت نشدن View و ثابت ماندن مقدار فیلد Input در Angular

رویدادها در MySQL

لیست شهر و استان‌های ایران