پیمایشگر (Iterator)

Iterator بلوک کدی است که، شامل همه مقادیری است که در یک حلقه foreach مورد استفاده قرار می‌گیرد. یک کلاس که نماینده یک کلکسیون است می‌تواند رابط System.Collections.IEnumerable را پیاده سازی کند. این رابط نیاز به پیاده سازی متد ()GetEnumerator دارد که یک رابط IEnumerator را بر می‌گرداند. رابط IEnumerator دارای خاصیت Current می‌باشد که مقدار جاری برگشت داده شده بوسیله iterator را در بر دارد. این رابط (IEnumerator) همچنین دارای متد ()MoveNext است که خاصیت Current را به سوی آیتم بعدی حرکت می دهد و در صورت عدم وجود آیتم مقدار false را بر می‌گرداند. متد ()Reset حلقه پیمایش را به اولین آیتم بر می‌گرداند. رابط IEnumerator توسط کلکسیون‌های مختلف دات نت پیاده سازی می‌شود که یکی از این کلکسیون‌ها آرایه‌های می‌باشند. پس این سؤال پیش می‌آید که چگونه با استفاده از حلقه foreach می‌توان به اجزای این کلکسیون‌ها دسترسی پیدا کرد؟ فرض کنید کدی شبیه به کد زیر داریم که همه عناصر یک آرایه را با استفاده از حلقه foreach می‌خواند.

int[] numbers = { 1, 2, 3, 4, 5 };

foreach(int n in numbers)
{
    Console.WriteLine(n);
}

برای درک بهتر کاربرد تکرارکننده‌ها (iterators)، اجازه دهید که حلقه foreach کد بالا را به فراخوانی متد ()GetEnumerator آرایه ترجمه کنیم :

int[] numbers = { 1, 2, 3, 4, 5 };

IEnumerator iterator = numbers.GetEnumerator();
while (iterator.MoveNext())
{
    Console.WriteLine(iterator.Current);
}

همانطور که مشاهده می‌کنید ما ابتدا یک پیمایشگر آرایه را با استفاده از متد ()GetEnumerator که یک رابط IEnumerator را بر می‌گرداند به دست می‌آوریم. سپس از این پیمایشگر در حلقه while استفاده کرده و متد ()MoveNext را فراخوانی می‌کنیم. متد ()MoveNext اولین عنصر یک کلکسیون مانند آرایه را بر می‌گرداند و اگر عملیات به دست آوردن اولین عنصر موفقیت آمیز باشد مقدار true را بر می‌گرداند. در فراخوانی بعدی دومین عنصر آرایه را بر می‌گرداند و این کار را تا آخرین عنصر آرایه انجام می‌دهد و وقتی که به پایان عناصر رسید مقدار false را برگشت می‌دهد. مقدار برگشت داده شده یک عنصر به وسیله خاصیت IEnumerator.Current قابل دسترسی است. برای استفاده از iterator نیاز به دستور yield return داریم. این دستور (yield) با دستور return متفاوت است. یکی از تفاوت‌های مشهود این دو، استفاده از کلمه کلیدی yield قبل از کلمه کلیدی return می‌باشد. yield يك عنصر مجموعه را برمي گرداند و موقعيت مكان نما را به عنصر بعدي هدايت می‌کند. به کد زیر توجه کنید :

public static IEnumerable GetMessages()
{                                      
    yield return "Message 1";          
    yield return "Message 2";          
    yield return "Message 3";          
}                                      

public static void Main()
{
    foreach (string message in GetMessages())
    {
         Console.WriteLine(message);
    }
}
Message 1
Message 2
Message 3

متد ()GetMessages یک شئ IEnumerable را بر می‌گرداند که، شامل تعریفی برای یک متد ()GetEnumerator می‌باشد. مقدار جلوی اولین دستور yield return در متغیر message دستور foreach قرار می‌گیرد و چاپ می‌شود. در فراخوانی بعدی متد ()GetMessages در حلقه foreach مقدار جلوی دومین دستور yield return چاپ می‌شود و این کار تا آخرین دستور yield return ادامه می‌یابد. برای توقف مقادیر برگشتی از متد می‌توان از دستور yield break به صورت زیر استفاده کرد :

public static IEnumerable GetMessages()
{
     yield return "Message 1";
     yield return "Message 2";
     yield break;
     yield return "Message 3";
}

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

   1: using System.Collections;
   2: using System;
   3: 
   4: public class Names : IEnumerable
   5: {
   6:     private ArrayList innerList;
   7: 
   8:     public Names(params object[] names)
   9:     {
  10:         innerList = new ArrayList();
  11: 
  12:         foreach (object n in names)
  13:         {
  14:             innerList.Add(n);
  15:         }
  16:     }
  17: 
  18:     public IEnumerator GetEnumerator()   
  19:     {                                    
  20:         foreach (object n in innerList)  
  21:         {                                
  22:             yield return n.ToString();   
  23:         }                                
  24:     }                                    
  25: }
  26: 
  27: public class Program
  28: {
  29:     public static void Main()
  30:     {
  31:         Names nameList = new Names("John", "Mark", "Lawrence", "Michael", "Steven");
  32: 
  33:         foreach (string name in nameList)
  34:         {                                
  35:             Console.WriteLine(name);     
  36:         }                                
  37:     }
  38: }
John
Mark
Lawrence
Michael
Steven

ابتدا یک کلاس مجموعه‌ای به نام Names که شامل لیستی از نام‌ها می‌باشد را ایجاد می‌کنیم (خطوط 25-4). همانطور که در تعریف کلاس در خط 4 مشاهده می‌کنید، ما کلاس CollectionBase را پیاده سازی نکرده‌ایم. چون که کلاس CollectionBase رابط IEnumerable را پیاده سازی می‌کند و در نتیجه دارای یک پیاده سازی از متد ()GetEnumerator این رابط نیز است. ما یک کلاس مجموعه‌ای را از صفر ایجاد و یک پیمایشگر سفارشی را تعریف کرده‌ایم. از آنجاییکه کلاس ما رابط IEnumerable را پیاده سازی کرده است پس لازم است که متد ()GetEnumerator از این رابط را هم پیاده سازی کند. متد ()GetEnumerator یک شمارنده است که کلکسیون‌ها را شمارش می‌کند. کلکسیون به مجموعه‌ای از عناصر هم نوع که الزاماً در حافظه پشت سر هم نیستند گفته می‌شود. در خطوط 24-18 پیمایشگر سفارشی را تعریف کرده‌ایم. در داخل آن به پیمایش هر یک از مقادیر فیلد innerList می‌پردازیم. هر مقدار به رشته تبدیل و و سپس با استفاده از دستور yield به متد فراخوان ارسال می‌شود.
خطوط 36 – 33 پیمایشگر ما را در عمل نمایش می‌دهد. از آنجاییکه دستور yield در پیمایشگرمان هر مقدار را به نوع رشته تبدیل می‌کند پس به راحتی می‌توانیم از نوع string در حلقه foreach استفاده کنیم (خط 33). وقتی که یک مقدار به وسیله پیمایشگر از طریق حلقه foreach واقع در متد ()Main برگدانده می‌شود، حلقه foreach به آیتم بعدی رفته و دستور yield مقدار پیمایش شده از innerList را بر می‌گرداند. بدون پیمایشگر ما قادر به استفاده از حلقه foreach در کلاسمان نیستیم. ایجاد پیمایشگر سفارشی به ما این قدرت را می‌دهد که کنترل بیشتری بر رفتار حلقه foreach هنگام کار با کلاس داشته باشیم. به عنوان مثال می‌توانیم پیمایشگر را به این صورت اصلاح کنیم که فقط نام‌هایی که با حرف M شروع می‌شوند را برگرداند :

public IEnumerator GetEnumerator()
{
    foreach (object n in innerList)
    {
        if (n.ToString().StartsWith("M"))
            yield return n.ToString();   
    }
}

همانطور که در کد بالا مشاهده می‌کنید برای تشخیص اینکه چه نامی با حرف M شروع شده است از متد ()StartsWith مربوط به کلاس System.String استفاده کرده‌ایم. اگر نام با حرف M شروع شده باشد، دستور yield آن را بر می‌گرداند، در غیر اینصورت، از آن رد شده و به نام بعدی در innerList را مورد بررسی قرار می‌دهد. با دستکاری متد ()GetEnumerator، هنگام استفاده از حلقه foreach در یک نمونه از کلاس Names، می‌توانیم فقط نام‌هایی را که با حرف M شروع می‌شوند را به دست آوریم. اگر چه می‌توانید از تکنیک بالا استفاده کرده و متد ()GetEnumerator تغییر دهید ولی بهتر است از یک متد جدا برای به دست آوردن نام‌هایی که با یک حرف خاص شروع می‌شوند استفاده کنید :

public IEnumerable GetNamesStartingWith(string letter)
{
    foreach (object n in innerList)
    {
        if (n.ToString().StartsWith(letter))
            yield return n.ToString();
    }
}

پیمایشگر بالا نسبت به پیمایشگر قبلی منعطف‌تر بوده و شما می‌توانید با استفاده از آن نام‌هایی که با یک حرف یا زیررشته خاص شروع شده‌اند را پیدا کنید. به این نکته توجه کنید که در پیمایشگر بالا از IEnumerable به جای IEnumerator استفاده کرده‌ایم. IEnumerable دارای متد ()GetEnumerator است. هنگام فراخوانی این پیمایشگر لازم است که حلقه foreach خطوط 36-33 را به صورت زیر تغییر دهید :

foreach (string name in nameList.GetNamesStartingWith("M"))
{
    Console.WriteLine(name);
}