docs/features/enhanced-using.md
Enhanced using consists of several related features that aim to make the disposable coding pattern easier to participate in for implementers, and easier to consume for end-users.
Using declarations aim to make consuming a disposable type simpler by allowing using to be added to a local declaration.
Pattern-based asynchronous disposal allows a type to opt into asynchronous disposal without needing to implement the IAsyncDisposable interface.
Pattern-based disposal for ref structs allows ref structs to opt into disposal without needing to implement the IDisposable interface.
See: the corresponding proposal in CSharpLang.
Using declarations allow a user to specify a using keyword as part of a local declaration statement:
using var x = ...
// other statements
This is equivalent to declaring the local inside of a using statement at the same location:
using (var x = ...)
{
// other statements
}
It is not valid to declare a using declaration without an initializer expression:
using IDisposable x; //error CS0210: You must provide an initializer in a fixed or using statement declaration
The initializer expression must result in a type that is considered to be Disposable. That is, the expression must also be valid when used directly inside a using statement:
using var x = <expression>
using (<expression>) { } // expression must also be valid in a standard using statement
The lifetime of the local extends to the scope in which it is declared; immediately prior to the variable going out of scope, it will be disposed.
if (...)
{
using var x = ...;
// other statements
// Dispose x
}
Using declarations in the same scope are disposed in the reverse order to which they are declared
{
using var x = ...;
using var y = ..., z = ...;
// Dispose z
// Dispose y
// Dispose x
}
As with a local declared as part of a using statement, a using local is readonly and may not be re-assigned after declaration.
using var x = ...;
x = ...; // error CS1656: Cannot assign to 'x' because it is a 'using variable'
A using local may be used in the right hand side of an assignment or declaration. This means it is possible to capture a reference that will exist for longer than the lifetime of the using local. The reference will still be disposed when the using local is going out of scope. Interacting with the reference after disposal is undefined, but in most cases it is expected that it would result in an ObjectDisposedException being thrown.
IDisposable y = null;
if (...)
{
using var x = ...;
y = x;
// Dispose x
}
y.Dispose(); // undefined. ObjectDisposedException in most cases
It is possible to use an existing reference as the initializer of a using local declaration. As above, the reference will be disposed when the declared local is going out of scope, but the existing reference will still be available for use via its previous declaration.
IDisposable y = ...;
if (...)
{
using var x = y;
// Dispose x, which points to the same object as y
}
y.Dispose(); // undefined. ObjectDisposedException in most cases
When inside a method marked async, a user may optionally specify an additional await keyword prior to the using keyword, to indicate asynchronous disposal:
await using var x = ...
Which is equivalent to
await using (var x = ...) { }
The initializer expression must result in a type that is considered to be asynchronously disposable. That is, the expression must also be valid when used directly in an await using statement:
await using var x = <expression>
await using (<expression>) { } // expression must also be valid in a standard await using statement
Attempting to specify the await keyword on a using declaration in a method not marked with the async keyword results in a compiler error:
public void M()
{
await using var x = ...; //error CS4033: The 'await' operator can only be used within an async method. Consider marking this method with the 'async' modifier and changing its return type to 'Task'.
}
In general, control flow is unaffected by the presence of a using declaration. However, there are some restrictions around the use of the goto statement when the statement and its target label lie either side of a using declaration.
It is forbidden to jump 'forward' to a location after a using declaration, when the goto or the target label are in the same or lower scope as the declaration.
goto label1; // error CS8648: A goto cannot jump to a location after a using declaration.
using var x = ...;
label1:
return;
goto label1; // ok. using declaration is in a lower scope than goto and label
{
using var x = ...;
}
label1:
return;
{
{
goto label1; // error CS8648: A goto cannot jump to a location after a using declaration.
}
using var x = ...;
}
label1:
return;
It is forbidden to jump 'backwards' to a location before a using declaration, when the label is in the same scope as the using declaration.
label1:
using var x = ...; //error CS8649: A goto cannot jump to a location before a using declaration within the same block.
goto label1;
Jumping 'backwards' to a label at a higher scope is specifically permitted.
label1:
{
using var x = ...;
goto label1; // allowed. label1 is in a higher scope than the using declaration
}
When jumping to a higher scope, any declared using variables will be disposed at the site of the goto. Variables not yet declared will not be disposed.
label1:
{
using var x = ...;
goto label1; // dispose x
using var y = ...;
}
It is always permissible to jump to the same scope, when the goto and target label do not lie either side of a using declaration.
goto label1; // ok, not jumping over a using declaration
label1:
using var x = ...;
label2:
goto label2; // ok, not jumping over a using declaration
A using declaration may not appear directly inside of a case label. It may instead be used within a block inside of a case label:
switch (...)
{
case ...:
using var x = ...; // error CS8647: A using variable cannot be used directly within a switch section (consider using braces).
break;
case ...:
{
using var y = ...; // ok
// Dispose y
}
break;
}
A using declaration may not appear directly as part of an out variable declaration. It is easy to emulate this behavior however, by adding a using declaration immediately after the out variable:
if(TryGetDisposable(out var x))
{
using var y = x;
// Dispose y, and thus x
}
With pattern-based asynchronous disposal a type can be used in an await using statement without needing to explicitly implement IAsyncDisposable if it meets certain structural requirements. Specifically:
"A reachable, non-generic Task-like returning instance method called DisposeAsync, that can be called with zero explicit arguments"
Where reachable means legal to call from the site of the await using(...) under normal accessibility rules.
public class C
{
public static async Task M()
{
await using (AsyncDisposer a = new AsyncDisposer())
{
}
}
}
public class AsyncDisposer
{
public async ValueTask DisposeAsync() => Console.WriteLine("DisposeAsync");
}
In the situation where a type can both be implicitly converted to IAsyncDisposable and also fits the asynchronous disposal pattern, IAsyncDisposable is chosen.
Pattern-based asynchronous disposal methods may contain optional or params parameters and still meet the requirements of the pattern. In general if you could write c.DisposeAsync() at the site of the await using syntax and have it be a valid call under normal language rules, then the type is considered to be asynchronously disposable.
public class AsyncDisposer
{
public async ValueTask DisposeAsync(int x = 0, params object[] args) => Console.WriteLine("DisposeAsync"); // valid pattern candidate
}
Extension methods may not be used to implement asynchronous disposal. The method must be a reachable instance method in order to be considered as a valid candidate for the pattern.
Note: This is currently not working as spec'd and is tracked by #34701
For nullable value types in a using statement, the behavior today is to call Dispose on the underlying type if and only if the type has a value (i.e. if(t.HasValue){ t.GetValueOrDefault().Dispose() } ). This allows lifted types to be used as if they were the underlying type, and dispose is only called in the case they are not null. This behavior is extended to pattern-based DisposeAsync methods in await using statements.
public class C
{
public static async Task M()
{
StructDisposer? a = null;
await using (a) { } // DisposeAsync is not invoked
StructDisposer? b = new StructDisposer();
await using (b) { } // DisposeAsync is invoked
}
}
public struct StructDisposer
{
public async ValueTask DisposeAsync() => Console.WriteLine("DisposeAsync");
}
Today ref structs can not participate in the IDisposable pattern as they can not implement an interface. This feature allows a ref struct to be considered disposable if it meets certain structural requirements. Specifically:
"A reachable, void returning instance method called Dispose, that can be called with zero explicit arguments"
Where reachable means legal to call from the site of the using(...) under normal accessibility rules.
using System;
public class C
{
public void M()
{
using var x = new Disposer();
}
}
ref struct Disposer
{
internal void Dispose() => Console.WriteLine("Disposed");
}
Note: ref structs can not be used in an async method and therefore can not participate in asynchronous disposal via await using
Pattern-based disposal methods may contain optional or params parameters and still meet the requirements of the pattern. In general if you could write s.Dispose() at the site of the using syntax and have it be a valid call under normal language rules, then the ref struct is considered to be disposable.
public ref struct Disposer
{
public void Dispose(int x = 0, params object[] args) => Console.WriteLine("Disposed"); // valid pattern candidate
}
Extension methods may not be used to implement disposal. The method must be a reachable instance method in order to be considered as a valid candidate for the pattern.