Casablanca: yield return

Hallo,

ich habe ein Verstänisproblem. Ich habe folgenden Code:

    public class Program
    {

        static IEnumerable<int> Foo()
        {
            Console.Write("A");
            yield return 1;
            Console.Write("B");
            yield return 2;
        } 

        static void Main(string[] args)
        {
            var items = Foo();

            Console.Write("C");
            foreach (var item in items)
            {
                Console.Write(item);
            }
        }
     }

Als ich das erste Mal den Code gesehne habe, habe ich mir gedacht, dass die Ausgabe CAB12 lauten muss. Beim nähren Betrachten wurde mir aber klar, dass die Ausgabe CA1B2 lautet. Kann jemand mir kurz erläutern, was da vor sich geht? Warum werden A und B erst in foreach-Schleife ausgegeben und nicht beim Aufruf der Methode? die Methode gibt ja eine int-Liset zurück, in der nur 1 und 2 stehen. yield return finde ich sowieso etwas verwirrend.

Danke im Voraus.

  1. Moin!

    ich habe ein Verstänisproblem. Ich habe folgenden Code:

    Hm. Programmlogik sollte eigentlich von der Kenntnis der Programmiersprache selbst weitgehend unabhängig sein. Deshalb wage ich es mal:

    Müsste bei Deinem Code nicht ABC12 herauskommen? Versuche:

         public class Program
         {
     
             static void Main(string[] args)
             {
                 var items = Foo();
     
                 Console.Write("Ausgabe der Daten in items:\r\n");
                 foreach (var item in items)
                 {
                     Console.Write(item);
                 }
             }
    
             static IEnumerable<int> Foo()
             {
                 Console.Write("Erster Wert (1) wird zu Foo hinzugefügt\r\n");
                 yield return 1;
                 Console.Write("Zweiter Wert (2) wird zu Foo hinzugefügt\r\n");
                 yield return 2;
             } 
          }
    

    Jörg Reinholz

    1. Hallo Jörg,

      danke. Die Ausgabe CA1B2 ist schon richtig. Man könnte aber auch die Ausgabe ABC12 vermuten. Das ist ja auch die eigentliche Frage. Warum verhält sich "yield return" so?

      Gruß

      1. Moin!

        danke. Die Ausgabe CA1B2 ist schon richtig. Man könnte aber auch die Ausgabe ABC12 vermuten. Das ist ja auch die eigentliche Frage. Warum verhält sich "yield return" so?

        yield return verhält sich im Zusammenhang eigentlich gar nicht

        Ich habe mal eben mono installiert unter Linux mit folgendem probiert:

        using System;
        using System.Collections;
        using System.Collections.Generic;
        
        public class Program
        {
        
            static void Main(string[] args)
            {
                var items = Foo();
                Console.Write("Ausgabe der Daten in items:\r\n");
                var count=0;
                foreach (var item in items)
                {
                    count ++;
                    Console.Write("count:");
                    Console.Write(count);
                    Console.Write("::");
                    Console.Write(item);
                    Console.Write("\r\n");
                }
            }
        
            static IEnumerable<int> Foo()
            {
                Console.Write("Erster Wert (1) wird zu Foo hinzugefügt\r\n");
                yield return 1;
                Console.Write("Zweiter Wert (2) wird zu Foo hinzugefügt\r\n");
                yield return 2;
            }
        }
        

        und dann:

        fastix@trainer:/tmp$ mcs test.mono
        fastix@trainer:/tmp$ mono test.exe 
        Ausgabe der Daten in items:
        Erster Wert (1) wird zu Foo hinzugefügt
        count:1::1
        Zweiter Wert (2) wird zu Foo hinzugefügt
        count:2::2
        

        Da sieht so aus als würde static IEnumerable<int> Foo() bei jedem Zugriff aufgerufen. Ein Horror! Das Verhalten könnte aber in der IEnumerable-Schnittstelle begründet sein.

        Jörg Reinholz

        1. Tach!

          Da sieht so aus als würde static IEnumerable<int> Foo() bei jedem Zugriff aufgerufen. Ein Horror! Das Verhalten könnte aber in der IEnumerable-Schnittstelle begründet sein.

          Horror? Das ist das übliche Verhalten eines Generators. Den und das Schlüsselwort yield gibt es auch in anderen Sprachen.

          dedlfix.

        2. Hallo Jörg Reinholz,

          Der Name der Programmiersprache C# muss csharp lauten, damit der Syntaxhighlighter arbeitet.

          Bis demnächst
          Matthias

          --
          Das Geheimnis des Könnens liegt im Wollen. (Giuseppe Mazzini)
          1. Moin!

            Der Name der Programmiersprache C# muss csharp lauten, damit der Syntaxhighlighter arbeitet.

            Danke!

            Jörg Reinholz

            1. Hallo Jörg Reinholz,

              Danke!

              Nicht dafür. Ich habs auch nur ausprobiert. Wofür die Vorschau doch so gut ist … ;-)

              Bis demnächst
              Matthias

              --
              Das Geheimnis des Könnens liegt im Wollen. (Giuseppe Mazzini)
  2. Tach!

    Kann jemand mir kurz erläutern, was da vor sich geht? Warum werden A und B erst in foreach-Schleife ausgegeben und nicht beim Aufruf der Methode? die Methode gibt ja eine int-Liset zurück, in der nur 1 und 2 stehen. yield return finde ich sowieso etwas verwirrend.

    Das yield ist der Schlüssel zum Verständnis. Die Methode Foo gibt keine Liste zurück, sondern etwas, über das man iterieren kann: IEnumerable und nicht IList oder List oder Array. Das yield veranlasst nun, dass ein so genannter Generator erstellt wird. Das IEnumerable hat eine Methode GetEnumerator(). Erst wenn von diesem das MoveNext() aufgerufen wird, wird die Generator-Methode ausgeführt. Dabei wird der Code bis zum ersten yield ausgeführt und das zurückgegeben, was hinter dem yield steht. Die Generator-Methode pausiert nun, bis MoveNext() erneut aufgerufen wird. Dann setzt sie nach dem yield fort bis zum nächsten oder bis zum Ende, je nachdem was zuerst erreicht wird. (Ein foreach bedient sich intern auch nur der Methoden vom Enumerator.)

    dedlfix.

    1. Hi,

      vielen Dank. Einleuchtend. Es ist mir aber noch nicht ganz klar: das, was von der Methode Foo() zurückgegeben wird, beinhaltet auch die zwei Asgaben A und B. Das heißt, dass diese Ausgaben erst dann ausgeführt werden, wenn über das, was zurückgegebn wird, iteriert wird? Welche Rolle spielt nun an dieser Stelle der Generator von yield?

      Gruß

      1. Tach!

        Es ist mir aber noch nicht ganz klar: das, was von der Methode Foo() zurückgegeben wird, beinhaltet auch die zwei Asgaben A und B.

        Das was direkt zurückgegeben wird ist sozusagen nur ein Verweis auf die Methode.

        Das heißt, dass diese Ausgaben erst dann ausgeführt werden, wenn über das, was zurückgegebn wird, iteriert wird? Welche Rolle spielt nun an dieser Stelle der Generator von yield?

        Das yield sagt dem Compiler, dass diese Methode ein Generator ist und nicht sofort ausgeführt werden soll, sondern erst wenn jemand darüber iteriert. Ansonsten würde sie sofort ausgeführt werden und sie hätte eine Liste erstellen müssen, damit der Aufrufer darüber iterieren kann.

        Der Vorteil eines Generators ist, dass der Code eben nicht sofort losläuft und damit vielleicht lange und viel Speicher braucht. Stattdessen läuft der Code schrittweise beim Iterieren ab.

        Meistens verwendet man das yield nicht nacheinander in Einzelschritten sondern in einer Schleife. Ein Beispiel wäre das Abfragen des Ergebnisses einer Datenbankabfrage. Statt den ganzen großen Batzen erst in eine Liste zu packen, über die man sowieso nur iterieren möchte, rückt ein Generator immer nur ein kleines Stück nach dem anderen raus.

        dedlfix.

        1. Hallo,

          danke. Wenn dem so ist und die Foo-Methode zunächst mal als ein Generator betrachtet wird, warum sieht man dann nach dem Aufruf von Foo-Methode in der items-Variable (var items = Foo();) nur die Werte 1 und 2 als eine Liste mit Index-Nummern. Wo bleiben dann die Ausgaben A und B?

          Wenn man an der Console.Write("C"); Zeile einen Breakpoint setzt, hat man sofort die Ausgabe ABAB. Die Ausgabe nach der F5 wird dann so aussehen: ABABCA1B2. Ohne Breakpoint aber CA1B2.

          Gruß

          1. Tach!

            Wenn dem so ist und die Foo-Methode zunächst mal als ein Generator betrachtet wird, warum sieht man dann nach dem Aufruf von Foo-Methode in der items-Variable (var items = Foo();) nur die Werte 1 und 2 als eine Liste mit Index-Nummern. Wo bleiben dann die Ausgaben A und B?

            Die Liste siehst du genau wo? Im Debugger? Der hat dann freundlicherweise für dich iteriert. Aber üblicherweise tut er das bei Enumerationen nur, wenn du das durch Klick bestätigst (ausgehend vom Visual Studio). Die Ausgaben A und B landen genau da, wo du sie hinschickst: in der Konsole. Es kann nur sein, dass der Debugger nicht in die/deine Konsole schreibt.

            Wenn man an der Console.Write("C"); Zeile einen Breakpoint setzt, hat man sofort die Ausgabe ABAB. Die Ausgabe nach der F5 wird dann so aussehen: ABABCA1B2. Ohne Breakpoint aber CA1B2.

            Hmm, dann schreibt der Debugger wohl doch in die Konsole, die du siehst.

            dedlfix.

            1. Tach!

              Wenn man an der Console.Write("C"); Zeile einen Breakpoint setzt, hat man sofort die Ausgabe ABAB. Die Ausgabe nach der F5 wird dann so aussehen: ABABCA1B2. Ohne Breakpoint aber CA1B2.

              Hmm, dann schreibt der Debugger wohl doch in die Konsole, die du siehst.

              Ich hab das mal überprüft. Das AB kommt nur vorn dran, wenn du im Debugger nachschaust und dazu auf die zwei Kreispfeile klickst, die vor dem Hinweis stehen: "Expanding the Results View will enumerate the IEnumerable". Dieser Hinweis steht da ja nicht umsonst, denn das Enumerieren kann Nebenwirkungen haben, so wie in deinem Fall. Dass du zweimal AB siehst, liegt daran, dass du zweimal nachgeschaut hast. Du hast damit einen Schrödingers-Katze-Effekt. Wenn du nachschaust, beeinflusst du das Experiment und machst es kaputt.

              dedlfix.

          2. Moin!

            Wenn dem so ist und die Foo-Methode zunächst mal als ein Generator betrachtet wird, warum sieht man dann nach dem Aufruf von Foo-Methode in der items-Variable (var items = Foo();) nur die Werte 1 und 2 als eine Liste mit Index-Nummern.

            Du vermischst hier offenbar die Ausgaben/Eigenschaften des Programms und die Ausgaben/Eigenschaften der IDE, welche Dir offenbar auch zeigen was der Generator ausgeben würde, wenn man ihn denn fragen würde.

            Wo bleiben dann die Ausgaben A und B?

            Das sind reine Ausgaben, die (außer im Programm) nirgends gespeichert werden. Diese Werte stehen für die IDE nicht im Generator zur Verfügung.

            Jörg Reinholz

          3. Hallo,

            ich danke euch für die Erklärungen und wünsche euch ein schönes Wochenende.

            Gruß

      2. Moin!

        Welche Rolle spielt nun an dieser Stelle der Generator von yield?

        Das muss man sich so vorstellen wie einen "Server", der als eigenständiges Programm läuft. Es handelt sich genau genommen eigentlich nicht um einen Array.

        In der Bash könnte das so aussehen:

        1. "Server"

        #/bin/bash
        mkfifo fifo
        echo "1" > fifo;
        echo "2" > fifo;
        echo "EOT" > fifo;
        rm fifo;
        

        Der wird gestartet und gibt der Reihe nach 1 2 EOT in den FIFO Puffer (und hält dann jeweils an, weil der fifo nicht bereit für die nächste Eingabe ist)

        Diese server.sh starte ich und schicke die in den Hintergrund.

        fastix@trainer:/tmp$ ./server.sh &
        [4] 17966
        

        2. Jetzt die client.sh:

        #/bin/bash
        while [ "$row" != "EOT" ]; do
          echo -n "gelesen: ";
          read row < fifo;
          echo $row;
        done
        

        starten und Ergebnis:

        fastix@trainer:/tmp$ ./client.sh
        gelesen: 1
        gelesen: 2
        gelesen: EOT
        fastix@trainer:/tmp$ fg 4
        bash: fg: Programm ist beendet.
        [4]+  Fertig                  ./server.sh
        

        Du hast nur halt keinen fifo (First-In-First-Out-Puffer) und C# erledigt das, wofür ich zwei Skripte anlegte, in einem Programm.

        Jörg Reinholz

        1. Tach!

          Welche Rolle spielt nun an dieser Stelle der Generator von yield?

          Das muss man sich so vorstellen wie einen "Server", der als eigenständiges Programm läuft.

          Das was du beschreibst ist kein Generator, sondern etwas, das eine vollständige Liste erstellt und dazu ein Verwender, der schrittweise durch die Liste läuft. Ein Generator erstellt keine Liste vorab, sondern ermittelt/erstellt die Daten des nächsten Schrittes erst wenn der Request dazu kommt.

          dedlfix.

          1. Moin!

            Ein Generator erstellt keine Liste vorab, sondern ermittelt/erstellt die Daten des nächsten Schrittes erst wenn der Request dazu kommt.

            Man Gottes!

            Das passiert hier ebenso. Denn der "Server" hält an, weil er nur einen Eintrag in den Fifo schreiben kann. Sonst wäre der fifo ja auch schon gelöscht wenn die client.sh gestartet wird.

            Jörg Reinholz

            1. Tach!

              Das passiert hier ebenso. Denn der "Server" hält an, weil er nur einen Eintrag in den Fifo schreiben kann.

              Ach, mkfifo ist der Trick.

              dedlfix.

              1. Moin!

                Ach, mkfifo ist der Trick.

                Ja. Die Dinger sind nice, weil ich einen Datenstrom mit tee auch in einen FIFO schreiben, dann aus dem FIFO lesen und weiter verarbeiten kann. Ich kann also einen Datenstrom, z.B. aus einem Gerät, nehmen und zeilen- bzw. blockweise in zwei oder mehr Strängen "in Echtzeit" verarbeiten.

                Jörg Reinholz