csharp--yield---deep-dive

C# "yield"- deep dive

İstənilən növ proqram yazarkən bu və ya digər formada müəyyən data strukturlarla işləyirik və çox vaxt həmin data strukturlarda iterasiya etmək lazım olur. İterasiyanın səbəbləri müxtəlif ola bilər: məlumatları sort etmək, filtrasiya etmək və s.

               C#-da data strukturun sadalanan ola bilməsi üçün onun GetEnumerator() adlı metoda malik olması lazımdır və metod mütləq IEnumerator və ya IEnumerator tipində geriyə nəticə qaytarmalıdır.

using System;
using System.Collections.Generic;


namespace yield_keyword
{
    class Worker
    {
        public string FullName { get; set; }
        public string WorkbookNumber { get; set; }
        public decimal Salary { get; set; }
        public override string ToString()
        {
            return $"Name = {FullName}, WorkbookNumber = {WorkbookNumber},Salary = {Salary}";
        }
    }
    class Company 
    {
        private readonly List< Worker > workers;
        public Company()
        {
            workers = new List< Worker >
            {
                new Worker { FullName = "Xanma Baki", WorkbookNumber = "JPN3443", Salary = 3400 },
                 new Worker { FullName = "Oroti Dobbo", WorkbookNumber = "JPN2443", Salary = 3550 },
                 new Worker { FullName = "Retsu Kayu", WorkbookNumber = "JPN6743", Salary = 3210 }
            };


        }
        //bu metod geriyə sadəcə IEnumerator interfeysi də qaytara bilər..
        public IEnumerator< Worker > GetEnumerator()
        {
            return workers.GetEnumerator();
        }
    }
    class Program
    {
        static void Main(string[] args)
        {
            Company company = new Company();


            //çox bəsit formada sadalama aparırıq...
            foreach (Worker worker in company)
            {
                Console.WriteLine(worker);
            }
            Console.ReadLine();
        }
    }
}


Bir klasın tərkibində belə bir mexanizmin olması sadalama prosesini asanlaşdırır.

Lakin sadalama prosesi bununla bitmir. Əgərki ,hər hansı bir proqram modulu arqument olaraq sadalana bilən mexanizm tələb edirsə, ozaman Company klasımızı həmin modul metoduna arqument olaraq ötürmək mümkün olmayacaq! Çünki böyük ehtimal həmin metod arqument olaraq IEnumerator/IEnumerable interfeyslərindən birini tələb edəcək. Belə olan zaman isə gərək bizim metodumuz bu interfeyslərdən birini realizə etsin.(Kontekstdən asılı olaraq)

//IEnumerable generic interfeysini realizə edək
 class Company : IEnumerable< Worker >
    {
        private readonly List< Worker > workers;
        
        public Company()
        {
            workers = new List< Worker >
            {
                new Worker { FullName = "Xanma Baki", WorkbookNumber = "JPN3443", Salary = 3400 },
                 new Worker { FullName = "Oroti Dobbo", WorkbookNumber = "JPN2443", Salary = 3550 },
                 new Worker { FullName = "Retsu Kayu", WorkbookNumber = "JPN6743", Salary = 3210 }
            };


        }


        //interfeysdən gələn metod...
        public IEnumerator< Worker > GetEnumerator()
        {
            //listin default enumeratorunu istifadə edək
            return workers.GetEnumerator();
        }


        //inteffeysdən gələn metod..
        IEnumerator IEnumerable.GetEnumerator()
        {
            return GetEnumerator();
        }
    }


Əgərki verilən standard listin sadalama alqoritmi bizi təmin etmirsə əlavə bir sadalama alqoritmini belə yazırdıq:

 class Company : IEnumerable< Worker >
    {
        private readonly List< Worker > workers;
        public Company()
        {
            workers = new List< Worker >
            {
                new Worker { FullName = "Xanma Baki", WorkbookNumber = "JPN3443", Salary = 3400 },
                 new Worker { FullName = "Oroti Dobbo", WorkbookNumber = "JPN2443", Salary = 3550 },
                 new Worker { FullName = "Retsu Kayu", WorkbookNumber = "JPN6743", Salary = 3210 }
            };
        }

        public IEnumerator< Worker > GetEnumerator()
        {
           //öz custom enumeratorumuzu çağıraq...
            return new MyCustomWorkerEnumerator(workers);
        }

        IEnumerator IEnumerable.GetEnumerator()
        {
            return GetEnumerator();
        }
    }


    /// 
    /// Reverse enumeration -custom class for reverse enumeration
    /// 
    class MyCustomWorkerEnumerator : IEnumerator< Worker >
    {
        private readonly List< Worker > workers;
        private int reverseCounter;
        public MyCustomWorkerEnumerator(List< Worker > workers)
        {
            this.workers = workers;
            reverseCounter = workers.Count;
        }


        public Worker Current => workers[reverseCounter];


        object IEnumerator.Current => workers[reverseCounter];


        public void Dispose()
        {
           
        }


        public bool MoveNext()
        {
            --reverseCounter;
            if (reverseCounter < 0)
                return false;
            else
            {
                return true;
            }
               
        }


        public void Reset()
        {
            reverseCounter = workers.Count;
        }
    }
    

Alınan cavab:

C#-da “yield” açar sözü məhz bu tipli situasiyalarda sadalama prosesini asanlaşdırmaq üçün yaradılıb. C#-ın 2.0 versiyasında sintaktik asanlaşdırma(sintactic sugar) olaraq gələn “yield” bizə öz IEnumerator/IEnumerable interfeyslərimizi yaratmadan sadalama işini görməyimizə xidmət edir. Yield bizə öz custom iterasiyalarımızı yazmağa imkan verir, hansı ki , bu iterasiyalar müəyyən vəziyyət(state) saxlaya bilirlər. Yazının davamında bunun mənasını izah edəcəm. Hələlik davam edək..

 public IEnumerator< Worker > GetEnumerator()
        {
            yield return workers[0];
            yield return workers[1];
            yield return workers[2];
        }

"Yield" açar sözü return sözü ilə birgə istifadə olunan zaman normal return məntiqindən fərqli formada işləyir. Beləki yuxarıdakı nümunədə ilk sətirdəki yield icra olunan zaman metoddan çıxış baş verir amma bu müvəqqəti çıxışdır!! Növbəti foreach iterasiyası zamanı əmr yenə yield olan metoda gələcək və  ikinci sətir yield işə düşəcək və bu proses bu formada davam edəcək. Məhz bu proses movcud vəziyyəti saxlama(state save)-dir. Bunun hesabına iterasiya prosesi return etsə də sonraki foreach zamanı qaldığı yerdən davam edə bilir!!! Nümunəyə breakpoint qoyaraq baxaq:

Göründüyü kimi, yield operatorlu return, adi return kimi işləmir, hətta adi return zamanı bir metodun içində ardıcıl 2 return yazmaq olmur! Yield olan situasiyada isə metoddan müvəqqəti çıxış baş verir, amma metod icra etdiyi sətrə qədər olan yeri yadda saxlayır! Növbəti dövr zamanı məhz həmin sətirdən işi davam etdirir. Yuxarıda yazdığımız kodu daha səliqəli formaya salaq:

 public IEnumerator< Worker > GetEnumerator()
        {
            foreach (Worker worker in workers)
            {
                yield return worker;
            }
        }

Əgər biz adi return yazsaydıq kod nümunəsi tamamilə yararsız hala düşərdi və kompilyasiya olunmazdı.

               Yield açar sözünün praktikada daha hansı məsələləri asanlaşdırdığını görmək üçün aşağıdakı koda baxaq. Bu kodda bizə sadə formada filtr metodu lazımdır.

  public IEnumerable< Worker > FilterBySalary(decimal salary)
        {
            List< Worker > tempList = new List< Worker >();
            foreach (Worker worker in workers)
            {
                if (worker.Salary > salary)
                    tempList.Add(worker);
            }
            return tempList;
        }

Yuxarıdakı metod heç də optimal deyil, çünki əlavə bir müvəqqəti list yaradılmasına gətirib çıxardır. Bu yield ilə bu cür qısalda bilərik.

 public IEnumerable< Worker > FilterBySalary(decimal salary)
        {
           
            foreach (Worker worker in workers)
            {
                if (worker.Salary > salary)
                    yield return worker;
            }
          
        }

               Böyük datalarla işləyən zaman yield istifadəsi bizə yaddaşa qənaətə gətirib çıxardır. Belə ki, yalnız lazım olan zaman geri dönüş edilməsi sürətə çox böyük təsir edir. Buna misal üzərindən baxaq:

static void Main(string[] args)
        {


            IEnumerable<int> GetDataWithYield()
            {
                for (int i = 0; i < 4_000_000; i++)
                    yield return 3 * i;
            }


            IEnumerable<int> GetNormalListData()
            {
                var myList = new List<int>();
                for (int i = 0; i < 4_000_000; i++)
                    myList.Add(3 * i);
                return myList;
            }


            var lazyDataWithYield = GetDataWithYield();
            // test ucun maksimumu tap
            var max = 0;
            foreach (var data in lazyDataWithYield)
                if (data > max)
                    max = data;


            var memoryWithYield = GC.GetTotalMemory(forceFullCollection: false);


            var EagerData = GetNormalListData();
            // test ucun maksimumu tap
            max = 0;
            foreach (var data in EagerData)
                if (data > max)
                    max = data;


            var memoryWithNormalList = GC.GetTotalMemory(forceFullCollection: false);


            Console.WriteLine($"Memory with yield (lazy calling): {memoryWithYield}, memory with normal list(eager calling): {memoryWithNormalList}");


            Console.ReadLine();
        }

İcradan sonra belə bir cavab alırıq:


Yuxarıdakı kod nümunəsindən göründüyü kimi , çox böyük datalarla işləyən zaman yield istifadə etmək sürətdə çox optimizasiya qazandırır. Yield ilə dataların iterasiya olunması prosesi çox vaxt tənbəl iterasiya adlanır(Lazy iteration) . Çünki iterasiyadakı datalar bizə yalnız runtime-da , iterasiya davam edən zaman verilir və iterasiya hansı datanın üzərindədirsə yalnız həmin data verilir. Digər normal iterasiyalar isə əvvəlcədən datanı operativ yaddaşa yığmış olur və bir oradan datanı addım-addım çəkirik.Bun eager iteration deyilir.

Qeyd: Bunları müqayisə edərkən , verilənlər bazasında connected və diconnected modelə bənzədə bilərsiniz. Connected model birbaşa runtime qoşulma ilə bazadan məlumatları verir, DataSet(disconnected model) isə dataları əvvəlcə operativ yaddaşa toplayır və oaradan dataları verir. Bu halda yield connected model, digər iterasiya isə disconnected modelə bənzəyir.


               "Yield" - deep dive

            İndi isə yield-in daxildə necə işlədiyinə baxaq. Əgər hər hansı bir klasın içərisində yield mexanizmi istifadə olunursa həmin klasın içərisində avtomatik olaraq sonlu avtomat(sonlu vəziyyət maşını)(State Machine) formalaşır.

Company klasına İL dilində baxaraq bunu görə bilərik.

Bu state machine müəyyən bir başlanğıc vəziyyətə malikdir. Hər bir iterasiya zamanı onun vəziyyəti dəyişir və iterasiyanın növbəti elementinə link saxlayır. İçərisində olan MoveNext() metodu ilə iterasiya tam bitənə qədər hər dəfə Current dəyişənindəki datanı geri verir. Əgər data bitərsə MoveNext() geriyə false qiyməti qaytarır və bununla da iterasiya başa çatmış sayılır.

 

 

 

Tural

Tural Süleymani

Süleymani Tural Microsoft-un MCSD statuslu mütəxəssisidir, 2008-ci ildən bu yana proqramlaşdırma üzrə tədris aparır

Müəllifin bu dildə ən son postları

Bu yazıları da bəyənə bilərsiniz