Quantcast
Channel: Damir's Corner
Viewing all articles
Browse latest Browse all 485

Be Careful with Using Declarations in C# 8

$
0
0

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.


Viewing all articles
Browse latest Browse all 485

Trending Articles