C# Language
Słowo kluczowe wydajności
Szukaj…
Wprowadzenie
Kiedy używasz słowa kluczowego fed w instrukcji, wskazujesz, że metoda, operator lub get accessor, w którym się pojawia, jest iteratorem. Użycie wydajności do zdefiniowania iteratora eliminuje potrzebę jawnej dodatkowej klasy (klasy przechowującej stan dla wyliczenia) podczas implementowania wzorca IEnumerable i IEnumerator dla niestandardowego typu kolekcji.
Składnia
- stopa zwrotu [TYPE]
- spadek wydajności
Uwagi
Umieszczenie słowa kluczowego yield
w metodzie z typem zwracanym IEnumerable
, IEnumerable<T>
, IEnumerator
lub IEnumerator<T>
nakazuje kompilatorowi wygenerowanie implementacji typu zwracanego ( IEnumerable
lub IEnumerator
), który po zapętleniu uruchamia metoda do każdej „wydajności”, aby uzyskać każdy wynik.
yield
kluczowe yield
jest przydatne, gdy chcesz zwrócić „następny” element teoretycznie nieograniczonej sekwencji, więc obliczenie całej sekwencji przedtem byłoby niemożliwe, lub gdy obliczenie pełnej sekwencji wartości przed zwróceniem doprowadziłoby do niepożądanej przerwy dla użytkownika .
yield break
można również zastosować do zakończenia sekwencji w dowolnym momencie.
Ponieważ yield
kluczowe wymaga typ interfejsu iterator jako zwracanego typu, takich jak IEnumerable<T>
, nie można korzystać z tego w sposób asynchroniczny jako Zwraca Task<IEnumerable<T>>
obiektów.
Dalsza lektura
Prosta obsługa
yield
kluczowe yield
służy do definiowania funkcji, która zwraca IEnumerable
lub IEnumerator
(a także ich pochodne warianty rodzajowe), których wartości są generowane leniwie, gdy obiekt wywołujący iteruje po zwróconej kolekcji. Przeczytaj więcej o celu w sekcji uwag .
Poniższy przykład zawiera instrukcję return return, która znajduje się for
pętli for
.
public static IEnumerable<int> Count(int start, int count)
{
for (int i = 0; i <= count; i++)
{
yield return start + i;
}
}
Następnie możesz to nazwać:
foreach (int value in Count(start: 4, count: 10))
{
Console.WriteLine(value);
}
Wyjście konsoli
4
5
6
...
14
Wersja demonstracyjna na żywo .NET Fiddle
Każda iteracja treści instrukcji foreach
powoduje wywołanie funkcji iteratora Count
. Każde wywołanie funkcji iteratora przechodzi do następnego wykonania instrukcji yield return
, która ma miejsce podczas następnej iteracji pętli for
.
Bardziej odpowiednie zastosowanie
public IEnumerable<User> SelectUsers()
{
// Execute an SQL query on a database.
using (IDataReader reader = this.Database.ExecuteReader(CommandType.Text, "SELECT Id, Name FROM Users"))
{
while (reader.Read())
{
int id = reader.GetInt32(0);
string name = reader.GetString(1);
yield return new User(id, name);
}
}
}
Istnieją oczywiście inne sposoby uzyskania IEnumerable<User>
z bazy danych SQL - to po prostu pokazuje, że możesz użyć yield
aby zamienić wszystko, co ma semantykę „sekwencji elementów” w IEnumerable<T>
którą ktoś może iterować .
Wczesne zakończenie
Można rozszerzyć funkcjonalność istniejących metod yield
, przekazując jedną lub więcej wartości lub elementów, które mogłyby zdefiniować warunek końcowy w ramach funkcji, wywołując yield break
aby zatrzymać wykonywanie wewnętrznej pętli.
public static IEnumerable<int> CountUntilAny(int start, HashSet<int> earlyTerminationSet)
{
int curr = start;
while (true)
{
if (earlyTerminationSet.Contains(curr))
{
// we've hit one of the ending values
yield break;
}
yield return curr;
if (curr == Int32.MaxValue)
{
// don't overflow if we get all the way to the end; just stop
yield break;
}
curr++;
}
}
Powyższa metoda będzie iterować od określonej pozycji start
dopóki nie earlyTerminationSet
napotkana jedna z wartości w parametrze earlyTerminationSet
.
// Iterate from a starting point until you encounter any elements defined as
// terminating elements
var terminatingElements = new HashSet<int>{ 7, 9, 11 };
// This will iterate from 1 until one of the terminating elements is encountered (7)
foreach(var x in CountUntilAny(1,terminatingElements))
{
// This will write out the results from 1 until 7 (which will trigger terminating)
Console.WriteLine(x);
}
Wynik:
1
2)
3)
4
5
6
Wersja demonstracyjna na żywo .NET Fiddle
Poprawne sprawdzanie argumentów
Metoda iteratora nie jest wykonywana, dopóki nie zostanie wyliczona wartość zwracana. Dlatego korzystne jest zapewnienie warunków wstępnych poza iteratorem.
public static IEnumerable<int> Count(int start, int count)
{
// The exception will throw when the method is called, not when the result is iterated
if (count < 0)
throw new ArgumentOutOfRangeException(nameof(count));
return CountCore(start, count);
}
private static IEnumerable<int> CountCore(int start, int count)
{
// If the exception was thrown here it would be raised during the first MoveNext()
// call on the IEnumerator, potentially at a point in the code far away from where
// an incorrect value was passed.
for (int i = 0; i < count; i++)
{
yield return start + i;
}
}
Kod strony wywołującej (użycie):
// Get the count
var count = Count(1,10);
// Iterate the results
foreach(var x in count)
{
Console.WriteLine(x);
}
Wynik:
1
2)
3)
4
5
6
7
8
9
10
Wersja demonstracyjna na żywo .NET Fiddle
Gdy metoda wykorzystuje yield
do wygenerowania wyliczalnego, kompilator tworzy maszynę stanów, która po iteracji uruchomi kod do yield
. Następnie zwraca uzyskany przedmiot i zapisuje jego stan.
Oznacza to, że nie dowiesz się o niepoprawnych argumentach (przekazanie null
itp.), Gdy wywołujesz metodę po raz pierwszy (ponieważ tworzy to maszynę stanu), tylko wtedy, gdy próbujesz uzyskać dostęp do pierwszego elementu (ponieważ dopiero wtedy kod znajduje się w metoda uruchamiana przez maszynę stanu). Zawijając go w normalną metodę, która najpierw sprawdza argumenty, możesz je sprawdzić po wywołaniu metody. To jest przykład szybkiego zawierania.
Gdy używasz C # 7+, funkcję CountCore
można wygodnie ukryć w funkcji Count
jako funkcję lokalną . Zobacz przykład tutaj .
Zwraca inny Enumerable w ramach metody zwracającej Enumerable
public IEnumerable<int> F1()
{
for (int i = 0; i < 3; i++)
yield return i;
//return F2(); // Compile Error!!
foreach (var element in F2())
yield return element;
}
public int[] F2()
{
return new[] { 3, 4, 5 };
}
Leniwa ocena
Dopiero gdy instrukcja foreach
przesunie się do następnego elementu, blok iteratora ocenia do następnej instrukcji yield
.
Rozważ następujący przykład:
private IEnumerable<int> Integers()
{
var i = 0;
while(true)
{
Console.WriteLine("Inside iterator: " + i);
yield return i;
i++;
}
}
private void PrintNumbers()
{
var numbers = Integers().Take(3);
Console.WriteLine("Starting iteration");
foreach(var number in numbers)
{
Console.WriteLine("Inside foreach: " + number);
}
}
Spowoduje to wygenerowanie:
Rozpoczęcie iteracji
Wewnątrz iteratora: 0
Wewnątrz foreach: 0
Wewnątrz iteratora: 1
Wewnątrz foreach: 1
Wewnątrz iteratora: 2
Wewnątrz foreach: 2
W konsekwencji:
- „Rozpoczęcie iteracji” jest drukowane jako pierwsze, mimo że metoda iteratora została wywołana przed wierszem wypisującym ją, ponieważ linia
Integers().Take(3);
tak naprawdę nie rozpoczyna iteracji (nieIEnumerator.MoveNext()
) - Linie drukowane w konsoli zmieniają się naprzemiennie między tą w metodzie iteratora i tą w
foreach
, a nie wszystkie w metodzie iteratora, oceniając najpierw - Program kończy się z powodu metody
.Take()
, mimo że metoda iteratora ma wartośćwhile true
której nigdy się nie wybija.
Spróbuj ... w końcu
Jeśli metoda iterator ma wydajność wewnątrz try...finally
, a następnie powrócił IEnumerator
wykona finally
oświadczenie gdy Dispose
nazywa na to, jak długo obecny punkt oceny jest wewnątrz try
bloku.
Biorąc pod uwagę funkcję:
private IEnumerable<int> Numbers()
{
yield return 1;
try
{
yield return 2;
yield return 3;
}
finally
{
Console.WriteLine("Finally executed");
}
}
Dzwoniąc:
private void DisposeOutsideTry()
{
var enumerator = Numbers().GetEnumerator();
enumerator.MoveNext();
Console.WriteLine(enumerator.Current);
enumerator.Dispose();
}
Następnie drukuje:
1
Dzwoniąc:
private void DisposeInsideTry()
{
var enumerator = Numbers().GetEnumerator();
enumerator.MoveNext();
Console.WriteLine(enumerator.Current);
enumerator.MoveNext();
Console.WriteLine(enumerator.Current);
enumerator.Dispose();
}
Następnie drukuje:
1
2)
W końcu wykonany
Wykorzystanie wydajności do utworzenia IEnumeratora podczas wdrażania IEnumerable
Interfejs IEnumerable<T>
ma jedną metodę GetEnumerator()
, która zwraca IEnumerator<T>
.
Chociaż słowa kluczowego yield
można użyć do bezpośredniego utworzenia IEnumerable<T>
, można go również użyć dokładnie w ten sam sposób, aby utworzyć IEnumerator<T>
. Jedyne, co się zmienia, to typ zwracanej metody.
Może to być przydatne, jeśli chcemy stworzyć własną klasę, która implementuje IEnumerable<T>
:
public class PrintingEnumerable<T> : IEnumerable<T>
{
private IEnumerable<T> _wrapped;
public PrintingEnumerable(IEnumerable<T> wrapped)
{
_wrapped = wrapped;
}
// This method returns an IEnumerator<T>, rather than an IEnumerable<T>
// But the yield syntax and usage is identical.
public IEnumerator<T> GetEnumerator()
{
foreach(var item in _wrapped)
{
Console.WriteLine("Yielding: " + item);
yield return item;
}
}
IEnumerator IEnumerable.GetEnumerator()
{
return GetEnumerator();
}
}
(Należy zauważyć, że ten konkretny przykład jest tylko ilustracyjny i może być bardziej przejrzysty zaimplementowany za pomocą jednej metody iteratora zwracającej IEnumerable<T>
.)
Chętna ocena
yield
kluczowe yield
pozwala na leniwą ocenę kolekcji. Wymuszone ładowanie całej kolekcji do pamięci nazywa się chętną oceną .
Poniższy kod pokazuje to:
IEnumerable<int> myMethod()
{
for(int i=0; i <= 8675309; i++)
{
yield return i;
}
}
...
// define the iterator
var it = myMethod.Take(3);
// force its immediate evaluation
// list will contain 0, 1, 2
var list = it.ToList();
Wywołanie ToList
, ToDictionary
lub ToArray
wymusi natychmiastową ocenę wyliczenia, pobierając wszystkie elementy do kolekcji.
Leniwy przykład oceny: liczby Fibonacciego
using System;
using System.Collections.Generic;
using System.Linq;
using System.Numerics; // also add reference to System.Numberics
namespace ConsoleApplication33
{
class Program
{
private static IEnumerable<BigInteger> Fibonacci()
{
BigInteger prev = 0;
BigInteger current = 1;
while (true)
{
yield return current;
var next = prev + current;
prev = current;
current = next;
}
}
static void Main()
{
// print Fibonacci numbers from 10001 to 10010
var numbers = Fibonacci().Skip(10000).Take(10).ToArray();
Console.WriteLine(string.Join(Environment.NewLine, numbers));
}
}
}
Jak to działa pod maską (zalecam dekompilację wynikowego pliku .exe w narzędziu IL Disaambler):
- Kompilator w języku C # generuje klasę implementującą
IEnumerable<BigInteger>
iIEnumerator<BigInteger>
(<Fibonacci>d__0
w ildasm). - Ta klasa implementuje maszynę stanu. Stan składa się z aktualnej pozycji w metodzie i wartości zmiennych lokalnych.
- Najciekawsze kody znajdują się w
bool IEnumerator.MoveNext()
. Zasadniczo, coMoveNext()
:- Przywraca bieżący stan. Zmienne takie jak
prev
icurrent
stają się polami w naszej klasie (<current>5__2
i<prev>5__1
w ildasm). W naszej metodzie mamy dwie pozycje (<>1__state
): pierwsza na otwierającym<>1__state
klamrowym, druga na<>1__state
yield return
. - Wykonuje kod do następnego
yield return
yield break
lubyield break
yield return
/}
. - W celu
yield return
wynikowa wartość jest zapisywana, więcCurrent
właściwość może ją zwrócić.true
jest zwracana. W tym momencie bieżący stan jest ponownie zapisywany do następnego wywołaniaMoveNext
. - Dla metody
yield break
/}
po prostu zwracafalse
co oznacza, że iteracja jest zakończona.
- Przywraca bieżący stan. Zmienne takie jak
Zauważ również, że 10001-ta liczba ma 468 bajtów. Automat stanów zapisuje tylko zmienne current
i prev
jako pola. Chociaż jeśli chcielibyśmy zapisać wszystkie liczby w sekwencji od pierwszej do 10000, rozmiar zajętej pamięci wyniesie ponad 4 megabajty. Tak leniwa ocena, jeśli jest właściwie stosowana, może w niektórych przypadkach zmniejszyć ślad pamięci.
Różnica między granicą podziału a granicą plastyczności
Używanie yield break
w przeciwieństwie do break
może nie być tak oczywiste, jak mogłoby się wydawać. Istnieje wiele złych przykładów w Internecie, w których użycie tych dwóch jest wymienne i tak naprawdę nie pokazuje różnicy.
Mylące jest to, że oba słowa kluczowe (lub frazy kluczowe) mają sens tylko w pętlach ( foreach
, while
...) Więc kiedy wybrać jedno z nich?
Ważne jest, aby zdawać sobie sprawę, że po użyciu słowa kluczowego yield
w metodzie skutecznie zamieniasz tę metodę w iterator . Jedynym celem takiej metody jest następnie iteracja zbioru skończonego lub nieskończonego i uzyskanie (wyprowadzenie) jego elementów. Po osiągnięciu celu nie ma powodu, aby kontynuować wykonywanie metody. Czasami dzieje się to naturalnie w ostatnim nawiasie zamykającym metody }
. Ale czasami chcesz przedwcześnie zakończyć tę metodę. W normalnej (nie iterującej) metodzie użyłbyś słowa kluczowego return
. Ale nie można użyć return
w iteratorze, należy użyć yield break
. Innymi słowy, yield break
dla iteratora jest taki sam jak return
dla metody standardowej. Natomiast instrukcja break
po prostu kończy najbliższą pętlę.
Zobaczmy kilka przykładów:
/// <summary>
/// Yields numbers from 0 to 9
/// </summary>
/// <returns>{0,1,2,3,4,5,6,7,8,9}</returns>
public static IEnumerable<int> YieldBreak()
{
for (int i = 0; ; i++)
{
if (i < 10)
{
// Yields a number
yield return i;
}
else
{
// Indicates that the iteration has ended, everything
// from this line on will be ignored
yield break;
}
}
yield return 10; // This will never get executed
}
/// <summary>
/// Yields numbers from 0 to 10
/// </summary>
/// <returns>{0,1,2,3,4,5,6,7,8,9,10}</returns>
public static IEnumerable<int> Break()
{
for (int i = 0; ; i++)
{
if (i < 10)
{
// Yields a number
yield return i;
}
else
{
// Terminates just the loop
break;
}
}
// Execution continues
yield return 10;
}