C# 8 using declarations
Visual Studio 2019 preview 2 was released a few days ago and I took the time to install it. Visual Studio itself is actually rather uninteresting to me, however the inclusion of the next C# 8 preview got my attention. I glanced at the feature highlights and posted “looks nice” on Twitter.
Predictably, I got a few responses like “I’m not sure I like that”, and there is always a guarantee that if F# has a similar feature, an F# developer will appear and tell you F# has had this feature for 600 years.
The one I like a lot is using declarations. This allows a local to automatically
be disposed at the end of the block. Essentially, it hides the try
/finally
or the using() {...}
. The .NET team’s blog kind of gave a bad example of this,
so I’ll use one from Open OPC SignTool. Here is the original snippet:
private static X509Certificate2 GetCertificateFromCertificateStore(string sha1)
{
using (var store = new X509Store(StoreName.My, StoreLocation.LocalMachine))
{
store.Open(OpenFlags.OpenExistingOnly | OpenFlags.ReadOnly);
var certificates = store.Certificates.Find(X509FindType.FindByThumbprint, sha1, false);
return certificates.Count > 0 ? certificates[0] : null;
}
}
A using var
can make this:
private static X509Certificate2 GetCertificateFromCertificateStore(string sha1)
{
using var store = new X509Store(StoreName.My, StoreLocation.LocalMachine);
store.Open(OpenFlags.OpenExistingOnly | OpenFlags.ReadOnly);
var certificates = store.Certificates.Find(X509FindType.FindByThumbprint, sha1, false);
return certificates.Count > 0 ? certificates[0] : null;
}
This has the same effect of store
having Dispose
called on it at the end of
the method. The benefit here being that there is less indentation and braces.
This keeps me focused on the code that matters. I don’t care when store
is
disposed in the method, I can just observe that it has a using
modifier on the
local and be assured that Dispose
will be called.
This isn’t the same as garbage collection or finalizers. Both of those are non-
deterministic, and can lead to unexpected program behavior. That’s less so in
the case of X509Store
, so let’s look at another example:
using Stream stream = entry.Open();
var xmlDocument = XDocument.Load(stream, LoadOptions.PreserveWhitespace);
return new OpcRelationships(location, xmlDocument, readOnlyMode);
Not disposing a stream that is backed by a file can cause access errors later in software that might try to open that file again - it is already open, so not only is it a bad idea it leave streams to the GC, it is just simply incorrect.
However again using
on the local ensures it is deterministically closed.
When it gets disposed I can see being slightly unclear to the developer. The quick explanation is when the local is no longer reachable, not when it is last used. The C# 8 above gets compiled roughly to:
var stream = entry.Open();
try
{
var xmlDocument = XDocument.Load(stream, LoadOptions.PreserveWhitespace);
return new OpcRelationships(location, xmlDocument, readOnlyMode);
}
finally
{
if (stream != null)
{
((IDisposable)stream).Dispose();
}
}
The disposal is done after the return, when the local is no longer reachable,
not after XDocument
is created.
I find this very helpful to keep code readable. This doesn’t work when you need
fine control over when Dispose
is called. A place where this does not work
well is when the Dispose
pattern is used for scopes, such as logging. The
AzureSignTool project has code similar to this in SignCommand
:
var logger = loggerFactory.CreateLogger<SignCommand>();
Parallel.ForEach(AllFiles, options, () => (succeeded: 0, failed: 0), (filePath, pls, state) =>
{
using (var loopScope = logger.BeginScope("File: {Id}", filePath))
{
logger.LogInformation("Signing file.");
//Sign the file. omit a bunch of other code.
logger.LogInformation("Done signing the file.");
}
logger.LogDebug("Incrementing success count.");
return (state.succeeded + 1, state.failed);
}
Here, we cannot change this to a using var
because then the LogDebug
would
be inside of that logging scope, which it wasn’t before. This is a place where
we continue to want Dispose
to be called at a different time from the when
loopScope
would no longer be in scope.
My impression from C# developers is that they do not tend to call Dispose
on resources as soon as it can be disposed, just at a reasonable point in the
same method. Most developers do not write this code:
public bool MightBeExe(string filePath)
{
var firstBytes = new byte[2];
int bytesRead;
using (var file = File.Open(filePath, FileMode.Open))
{
bytesRead = file.Read(firstBytes, 0, 2);
}
return bytesRead == 2 && firstBytes[0] == (byte)'M' && firstBytes[1] == (byte)'Z';
}
They will instead write something like:
public bool MightBeExe(string filePath)
{
using (var file = File.Open(filePath, FileMode.Open))
{
var firstBytes = new byte[2];
var bytesRead = file.Read(firstBytes, 0, 2);
return bytesRead == 2 && firstBytes[0] == (byte)'M' && firstBytes[1] == (byte)'Z';
}
}
Which is a perfect candidate for using var
:
public bool MightBeExe(string filePath)
{
using var file = File.Open(filePath, FileMode.Open);
var firstBytes = new byte[2];
var bytesRead = file.Read(firstBytes, 0, 2);
return bytesRead == 2 && firstBytes[0] == (byte)'M' && firstBytes[1] == (byte)'Z';
}
There are of course some reasonable limitations to this feature. For example, it cannot be combined with out-variables.
if (Crypto32.CryptEncodeObjectEx(
// other stuff
out var handle,
ref size)
)
{
using (handle)
{
// Do stuff
}
}
This does not work:
if (Crypto32.CryptEncodeObjectEx(
// other stuff
out using var handle,
ref size)
)
{
// Do stuff
}
Jared Parsons said on Twitter that C# folks thought of this, and decided that it had “Too much confusion about ownership.” Thinking about it myself, I agree, so it’s nice that the feature is limited in that regard.
Another limitation is that the variable cannot be reassigned. For example:
using var stream = entry.Open();
stream = entry2.Open();
This will produce error CS1656, “Cannot assign to ‘stream’ because it is a ‘using variable’”.
All in all, I very much like this small feature in C# 8. It has reasonable guard rails on it from doing something too weird like re-assigning to it, while giving the benefit of less blocks, braces, indentation.