Be Careful with Using Declarations in C# 8
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();
// reader is disposed
// jsonWriter is disposed
// stream 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
}
}
If you're familiar with how writing to streams works, you should be able to notice that while the method with using
statements works as expected, the method with using
declarations doesn't. The behavior of both methods is different because jsonWriter
is disposed at different times:
- In the method with
using
statements,jsonWriter
is disposed beforereader
starts reading from the stream. At that point theFlush
method is called implicitly to write everything to the underlyingstream
. - In the method with
using
declarations,jsonWriter
is disposed afterreader
is done with reading for the stream. Because theFlush
method was not called onjsonWriter
before that, not everything was necessarily written to the stream already.
There are two ways to fix this issue:
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(); // reader is disposed // jsonWriter is disposed // stream is disposed }
Alternatively, a
using
statement can be used forjsonWriter
to more strictly control when it's disposed. Theusing
declaration can still be used for the other two disposable objects which will be disposed at the end of the method: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 }
Interestingly enough, if you try running my original method with using
declarations before applying one of the two suggested fixes, it will throw an ObjectDisposedException
instead of simply returning the wrong result:
System.ObjectDisposedException : Cannot access a closed Stream.
The stack trace will help us determine what actually happened:
MemoryStream.Write(ReadOnlySpan`1 buffer)
Utf8JsonWriter.Flush()
Utf8JsonWriter.Dispose()
Tests.SerializeWithUsingDeclarations(IDictionary`2 properties)
When jsonWriter
was disposed, the Flush
method was called because not everything was yet written to the underlying stream
. Since the stream
was already closed at that time, the ObjectDisposedException
was thrown.
But weren't the objects supposed to be disposed in the order reversed to the one they were created in? I.e. reader
should be disposed first, jsonWriter
second and stream
last. That's true, but disposing a StreamReader
has an unfortunate side effect: it also closes the underlying stream. Hence, stream
was closed before jsonWriter
was disposed and Flush
was called. That's why the exception was thrown.
Use using
declarations with care. The exact location in the code where an object is 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.
Big thanks to Daniel for posting his comment below and bringing up the inaccuracies in the first revision of this post.