DEV Community

Myo Zaw Latt
Myo Zaw Latt

Posted on

IEnumerable, Iteration and Deferred Execution: behind the scenes

Programming basic လောက်ကိုသိတဲ့လူတယောက်အဖို့တော့ Array ဆိုတာ သိပ်မစိမ်းတဲ့ Data structure ဖြစ်နေမှာပါ။ C# မှာ Array ကို Access လုပ်ဖို့ Indexing, Looping/Iteration, Linq စသဖြင့် နည်းလမ်းများစွာရှိပါတယ်။ အဲဒီလို Collection-like data structure တွေအတွက်အဓိက အရေးပါတာကတော့ IEnumerable interface ပဲဖြစ်ပါတယ်။ ဒီ Post မှာတော့ C# မှာ Collection data structure တွေအတွက် အဓိက ထောက်ပံ့ပေးထားတဲ့ IEnumerable, IEnumerator နဲ့ Iteration တွေအကြောင်း ပြောပြသွားမှာပါ။

Iteration

Array ကို Access လုပ်ဖို့ မိမိနှစ်သက်တဲ့ Looping ပုံစံကိုသုံးနိုင်ပါတယ်။ ဒါပေမဲ့ ဘယ် Looping style ပဲသုံးသုံး Compiler က while loop အဖြစ်ပြောင်းရေးလိုက်မှာပါ။ ဒီလိုပါ။ ဆိုကြပါစို့။ အောက်မှာ Array တခုတည်ဆောက်ပြီး Looping ပုံစံနှစ်ခုနဲ့ Access လုပ်လိုက်ပါတယ်။

int[] intArray = new int[3];

for (int i = 0; i < intArray.Length; i++)
{

}
foreach (var number in intArray)    
{

}
Enter fullscreen mode Exit fullscreen mode

ဒါကို Compiler က ဒီလိုပြန်ရေးမှာပါ။

// for loop
int[] intArray = new int[3];
int i = 0;
while (i < intArray.Length)
{
     i++;
}

//foreach loop
int[] array = intArray;
int num = 0;
while (num < array.Length)
{
     int number = array [num];
     num++;
}
Enter fullscreen mode Exit fullscreen mode

အခြေခံအားဖြင့် တူညီတဲ့ While looping ပါဘဲ။ ဒါပေမဲ့ ဒီ Array ကိုဘဲ IEnumrable variable ထဲပြောင်းထည့်ပြီး Looping ပြန်ပတ်လိုက်ရင်တော့ ကွဲပြားတဲ့ပုံစံကို တွေ့ရမှာပါ။

IEnumerable  numerated = intArray;
foreach (var number in numerated)
{

}
Enter fullscreen mode Exit fullscreen mode

တခုသတိထားပြီးကြည့်ရင် numerated ဆိုတဲ့ Variable ကို for loop နဲ့ looping ပတ်လို့မရတာကိုတွေ့ရမှာပါ။ ဘာလို့လဲဆိုတော့ သူ့မှာ Computed property မပါတော့လို့ပါ။ အပေါ်က Array မှာတုန်းကတော့ Length property ကို Counter ထားပြီး Loop ကို exit လုပ်နိုင်ပါတယ်။ numerated variable ကို foreach နဲ့ပတ်ထားတဲ့ Looping ကိုတော့ Compiler က ဒီလိုပြန်ရေးမှာပါ။

IEnumerable numerated = intArray;
IEnumerator enumerator = numerated.GetEnumerator();
try
{
     while (enumerator.MoveNext())
     {
          object number2 = enumerator.Current;
     }
}
finally
{
     IDisposable disposable = enumerator as IDisposable;
     if (disposable != null)
     {
          disposable.Dispose();
     }
}
Enter fullscreen mode Exit fullscreen mode

ဒီနေရာမှာ Compiler ပြန်ရေးတဲ့ while loop က ထူးခြားနေတာကိုတွေ့ရမှာပါ။ မူရင်း instance ကနေပြီးတော့ Enumerator object တခုရယူပြီးတော့ အဲဒီ Enumerator မှာရှိတဲ့ MoveNext() method ကို Counter ထားပြီး Loop ပတ်သွားပါတယ်။ ဒါက Iterator pattern ကို အသုံးပြုထားတာဖြစ်ပါတယ်။ နောက်ထပ်ထူးခြားတာကတော့ try-finally block ကိုသုံးထားပြီး Enumerable object ကို Dispose လုပ်ထားတာပါ။

C# မှာရှိတဲ့ Object တိုင်းဟာ Object class က Inherit လုပ်ထားသလိုမျိုးဘဲ Array တိုင်းဟာ Array class ကို Inherit လုပ်ပါတယ်။ ဒါက Language Semantic ဖြစ်ပါတယ်။ ဒီတော့ Array instance တခုဟာ Array class မှာရှိတဲ့ Public method တွေကို ရရှိပါတယ်။ Array class ဟာ IList interface ကို Implement လုပ်ထားပြီး IList interface က IEnumerable interface ကို ICollection interface ကဆင့် inherit လုပ်ထားပါတယ်။ ဒါကြောင့်မလို့ IEnumerable variable ထဲကို Array instance assign လုပ်လို့ရတာပါ။

IEnumerable interface မှာ IEnumerator interface ကို return လုပ်ထားတဲ့ GetEnumerator() ဆိုတဲ့ Method တခုဘဲပါပါတယ်။ IEnumerator interface မှာ MoveNext() နဲ့ Reset() method နှစ်ခုနဲ့ Current ဆိုတဲ့ Read-only property တခုပါပါတယ်။ ဒီ Interface နှစ်ခုက C# ရဲ့ Iterator contract ပါ။

Iterator pattern
Iterator pattern ဆိုတာ နာမည်ကြီး GOF pattern တခုပါဘဲ။ Pattern definition ကတော့ Collection ထဲက Element တွေကို သူ့ရဲ့ဖွဲ့စည်းတည်ဆောက်ပုံကို သိစရာမလိုဘဲ တခုချင်းစီ Access လုပ်နိုင်ဖို့ပါ။ Iterator တခုမှာ Collection ထဲမှာ Element တွေရှိသေးရဲ့လားဆိုတာ စစ်တဲ့ Flag တခုနဲ့ လက်ရှိရောက်နေတဲ့ Element ရနိုင်တဲ့ Property တခုပါရှိရမှာပါ။ Iterable Collection တခုဖြစ်ဖို့ဆိုရင် Iterator တခုအမြဲရှိရမှာဖြစ်ပါတယ်။ ဒါကြောင့်မလို့ C# မှာဆိုရင် IEnumerable interface ဟာ Iterable collection ရစေဖို့အတွက် Iterator ဖြစ်တဲ့ IEnumerator interface ကို contract အနေနဲ့ ထည့်ထားတာဖြစ်ပါတယ်။
ဒီတော့ IEnumerable interface ကို Implement လုပ်ထားတဲ့ Collection တခုရဖို့ ဒီလိုရေးရမှာပါ။

using System.Collections;
class NameCollection : IEnumerable
{
    private readonly string[] _names = { "A name", "B name", "C name" };
    public NameCollection()
    {

    }
    public NameCollection(string[] names)
    {
        _names = names;
    }
    public IEnumerator GetEnumerator()
    {
        return new NameEnumerator(this);
    }

    private class NameEnumerator(NameCollection _nameCollection) : IEnumerator
    {
        private int _position = -1;
        public object Current => _position == -1 || _position >= _nameCollection._names.Length
            ? throw new InvalidOperationException()
            : _nameCollection._names[_position];


        public bool MoveNext()
        {
            _position++;
            return _position < _nameCollection._names.Length;
        }

        public void Reset()
        {
            _position = -1;
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

Client code က ဒါမျိုးဖြစ်မှာပါ။

var names = new NameCollection();
foreach (var name in names)
    Console.WriteLine(name);

var names2 = new NameCollection(["New name 1", "New name 2", "New name 3"]);
var enumearator = names2.GetEnumerator();
while (enumearator.MoveNext())
    Console.WriteLine(enumearator.Current);
Enter fullscreen mode Exit fullscreen mode

အပေါ်က code မှာ storage အနေနဲ့ Array ကိုပဲ backing field အနေနဲ့သုံးထားပါတယ်။ NameCollection ဟာ Looping ပုံစံနှစ်ခုလုံးနဲ့ အလုပ်လုပ်နိုင်တာကိုတွေ့ရမှာပါ။ Enumerable collection တခုကို Access လုပ်ဖို့အတွက် foreach နဲ့ while မှာဆိုရင်တော့ ကျနော့်အနေနဲ့ foreach loop ကိုဘဲရွေးချယ်ဖို့ အကြံပြုပါတယ်။ ဘာလို့လဲဆိုရင် foreach loop က Enumerator ကို Disposable ဖြစ်မဖြစ်စစ်ပေးပြီး တကယ်လို့ Disposable ဖြစ်တဲ့ Enumerator ဆိုရင် finally block ထဲမှာ တခါတည်း Dispose လုပ်ပေးသွားမှာမလို့ပါ။ ဒါကဘာနဲ့တူသလဲဆိုတော့ foreach က using block ကို shadow အနေနဲ့ သုံးပေးထားတာနဲ့တူပါတယ်။ ဒါဟာ Memory safety အတွက်အလွန်ကောင်းပါတယ်။ Disposable enumerator တွေက ဘယ်မှာတွေ့ရတတ်လဲဆိုတော့ System.Collections.Generic namespace အောက်မှာရှိတဲ့ IEnumerator<T> interface ကို implement လုပ်ထားတဲ့ enumerator မှာတွေ့ရပါတယ်။ IEnumerator<T> interface ဟာ IDisposable interface ကို inherit လုပ်ထားတာမလို့ Dispose() method ကိုပါ တခါတည်း Implement လုပ်ပေးရမှာပါ။ ဒီတော့ IEnumerable ကို Implement လုပ်ထားတဲ့ Collection ကို foreach နဲ့ loop ပတ်နိုင်တာဘာကြောင့်လဲ နားလည်လောက်ပြီ ယူဆပါတယ်။ ဒါက C# ရဲ့ Native iteration အကြောင်းပါဘဲ။

Deferred execution

Iterator/Enumerator တခုကို IEnumeartor interface ကို Implement မလုပ်ဘဲ yield statement သုံးပြီး တည်ဆောက်နိုင်ပါတယ်။ အောက်မှာကြည့်ပါ။

using System.Collections;
class NameCollection2 : IEnumerable
{
    public IEnumerator GetEnumerator()
    {
        yield return "A name";
        yield return "B name";
        yield return "C name";
    }
}
Enter fullscreen mode Exit fullscreen mode

အပေါ်မှာရေးထားတဲ့ Collection နဲ့ပုံစံတူ Collection တခုကိုရရှိမှာဖြစ်ပါတယ်။ ဒါပေမဲ့ ဒီမှာရရှိလာတဲ့ Enumerator ကတော့ Concrete class instance တခုမဟုတ်ဘဲ Compiler generate လုပ်ပေးလိုက်တဲ့ State machine instance ပဲဖြစ်ပါတယ်။ ဒါကို Lazy evaluation လို့ခေါ်တာပါ။ State machine instance ကို Access လုပ်တဲ့အခါမှာတော့ on-demand execution ပုံစံနဲ့လုပ်ဆောင်ပါတယ်။ Deferred execution လို့လည်းခေါ်ပါတယ်။ ဘယ်လိုပုံစံလဲဆိုတော့ Compiler က yield statement ကိုတွေ့ပြီဆိုတာနဲ့ သက်ဆိုင်ရာလိုအပ်ချက်နဲ့ ကိုက်ညီတဲ့ State machine class တခုကို method နာမည်နဲ့ generate လုပ်ပါတယ်။ အဲဒီ့ class က IEnumerator interface ကို Implement လုပ်ထားပါတယ်။ State machine မှာ Indicator တခုပါပြီးတော့ အဲဒီ Indicator က yield keyword ရှိတဲ့နေရာတိုင်းကိုမှတ်ထားပေးမှာပါ။ MoveNext() method ကိုတခါခေါ်တိုင်း state က တခါပြောင်းသွားပြီး yield statement ကို return လုပ်ပေးမှာပါ။ ပြီးရင်တော့ နောက်မှာရှိတဲ့ yield statement နေရာမှာ နောက်တခါ MoveNext() ကိုမခေါ်မချင်း Pause လုပ်ထားမှာပါ။ နောက်တခါ MoveNext() ကို ထပ်ခေါ်ရင် Pause ထားတဲ့နေရာကနေ ပြန် Run ပေးမှာပါ။ အပေါ်မှာရေးထားတဲ့ GetEnumerator() method အတွက် State machine က ဒီလိုဖြစ်နေမှာပါ (အတိအကျမဟုတ်ပါဘူး မြင်သာအောင်ရေးပြထားတာပါ။ တကယ် Generate လုပ်လိုက်တဲ့ code က ဒီထက်ပိုရှုပ်ထွေးပါတယ်)

public class GetEnumerator : IEnumerator
{
    private int _state;
    private object _current;
    public GetEnumerator(int state)
    {
        _state = state;
    }
    public bool MoveNext()
    {
        switch (_state)
        {
            case 0:
                _state = -1;
                _current = "A name";
                _state = 1;
                return true;
            case 1:
                _state = -1;
                _current = "B name";
                _state = 2;
                return true;
            case 2:
                _state = -1;
                _current = "C name";
                _state = 3;
                return true;
            default:
                return false;
        }
    }
    public void Reset() => throw new NotSupportedException();
    public object Current => _current;
}
Enter fullscreen mode Exit fullscreen mode

Deferred execution ကိုသုံးခြင်းအားဖြင့် Lazy evaluation ရရှိမှာဖြစ်ပြီးတော့ Performance efficient ဖြစ်စေမှာပါ။ ဥပမာ အောက်ကလို code မျိုးအတွက်ဆိုရင် Deferred execution က တော်တော်လေး သိသာမြင်သာရှိပါတယ်။

foreach (var num in GetNormalList(1000))
    Console.WriteLine(num);

foreach (var num in GetDeferredList(1000))
    Console.WriteLine(num);

int[] GetNormalList(int times)
{
    int[] numList = new int[times];
    for (int i = 0; i < times; i++)
    {
        numList[i] = i;
    }
    return numList;
}
IEnumerable<int> GetDeferredList(int times)
{
    for (int i = 0; i < times; i++)
    {
        yield return i;
    }
    yield break;
}
Enter fullscreen mode Exit fullscreen mode

GetNormalList method မှာဆိုရင် Client code ကနေခေါ်လိုက်တာနဲ့ ပေးထားတဲ့အကြိမ်ရေ မပြည့်မချင်း Loop ပတ်တာကို စောင့်ရမှာဖြစ်ပြီးတော့ ရလာတဲ့ number တွေကို Memory ထဲမှာသိမ်းထားရမှာပါ။ ဒီအတွက် Memory allocation လိုအပ်ပါတယ်။ Method ကို Execute လုပ်ပြီးတာနဲ့ Client code မှာရှိတဲ့ Looping အတိုင်း ဆက်သွားပြီး အကုန်လုံးကို printout လုပ်တာမလို့ Looping နှစ်ထပ်ပြီး mean time နှစ်ဆ တက်ပါတယ်။ GetDeferredList ကတော့ looping ထဲကနေ printout လုပ်တဲ့နေရာအထိ number တွေကို တိုက်ရိုက် Execute လုပ်တာမလို့ Memory allocation မရှိပါဘူး။ နောက်တခါ yield လုပ်ထားတဲ့တွက်ကြောင့် State machine နဲ့ execute လုပ်တာမလို့ Looping နှစ်ခုဟာ တိုက်ရိုက်ချိတ်ဆက်ပြီး အတူတွဲပြီး ပြီးဆုံးတဲ့အတွက် Mean time လည်းပိုနည်းသွားပါတယ်။ Breakpoint ထောက်ကြည့်လိုက်ရင် ပိုမြင်သာသွားမှာပါ။ ဒီတော့ Deferred execution ကို အသုံးပြုခြင်းအားဖြင့် ပိုမိုကောင်းမွန်တဲ့ Performance ကိုရရှိစေမှာဖြစ်ပါတယ်။ ကန့်သတ်ချက်အနေနဲ့ကတော့ Deferred execution က IEnumerator နဲ့ IEnumerable interface နှစ်ခုနဲ့ဘဲ yield လုပ်နိုင်ပါတယ်။ C# ရဲ့ Best of the best feature တခုဖြစ်တဲ့ Linq မှာတော့ Method chain တွေ အားလုံးဟာ Deferred execution နဲ့ပဲ Implement လုပ်ထားတာဖြစ်ပါတယ်။ ဒီလောက်ဆိုရင်တော့ Deferred execution အကြောင်း သဘောပေါက်လောက်ပြီ ယူဆပါတယ်။

Conclusion

ဒီတော့ အားလုံးကို ခြုံငုံကြည့်မယ်ဆိုရင် IEnumerable interface ဟာ C# ရဲ့ Iterator contract ဖြစ်ပြီး Iteration/Looping တွေဟာ Iterator pattern ကို အသုံးပြုပြီး execute လုပ်တယ်လို့နားလည်ထားရမှာပါ။ IEnumerable နဲ့ IEnumerator interface တွေဟာ Deferred execution ကို Enable လုပ်ပေးထားဖြစ်တဲ့အတွက် Lazy evaluation လိုအပ်ရင် yield statement သုံးပြီးရေးသားနိုင်ပါတယ်။
C# မှာရှိတဲ့ Array တွေအားလုံးကို Array class က Backing ပေးထားတာဖြစ်ပြီးတော့ Array class ဟာ IEnumerable interface ကို Implement လုပ်ထားပါတယ်။ ဒီတော့ C# မှာရှိတဲ့ Array အပါအဝင် Collection Data structure အားလုံးဟာ IEnumerable interface ကို Implement လုပ်ထားတယ်လို့ ယူဆနိုင်ပါတယ်။ IEnumerable, IEnumerator နဲ့ Iterator pattern ကို နားလည်သဘောပေါက်ထားရင် Collection data structure တွေကို ကိုင်တွယ်ရာမှာဘဲ ဖြစ်ဖြစ် ကိုယ်တိုင် တည်ဆောက်ရာမှာဘဲဖြစ်ဖြစ် ပိုပြီးကောင်းမွန်သပ်ရပ်တဲ့ ပုံစံတွေနဲ့ လုပ်ဆောင်နိုင်မှာဖြစ်ပါတယ်။

Thanks for reading, happy coding!

Top comments (0)