C# Language
Utilizzando la dichiarazione
Ricerca…
introduzione
Fornisce una comoda sintassi che garantisce l'uso corretto di oggetti IDisposable .
Sintassi
- usando (usa e getta) {}
- utilizzando (IDisposable disposable = new MyDisposable ()) {}
Osservazioni
L'oggetto IDisposable
using
deve implementare l'interfaccia IDisposable
.
using(var obj = new MyObject())
{
}
class MyObject : IDisposable
{
public void Dispose()
{
// Cleanup
}
}
Esempi più completi per l'implementazione IDisposable
possono essere trovati nei documenti MSDN .
Utilizzo delle nozioni di base sulle istruzioni
using
dello zucchero sintattico consente di garantire che una risorsa venga pulita senza bisogno di un blocco try-finally
esplicito. Questo significa che il tuo codice sarà molto più pulito e non perderai risorse non gestite.
Standard Dispose
cleanup pattern, per gli oggetti che implementano l'interfaccia IDisposable
(che la classe base Stream
FileStream
esegue in .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
semplifica la sintassi nascondendo l'esplicito 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
}
Proprio come i blocchi finally
eseguono sempre indipendentemente da errori o ritorni, using
sempre chiamate Dispose()
, anche in caso di errore:
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
}
Nota: Poiché si garantisce che Dispose
sia chiamato indipendentemente dal flusso del codice, è una buona idea assicurarsi che Dispose
non Dispose
mai un'eccezione quando si implementa IDisposable
. Altrimenti un'eccezione effettiva verrebbe sovrascritta dalla nuova eccezione che risulterebbe in un incubo di debug.
Ritornando dall'utilizzo del blocco
using ( var disposable = new DisposableItem() )
{
return disposable.SomeProperty;
}
A causa delle semantica di try..finally
cui il using
blocco traduce, il return
economico funziona come previsto - il valore di ritorno viene valutata prima finally
viene eseguito blocco e il valore disposto. L'ordine di valutazione è il seguente:
- Valuta il corpo del
try
- Valuta e memorizza il valore restituito
- Esegui infine il blocco
- Restituisce il valore di ritorno memorizzato nella cache
Tuttavia, non è possibile restituire la variabile disposable
e disposable
stessa, poiché essa conterrebbe un riferimento disposto non valido - vedere l' esempio correlato .
Molteplici utilizzo di istruzioni con un blocco
È possibile utilizzare più nidificate using
dichiarazioni senza aggiunta di più livelli di parentesi nidificate. Per esempio:
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
Un'alternativa è scrivere:
using (var input = File.OpenRead("input.txt"))
using (var output = File.OpenWrite("output.txt"))
{
input.CopyTo(output);
} // output and then input are disposed here
Quale è esattamente equivalente al primo esempio.
Nota: le istruzioni nidificate using
potrebbero attivare la regola CS2002 (vedere questa risposta per chiarimenti) e generare un avviso. Come spiegato nella risposta collegata, è generalmente sicuro annidare using
istruzioni.
Quando i tipi all'interno dell'istruzione using
sono dello stesso tipo, puoi separarli con virgole e specificare il tipo una sola volta (sebbene questo sia raro):
using (FileStream file = File.Open("MyFile.txt"), file2 = File.Open("MyFile2.txt"))
{
}
Questo può essere utilizzato anche quando i tipi hanno una gerarchia condivisa:
using (Stream file = File.Open("MyFile.txt"), data = new MemoryStream())
{
}
La parola chiave var
non può essere utilizzata nell'esempio precedente. Si verificherebbe un errore di compilazione. Anche la dichiarazione separata da virgole non funzionerà quando le variabili dichiarate hanno tipi di gerarchie diverse.
Gotcha: restituire la risorsa che si sta smaltendo
La seguente è una cattiva idea perché eliminerebbe la variabile db
prima di restituirla.
public IDBContext GetDBContext()
{
using (var db = new DBContext())
{
return db;
}
}
Questo può anche creare errori più sottili:
public IEnumerable<Person> GetPeople(int age)
{
using (var db = new DBContext())
{
return db.Persons.Where(p => p.Age == age);
}
}
Questo sembra ok, ma il problema è che la valutazione delle espressioni LINQ è DBContext
e probabilmente verrà eseguita solo in un secondo momento, quando il DBContext
sottostante è già stato eliminato.
Quindi in breve l'espressione non viene valutata prima di lasciare l' using
. Una possibile soluzione a questo problema, il che rende ancora uso di using
, è di indurre l'espressione di valutare immediatamente chiamando un metodo che enumerare il risultato. Ad esempio ToList()
, ToArray()
, ecc. Se si utilizza la versione più recente di Entity Framework, è possibile utilizzare le controparti async
come ToListAsync()
o ToArrayAsync()
.
Di seguito trovi l'esempio in azione:
public IEnumerable<Person> GetPeople(int age)
{
using (var db = new DBContext())
{
return db.Persons.Where(p => p.Age == age).ToList();
}
}
È importante notare, tuttavia, che chiamando ToList()
o ToArray()
, l'espressione verrà valutata con entusiasmo, il che significa che tutte le persone con l'età specificata verranno caricate in memoria anche se non si esegue iterazione su di esse.
L'utilizzo delle istruzioni è sicuro
Non è necessario controllare l'oggetto IDisposable
per null
. using
non genererà un'eccezione e Dispose()
non verrà chiamato:
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: Eccezione nel metodo Dispose che maschera altri errori in Uso dei blocchi
Considera il seguente blocco di codice.
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.");
}
}
Ci si può aspettare di vedere "Impossibile eseguire l'operazione" stampato sulla console ma si vedrebbe effettivamente "Impossibile eseguire correttamente l'operazione". poiché il metodo Dispose viene ancora chiamato anche dopo che la prima eccezione è stata lanciata.
Vale la pena essere consapevoli di questa sottigliezza poiché potrebbe nascondere l'errore reale che impediva la disposizione dell'oggetto e rendeva più difficile il debug.
Utilizzando le istruzioni e le connessioni al database
La parola chiave using
assicura che la risorsa definita all'interno dell'istruzione esista solo nell'ambito dell'istruzione stessa. Qualsiasi risorsa definita all'interno dell'istruzione deve implementare l'interfaccia IDisposable
.
Questi sono incredibilmente importanti quando si tratta di connessioni che implementano l'interfaccia IDisposable
in quanto possono garantire che le connessioni non solo siano chiuse correttamente ma che le loro risorse siano liberate dopo che l'istruzione using
è fuori dall'ambito.
Classi di dati comuni IDisposable
Molti dei seguenti sono classi correlate ai dati che implementano l'interfaccia IDisposable
e sono candidati perfetti per una dichiarazione using
:
-
SqlConnection
,SqlCommand
,SqlDataReader
, ecc. -
OleDbConnection
,OleDbCommand
,OleDbDataReader
, ecc. -
MySqlConnection
,MySqlCommand
,MySqlDbDataReader
, ecc. -
DbContext
Tutti questi sono comunemente usati per accedere ai dati tramite C # e verranno comunemente riscontrati durante la creazione di applicazioni incentrate sui dati. FooDataReader
si può aspettare che molte altre classi che non sono menzionate che implementano le stesse FooConnection
, FooCommand
, FooDataReader
si comportino allo stesso modo.
Modello di accesso comune per connessioni ADO.NET
Un modello comune che può essere utilizzato quando si accede ai dati tramite una connessione ADO.NET potrebbe essere il seguente:
// 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
}
}
}
O se stavi semplicemente eseguendo un semplice aggiornamento e non avessi bisogno di un lettore, si applicherebbe lo stesso concetto di base:
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();
}
}
Utilizzare le istruzioni con DataContexts
Molti ORM come Entity Framework espongono le classi di astrazione utilizzate per interagire con i database sottostanti sotto forma di classi come DbContext
. Questi contesti generalmente implementano anche l'interfaccia IDisposable
e dovrebbero trarne vantaggio attraverso l' using
dichiarazioni quando possibile:
using(var context = new YourDbContext())
{
// Access your context and perform your query
var data = context.Widgets.ToList();
}
Utilizzare Dispose Syntax per definire l'ambito personalizzato
Per alcuni casi d'uso, è possibile utilizzare la sintassi using
per aiutare a definire un ambito personalizzato. Ad esempio, è possibile definire la seguente classe per eseguire codice in una cultura specifica.
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;
}
}
È quindi possibile utilizzare questa classe per definire blocchi di codice che vengono eseguiti in una cultura specifica.
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
Nota: poiché non utilizziamo l'istanza di CultureContext
che creiamo, non assegniamo una variabile per questo.
Questa tecnica viene utilizzata dal BeginForm
aiutante in ASP.NET MVC.
Esecuzione del codice nel contesto del vincolo
Se si dispone di codice (una routine ) che si desidera eseguire in un contesto specifico (vincolo), è possibile utilizzare l'iniezione della dipendenza.
L'esempio seguente mostra il vincolo di esecuzione in una connessione SSL aperta. Questa prima parte sarà nella libreria o nel framework, che non esporrai al codice cliente.
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);
}
}
}
}
Ora il codice client che vuole fare qualcosa sotto SSL ma non vuole gestire tutti i dettagli SSL. Ora puoi fare tutto ciò che vuoi all'interno del tunnel SSL, ad esempio scambiare una chiave simmetrica:
public void ExchangeSymmetricKey(BinaryReader sslReader, BinaryWriter sslWriter)
{
byte[] bytes = new byte[8];
(new RNGCryptoServiceProvider()).GetNonZeroBytes(bytes);
sslWriter.Write(BitConverter.ToUInt64(bytes, 0));
}
Esegui questa routine come segue:
SSLContext.ClientTunnel(tcpClient, this.ExchangeSymmetricKey);
Per fare ciò, è necessaria la clausola using()
perché è l'unico modo (a parte un try..finally
block) che puoi garantire che il codice client ( ExchangeSymmetricKey
) non esca mai senza disporre correttamente delle risorse usa e getta. Senza la clausola using()
, non si potrebbe mai sapere se una routine potrebbe rompere il vincolo del contesto per smaltire tali risorse.