Rozumiejąc, jak yield
działa słowo kluczowe, natknąłem się na link1 i link2 na StackOverflow, który zaleca korzystanie z yield return
iteracji nad DataReaderem i odpowiada to również moim potrzebom. Ale zastanawiam się, co się stanie, jeśli użyję, yield return
jak pokazano poniżej i jeśli nie wykonam iteracji przez cały DataReader, czy połączenie DB pozostanie otwarte na zawsze?
IEnumerable<IDataRecord> GetRecords()
{
SqlConnection myConnection = new SqlConnection(@"...");
SqlCommand myCommand = new SqlCommand(@"...", myConnection);
myCommand.CommandType = System.Data.CommandType.Text;
myConnection.Open();
myReader = myCommand.ExecuteReader(CommandBehavior.CloseConnection);
try
{
while (myReader.Read())
{
yield return myReader;
}
}
finally
{
myReader.Close();
}
}
void AnotherMethod()
{
foreach(var rec in GetRecords())
{
i++;
System.Console.WriteLine(rec.GetString(1));
if (i == 5)
break;
}
}
Próbowałem tego samego przykładu w przykładowej aplikacji konsolowej i zauważyłem podczas debugowania, że ostatecznie blok GetRecords()
nie jest wykonywany. Jak mogę wtedy zapewnić zamknięcie połączenia DB? Czy istnieje lepszy sposób niż użycie yield
słowa kluczowego? Próbuję zaprojektować niestandardową klasę, która będzie odpowiedzialna za wykonywanie wybranych instrukcji SQL i procedur składowanych w bazie danych i zwróci wynik. Ale nie chcę zwracać DataReader do dzwoniącego. Chcę również upewnić się, że połączenie zostanie zamknięte we wszystkich scenariuszach.
Edytuj Zmieniono odpowiedź na odpowiedź Bena, ponieważ niepoprawne jest oczekiwanie, że osoby wywołujące metodę będą używać tej metody poprawnie, a w odniesieniu do połączenia z DB będzie ona droższa, jeśli metoda zostanie wywołana wiele razy bez powodu.
Dzięki Jakob i Ben za szczegółowe wyjaśnienia.
Odpowiedzi:
Tak, napotkasz opisany problem: dopóki nie zakończysz iteracji wyniku, połączenie pozostanie otwarte. Są dwa ogólne podejścia, które mogę wymyślić, aby sobie z tym poradzić:
Pchaj, nie ciągnij
Obecnie zwracasz
IEnumerable<IDataRecord>
strukturę danych, z której możesz pobrać. Zamiast tego możesz zmienić metodę, aby wypchnąć jej wyniki. Najprostszym sposobem byłoby przekazanie polecenia,Action<IDataRecord>
które jest wywoływane przy każdej iteracji:Pamiętaj, że biorąc pod uwagę, że masz do czynienia z kolekcją elementów,
IObservable
/IObserver
może być nieco bardziej odpowiednią strukturą danych, ale jeśli nie jest to potrzebne, prostaAction
jest znacznie prostsza .Chętnie oceniaj
Alternatywą jest upewnienie się, że iteracja całkowicie się zakończyła przed powrotem.
Zwykle możesz to zrobić, po prostu umieszczając wyniki na liście, a następnie zwracając je, ale w tym przypadku istnieje dodatkowa komplikacja każdego elementu będącego tym samym odwołaniem do czytelnika. Potrzebujesz więc czegoś, co wyodrębni wynik z czytnika:
źródło
Twój
finally
blok zawsze będzie wykonywany.Podczas korzystania
yield return
z kompilatora utworzy nową klasę zagnieżdżoną do implementacji automatu stanów.Ta klasa będzie zawierać cały kod z
finally
bloków jako osobne metody. Będzie śledziłfinally
bloki, które należy wykonać w zależności od stanu. Wszystkie niezbędnefinally
bloki zostaną wykonaneDispose
metodą.Zgodnie ze specyfikacją języka C #
foreach (V v in x) embedded-statement
jest rozszerzony dowięc moduł wyliczający zostanie usunięty, nawet jeśli wyjdziesz z pętli za pomocą
break
lubreturn
.Aby uzyskać więcej informacji na temat implementacji iteratora, przeczytaj ten artykuł Jon Skeet
Edytować
Problem z takim podejściem polega na tym, że polegasz na klientach swojej klasy, aby prawidłowo używać metody. Mogliby na przykład pobrać moduł wyliczający bezpośrednio i iterować go w
while
pętli bez usuwania go.Powinieneś rozważyć jedno z rozwiązań sugerowanych przez @BenAaronson.
źródło
var records = GetRecords(); var first = records.First(); var count = records.Count()
faktycznie wykona tę metodę dwukrotnie, za każdym razem otwierając i zamykając połączenie i czytnik. Dwukrotne powtórzenie tego samego robi to samo. Możesz również przekazać wynik przez długi czas, zanim zacznie się iteracja itd. Nic w podpisie metody nie sugeruje tego rodzaju zachowaniaGetRecords
returnIEnumerable
, oczekuję, że załaduje ona rekordy do pamięci. Z drugiej strony, jeśliRecords
byłaby to właściwość , wolałbym raczej założyć, że jest to jakaś abstrakcja nad bazą danych.Niezależnie od określonego
yield
zachowania, kod zawiera ścieżki wykonania, które nie będą poprawnie pozbywały się zasobów. Co się stanie, jeśli druga linia zgłosi wyjątek lub trzecią? A nawet twój czwarty? Będziesz potrzebować bardzo złożonego łańcucha try / wreszcie lub możesz użyćusing
bloków.Ludzie zauważyli, że nie jest to intuicyjne, że osoby wywołujące twoją metodę mogą nie wiedzieć, że dwukrotne wyliczenie wyniku wywoła metodę dwukrotnie. Cóż, pech. Tak działa ten język. To jest sygnał, że
IEnumerable<T>
wysyła. Istnieje powód, dla którego nie zwracaList<T>
lubT[]
. Ludzie, którzy nie wiedzą o tym, muszą się kształcić, a nie pracować.Visual Studio ma funkcję o nazwie statyczna analiza kodu. Możesz go użyć, aby sprawdzić, czy prawidłowo zutylizowałeś zasoby.
źródło
IEnumerable
różnych czynności, takich jak zgłaszanie całkowicie niepowiązanych wyjątków lub zapisywanie plików na dysku o pojemności 5 GB. Tylko dlatego, że język na to pozwala, nie oznacza, że dopuszczalne jest zwrócenieIEnumerable
tego, co robi to z metody, która nie daje żadnych wskazówek, że tak się stanie.IEnumerable<T>
oznacza , że dwukrotne policzenie spowoduje dwa połączenia. Otrzymasz nawet pomocne ostrzeżenia w swoich narzędziach, jeśli wyliczysz wyliczenie wiele razy. Punkt dotyczący zgłaszania wyjątków jest równie ważny bez względu na typ zwrotu. Jeśli ta metoda zwróci aList<T>
, wyrzuci te same wyjątki.IEnumerable
opróżnienie. Ale myślę też, że jako twórca metod jesteś odpowiedzialny za przestrzeganie tego, co sugeruje twój podpis. Wywoływana jest metodaGetRecords
, więc kiedy powróci, można się spodziewać, że uzyskała rekordy . Gdyby został nazwanyGiveMeSomethingICanPullRecordsFrom
(lub bardziej realistycznieGetRecordSource
lub podobnie), leniwyIEnumerable
byłby bardziej do przyjęcia.File
,ReadLines
iReadAllLines
można je łatwo rozdzielić i to dobrze. (Chociaż jest to bardziej spowodowane faktem, że przeciążenie metody nie może różnić się tylko typem zwrotu). Może ReadRecords i ReadAllRecords pasują tutaj również jako schemat nazewnictwa.To, co zrobiłeś, wydaje się złe, ale zadziała dobrze.
Powinieneś użyć IEnumerator <> , ponieważ dziedziczy on także IDisposable .
Ale ponieważ używasz foreach, kompilator nadal używa IDisposable do generowania IEnumerator <> dla foreach.
w rzeczywistości Foreach zawiera wiele rzeczy wewnętrznych.
Sprawdź /programming/13459447/do-i-need-to-consider-disposing-of-any-ienumerablet-i-use
źródło