C# Language
Za pomocą instrukcji
Szukaj…
Wprowadzenie
Zapewnia wygodną składnię, która zapewnia prawidłowe użycie obiektów IDisposable .
Składnia
- using (jednorazowy) {}
- using (IDisposable disposable = new MyDisposable ()) {}
Uwagi
Obiekt w instrukcji using
musi implementować interfejs IDisposable
.
using(var obj = new MyObject())
{
}
class MyObject : IDisposable
{
public void Dispose()
{
// Cleanup
}
}
Bardziej kompletne przykłady implementacji IDisposable
można znaleźć w dokumentacji MSDN .
Korzystanie z podstaw instrukcji
using
to cukier składniowy, który pozwala zagwarantować, że zasób zostanie oczyszczony bez potrzeby jawnego blokowania try-finally
. Oznacza to, że Twój kod będzie znacznie czystszy i nie wycieknie niezarządzanych zasobów.
Standardowy wzorzec czyszczenia Dispose
, dla obiektów, które implementują interfejs IDisposable
(co robi Stream
podstawowy klasy FileStream
w .NET):
int Foo()
{
var fileName = "file.txt";
{
FileStream disposable = null;
try
{
disposable = File.Open(fileName, FileMode.Open);
return disposable.ReadByte();
}
finally
{
// finally blocks are always run
if (disposable != null) disposable.Dispose();
}
}
}
using
upraszcza składnię, ukrywając jawne try-finally
:
int Foo()
{
var fileName = "file.txt";
using (var disposable = File.Open(fileName, FileMode.Open))
{
return disposable.ReadByte();
}
// disposable.Dispose is called even if we return earlier
}
Tak jak w finally
bloki zawsze wykonują się niezależnie od błędów lub zwrotów, using
zawsze wywołuje Dispose()
, nawet w przypadku błędu:
int Foo()
{
var fileName = "file.txt";
using (var disposable = File.Open(fileName, FileMode.Open))
{
throw new InvalidOperationException();
}
// disposable.Dispose is called even if we throw an exception earlier
}
Uwaga: Ponieważ gwarantuje się, że Dispose
jest wywoływany niezależnie od przepływu kodu, warto upewnić się, że Dispose
nigdy nie zgłasza wyjątku podczas implementacji IDisposable
. W przeciwnym razie nowy wyjątek zostałby zastąpiony przez nowy wyjątek, co spowodowałoby koszmar debugowania.
Wracając z używania bloku
using ( var disposable = new DisposableItem() )
{
return disposable.SomeProperty;
}
Ze względu na semantykę try..finally
na którą tłumaczy się blok using
, instrukcja return
działa zgodnie z oczekiwaniami - zwracana wartość jest obliczana przed finally
bloku i przekazaniem wartości. Kolejność oceny jest następująca:
- Ocenić
try
ciało - Oceń i buforuj zwróconą wartość
- Wykonaj wreszcie blok
- Zwraca buforowaną wartość zwracaną
Nie możesz jednak zwrócić zmiennej disposable
, ponieważ zawierałaby ona niepoprawne odwołanie - patrz powiązany przykład .
Wielokrotne użycie instrukcji w jednym bloku
Możliwe jest użycie wielu zagnieżdżonych using
instrukcji bez dodawania wielu poziomów zagnieżdżonych nawiasów klamrowych. Na przykład:
using (var input = File.OpenRead("input.txt"))
{
using (var output = File.OpenWrite("output.txt"))
{
input.CopyTo(output);
} // output is disposed here
} // input is disposed here
Alternatywą jest napisanie:
using (var input = File.OpenRead("input.txt"))
using (var output = File.OpenWrite("output.txt"))
{
input.CopyTo(output);
} // output and then input are disposed here
Co jest dokładnie równoważne z pierwszym przykładem.
Uwaga: Zagnieżdżone using
instrukcji może wywołać regułę Microsoft Code Analysis CS2002 (patrz wyjaśnienie w tej odpowiedzi ) i wygenerować ostrzeżenie. Jak wyjaśniono w odpowiedzi na link, można bezpiecznie zagnieżdżać using
instrukcji.
Gdy typy w instrukcji using
są tego samego typu, możesz je rozdzielić przecinkami i określić typ tylko raz (choć jest to rzadkie):
using (FileStream file = File.Open("MyFile.txt"), file2 = File.Open("MyFile2.txt"))
{
}
Można tego również użyć, gdy typy mają wspólną hierarchię:
using (Stream file = File.Open("MyFile.txt"), data = new MemoryStream())
{
}
var
kluczowego var
nie można użyć w powyższym przykładzie. Wystąpił błąd kompilacji. Nawet deklaracja oddzielona przecinkami nie będzie działać, gdy deklarowane zmienne mają typy z różnych hierarchii.
Gotcha: zwracanie zasobu, który dysponujesz
Poniższy pomysł jest zły, ponieważ db
zmiennej db
przed jej zwróceniem.
public IDBContext GetDBContext()
{
using (var db = new DBContext())
{
return db;
}
}
Może to również powodować bardziej subtelne błędy:
public IEnumerable<Person> GetPeople(int age)
{
using (var db = new DBContext())
{
return db.Persons.Where(p => p.Age == age);
}
}
Wygląda to dobrze, ale haczyk polega na tym, że ocena wyrażenia LINQ jest leniwa i prawdopodobnie zostanie wykonana później, gdy bazowy DBContext
zostanie już usunięty.
Krótko mówiąc, wyrażenie nie jest oceniane przed opuszczeniem using
. Jednym z możliwych rozwiązań tego problemu, który wciąż korzysta z using
, jest spowodowanie natychmiastowej oceny wyrażenia przez wywołanie metody, która wyliczy wynik. Na przykład ToList()
, ToArray()
itp. Jeśli używasz najnowszej wersji Entity Framework, możesz użyć async
odpowiedników, takich jak ToListAsync()
lub ToArrayAsync()
.
Poniżej znajdziesz przykład w akcji:
public IEnumerable<Person> GetPeople(int age)
{
using (var db = new DBContext())
{
return db.Persons.Where(p => p.Age == age).ToList();
}
}
Należy jednak zauważyć, że wywołując ToList()
lub ToArray()
, wyrażenie zostanie z niecierpliwością ocenione, co oznacza, że wszystkie osoby o określonym wieku zostaną załadowane do pamięci, nawet jeśli nie wykonasz iteracji.
Korzystanie z instrukcji jest bezpieczne
Nie musisz sprawdzać obiektu IDisposable
kątem null
. using
nie spowoduje zgłoszenia wyjątku, a funkcja Dispose()
nie zostanie wywołana:
DisposableObject TryOpenFile()
{
return null;
}
// disposable is null here, but this does not throw an exception
using (var disposable = TryOpenFile())
{
// this will throw a NullReferenceException because disposable is null
disposable.DoSomething();
if(disposable != null)
{
// here we are safe because disposable has been checked for null
disposable.DoSomething();
}
}
Gotcha: wyjątek w metodzie Dispose maskują inne błędy w korzystaniu z bloków
Rozważ następujący blok kodu.
try
{
using (var disposable = new MyDisposable())
{
throw new Exception("Couldn't perform operation.");
}
}
catch (Exception ex)
{
Console.WriteLine(ex.Message);
}
class MyDisposable : IDisposable
{
public void Dispose()
{
throw new Exception("Couldn't dispose successfully.");
}
}
Możesz spodziewać się, że na konsoli wyświetli się komunikat „Nie można wykonać operacji”, ale w rzeczywistości zobaczysz komunikat „Nie można skutecznie usunąć”. ponieważ metoda Dispose jest nadal wywoływana nawet po zgłoszeniu pierwszego wyjątku.
Warto mieć świadomość tej subtelności, ponieważ może ona maskować prawdziwy błąd, który uniemożliwił usunięcie obiektu i utrudnił debugowanie.
Korzystanie z instrukcji i połączeń z bazą danych
using
kluczowe using
zapewnia, że zasób zdefiniowany w instrukcji istnieje tylko w zakresie samej instrukcji. Wszelkie zasoby zdefiniowane w instrukcji muszą implementować interfejs IDisposable
.
Są one niezwykle ważne, gdy mamy do czynienia z wszelkimi połączeniami, które implementują interfejs IDisposable
, ponieważ może to zapewnić nie tylko prawidłowe zamknięcie połączeń, ale także uwolnienie ich zasobów, gdy instrukcja using
jest poza zakresem.
Wspólne klasy danych IDisposable
Wiele z poniższych to klasy związane z danymi, które implementują interfejs IDisposable
i są idealnymi kandydatami na instrukcję using
:
-
SqlConnection
,SqlCommand
,SqlDataReader
itp. -
OleDbConnection
,OleDbCommand
,OleDbDataReader
itp. -
MySqlConnection
,MySqlCommand
,MySqlDbDataReader
itp. -
DbContext
Wszystkie te są powszechnie używane do uzyskiwania dostępu do danych za pośrednictwem C # i będą często spotykane podczas tworzenia aplikacji zorientowanych na dane. Można oczekiwać, że wiele innych niewymienionych klas, które implementują te same FooConnection
, FooCommand
, FooDataReader
będą zachowywać się w ten sam sposób.
Wspólny wzorzec dostępu dla połączeń ADO.NET
Typowy wzorzec, którego można użyć podczas uzyskiwania dostępu do danych za pośrednictwem połączenia ADO.NET, może wyglądać następująco:
// This scopes the connection (your specific class may vary)
using(var connection = new SqlConnection("{your-connection-string}")
{
// Build your query
var query = "SELECT * FROM YourTable WHERE Property = @property");
// Scope your command to execute
using(var command = new SqlCommand(query, connection))
{
// Open your connection
connection.Open();
// Add your parameters here if necessary
// Execute your query as a reader (again scoped with a using statement)
using(var reader = command.ExecuteReader())
{
// Iterate through your results here
}
}
}
Lub jeśli po prostu przeprowadzasz prostą aktualizację i nie potrzebujesz czytnika, zastosowanie miałaby ta sama podstawowa koncepcja:
using(var connection = new SqlConnection("{your-connection-string}"))
{
var query = "UPDATE YourTable SET Property = Value WHERE Foo = @foo";
using(var command = new SqlCommand(query,connection))
{
connection.Open();
// Add parameters here
// Perform your update
command.ExecuteNonQuery();
}
}
Używanie instrukcji z danymi DataContexts
Wiele ORM, takich jak Entity Framework, udostępnia klasy abstrakcji, które są używane do interakcji z bazowymi bazami danych w postaci klas takich jak DbContext
. Te konteksty ogólnie implementują również interfejs IDisposable
i powinny to using
poprzez using
instrukcji, gdy to możliwe:
using(var context = new YourDbContext())
{
// Access your context and perform your query
var data = context.Widgets.ToList();
}
Używanie składni Dispose do zdefiniowania niestandardowego zakresu
W niektórych przypadkach użycia można użyć składni using
aby pomóc zdefiniować zakres niestandardowy. Na przykład możesz zdefiniować następującą klasę do wykonywania kodu w określonej kulturze.
public class CultureContext : IDisposable
{
private readonly CultureInfo originalCulture;
public CultureContext(string culture)
{
originalCulture = CultureInfo.CurrentCulture;
Thread.CurrentThread.CurrentCulture = new CultureInfo(culture);
}
public void Dispose()
{
Thread.CurrentThread.CurrentCulture = originalCulture;
}
}
Następnie można użyć tej klasy do zdefiniowania bloków kodu wykonanych w określonej kulturze.
Thread.CurrentThread.CurrentCulture = new CultureInfo("en-US");
using (new CultureContext("nl-NL"))
{
// Code in this block uses the "nl-NL" culture
Console.WriteLine(new DateTime(2016, 12, 25)); // Output: 25-12-2016 00:00:00
}
using (new CultureContext("es-ES"))
{
// Code in this block uses the "es-ES" culture
Console.WriteLine(new DateTime(2016, 12, 25)); // Output: 25/12/2016 0:00:00
}
// Reverted back to the original culture
Console.WriteLine(new DateTime(2016, 12, 25)); // Output: 12/25/2016 12:00:00 AM
Uwaga: ponieważ nie używamy tworzonej instancji CultureContext
, nie przypisujemy do niej zmiennej.
Ta technika jest używana przez pomocnika BeginForm
w ASP.NET MVC.
Wykonywanie kodu w kontekście ograniczeń
Jeśli masz kod ( procedurę ), który chcesz wykonać w określonym (ograniczonym) kontekście, możesz użyć wstrzykiwania zależności.
Poniższy przykład pokazuje ograniczenie wykonywania przy otwartym połączeniu SSL. Ta pierwsza część będzie w twojej bibliotece lub frameworku, której nie ujawnisz kodowi klienta.
public static class SSLContext
{
// define the delegate to inject
public delegate void TunnelRoutine(BinaryReader sslReader, BinaryWriter sslWriter);
// this allows the routine to be executed under SSL
public static void ClientTunnel(TcpClient tcpClient, TunnelRoutine routine)
{
using (SslStream sslStream = new SslStream(tcpClient.GetStream(), true, _validate))
{
sslStream.AuthenticateAsClient(HOSTNAME, null, SslProtocols.Tls, false);
if (!sslStream.IsAuthenticated)
{
throw new SecurityException("SSL tunnel not authenticated");
}
if (!sslStream.IsEncrypted)
{
throw new SecurityException("SSL tunnel not encrypted");
}
using (BinaryReader sslReader = new BinaryReader(sslStream))
using (BinaryWriter sslWriter = new BinaryWriter(sslStream))
{
routine(sslReader, sslWriter);
}
}
}
}
Teraz kod klienta, który chce zrobić coś w ramach SSL, ale nie chce obsłużyć wszystkich szczegółów SSL. Możesz teraz robić, co chcesz w tunelu SSL, na przykład wymienić klucz symetryczny:
public void ExchangeSymmetricKey(BinaryReader sslReader, BinaryWriter sslWriter)
{
byte[] bytes = new byte[8];
(new RNGCryptoServiceProvider()).GetNonZeroBytes(bytes);
sslWriter.Write(BitConverter.ToUInt64(bytes, 0));
}
Wykonujesz tę procedurę w następujący sposób:
SSLContext.ClientTunnel(tcpClient, this.ExchangeSymmetricKey);
Aby to zrobić, potrzebujesz klauzuli using()
, ponieważ jest to jedyny sposób (oprócz bloku try..finally
), aby zagwarantować, że kod klienta ( ExchangeSymmetricKey
) nigdy nie zostanie try..finally
bez odpowiedniego usunięcia zasobów jednorazowych. Bez using()
klauzuli using()
nigdy nie wiadomo, czy procedura mogłaby przełamać ograniczenia kontekstu dotyczące dysponowania tymi zasobami.