Using-declarations in C# 8.0
One of new language features of C# 8.0 is support for using declarations. These declarations enable shorter syntax for declaring disposable variables we want to dispose. Also using declarations give us a little bit cleaner code while compiler makes a dirty work of producing correct code that takes care of disposing disposable variables.
- C# 8: Default implementations in interfaces
- Using-declarations in C# 8.0
Using declarations in action
Let’s take simple piece of code and see how we can make it use using declarations. Here is one classic block of code where stream and stream reader are used. They both are disposable.
static void MainOld(string[] args) { using (var file = new FileStream("input.txt", FileMode.Open)) using (var reader = new StreamReader(file)) { var s = reader.ReadToEnd(); // Do something with data } }
Let’s make this code now to using declarations. For this I just make another method to compare them later with disassembler.
static void MainNew(string[] args) { using Stream file = new FileStream("input.txt", FileMode.Open); using StreamReader reader = new StreamReader(file); var s = reader.ReadToEnd(); // Do something with data }
We ended up with similar code but it’s a little bit shorter. Where it may pay off are methods with at least three or five using blocks.
Using declarations after compiling
Experienced developers want to ask one question now: what are using declarations internally and how they are handled by compiler? To see what’s going on let’s build the code and open resulting assembly in disassembler. This is what compiler built for us.
private static void MainNew(string[] args) { using (Stream stream = (Stream)new FileStream("input.txt", FileMode.Open)) { using (StreamReader streamReader = new StreamReader(stream)) streamReader.ReadToEnd(); } } private static void MainOld(string[] args) { using (FileStream fileStream = new FileStream("input.txt", FileMode.Open)) { using (StreamReader streamReader = new StreamReader((Stream)fileStream)) streamReader.ReadToEnd(); } }
As a result we got two identical methods. They differ of implementation a bit but what they do is exactly the same.
Scope of using declarations today
Another question that experienced developers ask is how long is automatically generated using block? There are situations where we want using block to be as short as possible to lessen memory or other system resource footprint of application.
Let’s try out with the following piece of code.
static void MainNew(string[] args) { string buffer = null; using Stream file = new FileStream("input.txt", FileMode.Open); using StreamReader reader = new StreamReader(file); buffer = reader.ReadToEnd(); // Do something with data buffer = null; } static void MainOld(string[] args) { string buffer = null; using (var file = new FileStream("input.txt", FileMode.Open)) using (var reader = new StreamReader(file)) { buffer = reader.ReadToEnd(); }
// Do something with data
buffer = null; }
MainOld() method keeps disposables up only until the contents of file are read. After this file stream and stream reader are disposed immediately. For MainNew() method we don’t know what happens and it’s not possible for us to read it out from code. Let’s build again and use disassembler again to see what compiler built.
private static void MainNew(string[] args) { string str = (string)null; using (Stream stream = (Stream)new FileStream("input.txt", FileMode.Open)) { using (StreamReader streamReader = new StreamReader(stream)) { str = streamReader.ReadToEnd(); str = (string)null; } } } private static void MainOld(string[] args) { string str = (string)null; using (FileStream fileStream = new FileStream("input.txt", FileMode.Open)) { using (StreamReader streamReader = new StreamReader((Stream)fileStream)) str = streamReader.ReadToEnd(); } str = (string)null; }
We got again similar methods but they are not same. Take a look at where using blocks end in both methods. In MainOld() method disposables are kept alive for sorter time than in MainNew() method.
Warning! C# 8.0 is not stable yet and things may change. Until there is no stable version yet we cannot say if this difference will remain or if it can be controlled somehow. We can be sure that the difference exists today and when writing C# 8 code today we have to consider length of scope of using declarations blocks.
Wrapping up
Using declarations help us write a little bit shorter code where details of using blocks are hidden from us. In current implementation using block seems to cover all code from point where disposable variable was declared using using declaration. If this holds true for first stable release and from there on – I don’t know. We can all be part of the story and tell what we think at official C# language discussions. Personally I prefer shortest scopes possible.
Not a new post but I’ll give it a try to get an answer I wonder about.
This is from your example:
“`
void Test1()
{
using Stream file = new FileStream(“input.txt”, FileMode.Open);
using StreamReader reader = new StreamReader(file);
var s = reader.ReadToEnd();
// Do something with data
}
“`
What if I inline one ‘using’, does that dispose during the outer one?
“`
void Test2()
{
using StreamReader reader = new StreamReader(new FileStream(“input.txt”, FileMode.Open));
var s = reader.ReadToEnd();
// Do something with data
}
“`
What if I also inline the last one, does no disposing occur at all?
“`
void Test3()
{
var s = new StreamReader(new FileStream(“input.txt”, FileMode.Open)).ReadToEnd();
// Do something with data
}
“`