Už vím jak na iterátory

Reference nebo studie

Ve třetím díle našeho seriálu budeme opět spojovat OOP vzory, funkcionální programování a pohled "pod kapotu" programovacích jazyků. Tentokrát u mnohem používanější konstrukce foreach, která se dnes používá daleko víc než původní for s číselnou proměnnou.

Dnes budeme pokračovat v našem povídání o cyklech. Budeme se snažit pochopit, jak "uvnitř" funguje konstrukce "foreach". Začneme nejdřív vzorem "Iterator", ze kterého konstrukce foreach vychází. Předvedeme si i novější "progresivní" funkce jazyka C#, objekt Lazy. Podíváme se dále, jak se liší iterátor (a konstrukce foreach) v Pythonu oproti C# (aneb kolik jazyků umíš, tolikrát jsi programátorem). Příště se prozkoumáme zajímavou konstrukci yield return, která také s iterováním souvisí a další funkcionální možnosti pro práci se seznamy, jako je knihovna LINQ.  

Vzor Iterator

Návrhový vzor "Iterator" od naší známé skupiny "Gang of four" zajišťuje možnost procházení prvků bez znalosti jejich implementace. Oproti našemu původnímu "basicovému" cyklu for / next a jeho alternativním přepisům v jiných jazycích,

10 FOR I=0 TO 9

20   PRINT I

30 NEXT I

který se dá využít leda pro datovou strukturu pole (proměnná I jako index pole) se dnes daleko víc používají pro průchod datovými strukturami více "abstraktnější" metody procházení, založené právě na návrhovém vzoru "Iterator". Jeho pochopení není složité, ve zkratce vytvoříme pro průchod každou "vícečetnou" strukturou abstraktní rozhraní Iterator, které obsahuje pouze dvě nutné metody: Next() a HasNext().

Obr. 1 - Class Iterator diagram

Podrobnější vysvětlení. Klient zvolí konkrétní "agregátor", se kterým pracuje (tj. volí konkrétní ConcreteAggregate), a požádá jej o Iterator. Dostane odpovídající Iterator, kterému je při zrodu dosazena reference na "konkrétní" agregátor, tj. při tvorbě iterátoru se pak dosadí objektová reference this. Klient pak používá pouze interfacy Aggregate a hlavně Iterator. Zatím je to možná trochu nesrozumitelné, ale vše si objasníme konkrétní implementací v C#.

C# verze iterátoru

V tomto našem případě vyjdeme ze "základní" struktury List ("nafukovací" pole).  Zavoláním jeho metody GetEnumerator() jsme získali objekt typu IEnumerator  (podle návrhového vzoru Iterator máme vrácený abstraktní objekt Iterator). ConcreteAggregate je pak objekt List, abstraktní Aggregate pak rozhraní IEnumerable, které List implementuje a má jedinou metodu GetEnumerator() - v našem UML diagramu CreateIterator().

ConcreteIterator je "uvnitř" vytvořená struktura (struct) Enumerator, která má v konstruktoru daný objekt List (další datové struktury jako Stack nebo Queue mají pochopitelně svou odlišnou implementaci).

Obr. 2 - Class Iterator diagram

Tento objekt v sobě "drží" svou instanci Listu. Menší technická odlišnost je nahrazení metody Next() vracející daný objekt metodou MoveNext(), která vrací bool podobně jako HasNext() ve vzoru a nová vlastnost Current, která vrací konkrétní objekt, na kterém se Iterator nachází.

Pro průchod celým listem nám tak jenom stačí v cyklu while hlídat poslední prvek pomocí metody MoveNext() a vracet prvky pomocí Current takto:

List<string> list = new List<string>() { "A", "B", "C" };

IEnumerator<string> enumerator = list.GetEnumerator();

 

while (enumerator.MoveNext())

{

    Console.WriteLine($"Prvek {enumerator.Current}");

}

Cyklus foreach je tak pouze jinak zapsané while (enumerator.MoveNext()), chování je stejné. 

List<string> list = new List<string>() { "A", "B", "C" };

 

foreach (string current in list)

{

    Console.WriteLine($"Prvek {current}");

}

Zajímavé je chování hotového "Iteratoru" (tj. Enumerátoru) v případě přidání prvku. Následující kód neprojde.

List<string> list = new List<string>() { "A", "B", "C" };

IEnumerator<string> enumerator = list.GetEnumerator();

list.add("D");

 

while (enumerator.MoveNext())

{

    Console.WriteLine($"Prvek {enumerator.Current}");

}

Pokud chceme zmíněné chování obejít, můžeme s úspěchem použít objekt Lazy (přidaný až do původního .NET Framework 4), ve kterém pomocí lambda funkce vytvoříme "lazy evaluation" dané hodnoty, kde se její hodnota vyhodnotí pouze jednou a vrací tak pouze "cached" value. Ideální pro implementaci "Gang of Four" nejprimitivnějšího vzoru "Singleton", který při studiu OOP pochopil naprosto každý a každý si ho přál při zkoušce. 

List<string> list = new List<string>() { "A", "B", "C" };

Lazy<IEnumerator<string>> enumerator = new Lazy<IEnumerator<string>>(() => list.GetEnumerator());

list.Add("D");

 

while (enumerator.Value.MoveNext())

{

    Console.WriteLine($"Prvek {enumerator.Value.Current}");

}

Navíc objekt Lazy má výhodu "thread safe" oproti jednoduché, snadno pochopitelné a z hlediska vláken úplně špatné "klasické" implementaci Singletona v tomto stylu:

// Je to ve více vláknech špatně!

public sealed class Singleton

{

    private static Singleton instance = null;

 

    private Singleton()

    {

    }

 

    public static Singleton Instance

    {

        get

        {

            if (instance == null)

            {

                instance = new Singleton();

            }

            return instance;

        }

    }

}

Líné vyhodnocení nám ovšem nijak nepomůže ve smyčce foreach, při průchodu strukturou prostě prvek nepřidáme (na rozdíl od Pythonu, jak uvidíme dále):

List<string> list = new List<string>() { "A", "B", "C" };

 

foreach (string current in list)

{

    if (current == "A") list.Add("D");

    Console.WriteLine($"Prvek {current}");

}

Další zajímavostí je, jak C# pracuje při iterování s polem. Všechno vypadá stejně jako v Listu.

string[] array = new string[] { "A", "B", "C" };

 

foreach (string current in array)

{

    Console.WriteLine($"Prvek {current}");

}

 

Pole ale nemá definovaný správně "genericky typový" IEnumerator (v našem případě IEnumerator<string>), ale po přetypování na IEnumerable<string>(nebo na IList<string>) ho má. Trochu nekonzistence, ne? Řekl bych, že je zde vidět vývoj .NETu, kde se postupně přidala generika (s některými kompromisy) a pak se s postupem verzí nabalovalo i funkcionální rozšíření (o LINQ si povíme příště).

string[] array = new string[] { "A", "B", "C" };

IEnumerator<string> enumerator = ((IEnumerable<string>)array).GetEnumerator();

 

while (enumerator.MoveNext())

{

    Console.WriteLine($"Prvek {enumerator.Current}");

}

https://onecompiler.com/csharp

https://csharpindepth.com/articles/singleton

Python verze

Vzor "Iterátor" se v Pythonu modeluje hůř než v C#, protože iterátor nemá žádnou metodu "HasNext". Konec tak poznáme pomocí výjimky (exception) StopIteration. Nevěřil bych tomu, ale píšou to i na "StackOverflow". Říkají tomu "It's easier to ask for forgiveness than permission". Připadá mi to stejně "chytré" jako odsazovat bloky mezerami, ale co už. V Pythonu není práce s výjimkami tak náročná na zdroje jako v C#, kde platí princip "look before you leap".

Jinak v Pythonu není problém přidat prvek po konstrukci iterátoru.

myList = ["A", "B", "C"]

myIterator = iter(myList)

myList.append("D");

 

end_cursor = False

while not end_cursor:

    try:

        print(next(myIterator))

    except StopIteration:

        end_cursor = True

    except:

        print('other exceptions to manage')

        end_cursor = True

https://devblogs.microsoft.com/python/idiomatic-python-eafp-versus-lbyl/

Python umožňuje i přidání prvku při iterování ve "foreach", jak pro list, tak pro pole:

import array as arr

 

myArray = arr.array("u", ["A", "B", "C"])

for current in myArray:

  if (current == "A"):

    myArray.append("D")

  print(current)

print("------------------") 

myList = ["A", "B", "C"]

for current in myList:

  if (current == "A"):

    myList.append("D")

  print(current)

https://onecompiler.com/python

https://www.scaler.com/topics/difference-between-array-and-list-in-python/

Ostatní blogy