Kaczka

Wskazówka w języku C#: yield

Słowo kluczowe yield – obecne w języku C# od dawien dawna – w prosty sposób może przyczynić się do poprawy wydajności naszego kodu. Jak działa? Gdzie go używać? Jak może przyczynić się do poprawy działania aplikacji? Przekonajmy się, zaczynając przygodę z yieldem bez jego użycia.

Wyobraźmy sobie hipotetyczną sytuację, w której pytamy Internet o wszystkie mądrości znanego pisarza jakie posiada w swoich zasobach, ale interesują nas tylko niezbyt długie (na przykład chcemy je wszystkie zatweetować, lecz niestety Twitter ogranicza nam wielkość wiadomości do 140 znaków):

public class PauloCoelhoQuotesProvider
{
  private const int MaxShortQuoteLength = 140;

  public IEnumerable<string> DownloadShortQuotes()
  {
    var shortQuotes = new List<string>();

    while (Internet.IsThereMoreQuotesOfPauloCoelho())
    {
      var quote = Internet.GetNextQuoteOfPauloCoelho();

      if (quote.Length <= MaxShortQuoteLength)
      {
        shortQuotes.Add(quote);
      }
    }

    return shortQuotes;
  }
}

Zakładając, że magiczna klasa Internet istnieje – zanim zaczniemy bombardować Twittera solidną porcją cytatów – chcielibyśmy sprawdzić działanie naszej metody wyświetlając wynik jej wywołania na ekran konsoli:

class Program
{
  static void Main()
  {
    WriteLineWithTime("Execution started");

    var quotesProvider = new PauloCoelhoQuotesProvider();

    var quotes = quotesProvider.DownloadShortQuotes();

    foreach (var quote in quotes)
    {
      WriteLineWithTime(quote);
    }

    WriteLineWithTime("Execution finished");
  }

  private static void WriteLineWithTime(string text)
  {
    Console.WriteLine(
      $"{DateTime.Now.ToLongTimeString()}: " +
      $"{text}{Environment.NewLine}");
  }
}

Na potrzeby przeanalizowania procesu, który tutaj zachodzi, oprócz cytatów, wyświetlamy informację o momencie wypisywania na ekran danego zdarzenia.

Zakładając, że Internet jest dość wolny i pobranie jednego cytatu zajmuje średnio kilka sekund oraz fakt, że niektóre cytaty ze względu na długość zostały odrzucone, wywołanie programu mogłoby wyglądać następująco:

Wynik wywołania aplikacji bez użycia yield
Wynik wywołania aplikacji bez użycia yield

Łatwo jest zauważyć, że przez 13 sekund po uruchomieniu programu nic się nie dzieje, zaś dopiero po pobraniu wszystkich cytatów trafiają one na raz na ekran.

Co, jeśli Internet zawiera ich tak dużą liczbę, że na jakikolwiek rezultat będziemy czekać w nieskończoność?

Z pomocą przychodzi słowo kluczowe yield.

Zobaczmy zmodyfikowaną metodę DownloadShortQuotes() z użyciem słowa kluczowego yield:

public static class PauloCoelhoQuotesProvider
{
  private const int MaxShortQuoteLength = 140;

  public IEnumerable<string> DownloadShortQuotes()
  {
    while (Internet.IsThereMoreQuotesOfPauloCoelho())
    {
      var quote = Internet.GetNextQuoteOfPauloCoelho();

      if (quote.Length <= MaxShortQuoteLength)
      {
        yield return quote;
      }
    }
  }
}

Co się zmieniło? Po pierwsze, nie musimy tworzyć listy, która będzie zwracana jako wynik wywołania metody. Pojedyńczym wywołaniem yield return dodajemy do wynikowej kolekcji jeden element.

Jedynym, warunkiem, który musi spełniać metoda, aby można było posłużyć się konstrukcją yield jest ustawienie wyniku jej wywołania na jeden z typów: IEnumerable, IEnumerable<T>, IEnumerator lub IEnumerator<T>.

Zobaczmy, jak zmienił się rezultat uruchomienia naszego programu po wprowadzeniu modyfikacji:

Wynik wywołania aplikacji z użyciem yield
Wynik wywołania aplikacji z użyciem yield

Na pierwszy rzut oka widać benefit z użycia konstrukcji yield w naszym programie. Po każdym wywołaniu yield return, sterowanie przekazywane jest do miejsca, w którym przy pomocy foreach dostajemy się do elementów kolekcji. Dzięki temu możemy przetwarzać zwracane elementy bez konieczności czekania aż wszystkie zostaną pobrane. W naszym przypadku oznacza to możliwość bieżącego czytania cytatów bohatera niniejszego wpisu bez konieczności czekania, aż Internet zwróci je wszystkie. Szczególnie przydatne w przypadku, gdyby okazało się, że jest ich nieskończenie wiele 🙂

Język udostępna nam także konstrukcję yield break, która pozwala na zakończenie metody w dowolnym momencie. Na przykład wtedy, gdy uznamy, że pobraliśmy już wystarczającą liczbę elementów.

W naszym przykładzie yield break moglibyśmy wykorzystać w następujący sposób:

public IEnumerable<string> DownloadShortQuotes()
{
  while (true)
  {
    if (!Internet.IsThereMoreQuotesOfPauloCoelho())
    {
      yield break;
    }

    var quote = Internet.GetNextQuoteOfPauloCoelho();

    if (quote.Length <= MaxShortQuoteLength)
    {
      yield return quote;
    }
  }
}


Uwaga na zakończenie:

Konstrukcji yield nie da się użyć wewnątrz bloku try. Dopuszczalna jest jedynie sytuacja, w której blok ten nie zawiera klauzuli catch, zaś zakończony jest klauzulą finally.

Poniżej znajduje się link afiliacyjny do serwisu ceneo.pl.
Jeżeli przy jego pomocy przejdziesz na stronę sklepu, otrzymam za to prowizję.

Przygotowując niniejszy wpis posiłkowałem się wiedzą z książki:

Joseph Albahari, Ben Albahari, “C# 6.0 w pigułce”, Helion 2016