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:

  1. Ocenić try ciało
  2. Oceń i buforuj zwróconą wartość
  3. Wykonaj wreszcie blok
  4. 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.



Modified text is an extract of the original Stack Overflow Documentation
Licencjonowany na podstawie CC BY-SA 3.0
Nie związany z Stack Overflow