One of the less talked about new features in C# 8 are using
declarations. That's not too surprising since it's essentially just syntactic sugar for using
statements.
Before C# 8, one could create an IDisposable
object with a using
statement so that it would be automatically disposed at the end of the using
block:
private IEnumerable<string> ReadLines(string path)
{
using (var reader = new StreamReader(path))
{
var line = reader.ReadLine();
while (line != null)
{
yield return line;
line = reader.ReadLine();
}
// reader is disposed
}
}
In C# 8, a using
declaration can be used in such a scenario. Unlike the using
statement, it doesn't introduce its own code block. Hence, the object is disposed at the end of the block it is contained in:
private IEnumerable<string> ReadLines(string path)
{
using var reader = new StreamReader(path);
var line = reader.ReadLine();
while (line != null)
{
yield return line;
line = reader.ReadLine();
}
// reader is disposed
}
The new syntax might make it less obvious what's happening until you get used to it. But it reduces the number of nested blocks, especially when you're dealing with multiple disposable objects:
private string Serialize(IDictionary<string, string> properties)
{
using var stream = new MemoryStream();
using var jsonWriter = new Utf8JsonWriter(stream);
jsonWriter.WriteStartObject();
foreach (var pair in properties)
{
jsonWriter.WriteString(pair.Key, pair.Value);
}
jsonWriter.WriteEndObject();
stream.Position = 0;
using var reader = new StreamReader(stream);
return reader.ReadToEnd();
// stream is disposed
// jsonWriter is disposed
// reader is disposed
}
There are three using
declarations in the code above which would mean three nested using
blocks if using
statements were used instead:
private string Serialize(IDictionary<string, string> properties)
{
using (var stream = new MemoryStream())
{
using (var jsonWriter = new Utf8JsonWriter(stream))
{
jsonWriter.WriteStartObject();
foreach (var pair in properties)
{
jsonWriter.WriteString(pair.Key, pair.Value);
}
jsonWriter.WriteEndObject();
// jsonWriter is disposed
}
stream.Position = 0;
using (var reader = new StreamReader(stream))
{
return reader.ReadToEnd();
// reader is disposed
}
// stream is disposed
}
}
However, the method with using
declarations is broken. It will throw an ObjectDisposedException
:
System.ObjectDisposedException : Cannot access a closed Stream.
Looking at the stack trace will make it more obvious what happened:
MemoryStream.Write(ReadOnlySpan`1 buffer)
Utf8JsonWriter.Flush()
Utf8JsonWriter.Dispose()
Tests.SerializeWithUsingDeclarations(IDictionary`2 properties)
The disposable objects were disposed at the end of the method in the order they were declared. This means that the MemoryStream
was disposed first and the Utf8JsonWriter
second. When the latter was disposed, the Flush
method was called because not everything was yet written to the underlying MemoryStream
. The MemoryStream
was already disposed at that time, therefore the ObjectDisposedException
was thrown.
Why doesn't that happen with using
statements? The way they work, the object created in the inner using
statement will always be disposed first. Also, notice how I made the using
block for Utf8JsonWriter
smaller to ensure it was disposed (and flushed) before I start reading from the MemoryStream
.
How could the code with using
declarations be fixed? The Flush
method can be called explicitly where it needs to be:
private string Serialize(IDictionary<string, string> properties)
{
using var stream = new MemoryStream();
using var jsonWriter = new Utf8JsonWriter(stream);
jsonWriter.WriteStartObject();
foreach (var pair in properties)
{
jsonWriter.WriteString(pair.Key, pair.Value);
}
jsonWriter.WriteEndObject();
jsonWriter.Flush();
stream.Position = 0;
using var reader = new StreamReader(stream);
return reader.ReadToEnd();
// stream is disposed
// jsonWriter is disposed
// reader is disposed
}
Although the objects will still be disposed in the order they were created, no exception will be thrown because the Utf8JsonWriter
doesn't need to be flushed when it's disposed.
Alternatively, a using
statement can be used for Utf8JsonWriter
to more strictly control when it's disposed. The using
declaration can still be used for the other two disposable objects which will be disposed at the end of the method:
private string Serializ(IDictionary<string, string> properties)
{
using var stream = new MemoryStream();
using (var jsonWriter = new Utf8JsonWriter(stream))
{
jsonWriter.WriteStartObject();
foreach (var pair in properties)
{
jsonWriter.WriteString(pair.Key, pair.Value);
}
jsonWriter.WriteEndObject();
// jsonWriter is disposed
}
stream.Position = 0;
using var reader = new StreamReader(stream);
return reader.ReadToEnd();
// stream is disposed
// reader is disposed
}
Use using
declarations with care. The exact location in the code where an object is disposed and the order in which multiple objects are disposed might be different than with using
statements. Since other methods might be called implicitly when an object is disposed, this might change the behavior of your code making it incorrect.