decouple IDisposable and using operator, define clear semantic #3025
-
The most popular unofficial use case is implicit lifetime management. For example The aim of this proposal is to segregate from IDisposable such "unofficial use cases" to corresponding abstractions. Firstly we needs to allow reuse using operator for arbitrary purposes // using operator simplified microcode, along with existing for IDisposable
{ // using (ICodeScopeExtension obj = ....)
try
{
// scope code
}
catch (Exception ex)
{
obj?.OnLoseCodeScope(ex);
throw;
}
obj?.OnLoseCodeScope(null);
}
/// Contracts that using operator should start to support
/// <summary>
/// Scoped language construct extension, primarily <see langword="using" /> operator.
/// </summary>
public interface ICodeScopeExtension // using operator should be extended to support also this interface.
{
/// <summary>
/// Executed by a language construct when the language construct loses its scope.
/// </summary>
/// <param name="exception">
/// Exception that was caused the scope loosing, or <see langword="null" /> for a normal execution
/// flow.
/// </param>
void OnLoseCodeScope(Exception? exception);
}
/// <summary>
/// Scoped language construct asynchronous extension, primarily <see langword="using" /> statement.
/// </summary>
public interface IAsyncCodeScopeExtension // await using statement
{
/// <summary>
/// Asynchronously executed by a language construct when the language construct loses its scope.
/// </summary>
/// <param name="exception">
/// Exception that was caused the scope loosing, or <see langword="null" /> for a normal execution
/// flow.
/// </param>
/// <returns>Async operation task.</returns>
ValueTask OnLoseCodeScopeAsync(Exception? exception);
} Now we assume that Example: Explicit lifetime management abstraction /// ILifetimeExplicit proposed to be added to .Net Standard, as it very very common usage scenario. But it could be just in a popular nuget library (like Newtonsof.Json).
/// <summary>
/// The object with the explicitly controlled lifetime.
/// </summary>
public interface ILifetimeExplicit: ICodeScopeExtension
{
/// <summary>
/// Gracefully finalizes lifetime of the object. Supports one and only one invocation.
/// </summary>
void Dispose();
// C# 8.0 default interface methods behavior override, already supported.
void ICodeScopeExtension.OnLoseCodeScope(Exception? ex)
{
Dispose();
}
}
/// <summary>
/// The object with the explicitly controlled lifetime, suitable for asynchronous execution.
/// </summary>
public interface IAsyncLifetimeExplicit: IAsyncCodeScopeExtension
{
/// <summary>
/// Gracefully finalizes lifetime of the object. Supports one and only one invocation.
/// </summary>
ValueTask DisposeAsync();
// C# 8.0 default interface methods behavior override, already supported.
ValueTask IAsyncCodeScopeExtension.OnLoseCodeScopeAsync(Exception? exception)
{
return DisposeAsync();
}
}
/// <summary>
/// The object with explicit lifetime management which also distinguish lifetime termination.
/// </summary>
public interface ILifetimeTerminable : ILifetimeExplicit
{
/// <summary>
/// Terminates lifetime. Supports one and only one invocation mutually exclusive with Dispose.
/// </summary>
/// <param name="reasonException">The exception caused lifetime termination.</param>
void Terminate(Exception reasonException);
// C# 8.0 default interface methods behavior override, already supported.
void ICodeScopeExtension.OnLoseCodeScope(Exception? exception)
{
if (exception == null)
Dispose();
else
Terminate(exception);
}
}
/// <summary>
/// The object with explicit lifetime management which also distinguish lifetime termination and suitable for asynchronous execution.
/// </summary>
public interface IAsyncLifetimeTerminable : IAsyncLifetimeExplicit
{
/// <summary>
/// Terminates lifetime. Supports one and only one invocation mutually exclusive with Dispose.
/// </summary>
/// <param name="reasonException">The exception caused lifetime termination.</param>
ValueTask TerminateAsync(Exception reasonException);
// C# 8.0 default interface methods behavior override, already supported.
ValueTask IAsyncCodeScopeExtension.OnLoseCodeScopeAsync(Exception? exception)
{
return exception == null ? DisposeAsync() : TerminateAsync(exception);
}
} Explicit lifetime management abstraction usage demo public class XmlFileEditContext : ILifetimeTerminable
{
private readonly string _xmlFileName;
public XmlDocument Xml { get; }
public XmlFileEditContext(string xmlFileName)
{
_xmlFileName = xmlFileName;
Xml = new XmlDocument();
Xml.Load(xmlFileName);
}
public void Terminate(Exception reasonException)
{
// Some error occurred.
// We needs only write trace message
Trace.WriteLine($"Xml editing failed, error = {reasonException.Message}");
}
public void Dispose()
{
// This method is not about Unmanaged resources anymore.
// IExplicitLifetime.Dispose especially designed to perform any actions at the end of lifetime.
Xml.Save(_xmlFileName);
}
}
public class Program
{
public static void Main()
{
using (var ctx = new XmlFileEditContext("my.xml"))
{
ctx.Xml.DocumentElement.InnerText = "Successful op";
ctx.Xml.DocumentElement["UnknownNode"].InnerText = ""; // Raises exception, which passed to ctx.Terminate method implicitly by using operator.
}
}
} Should
|
Beta Was this translation helpful? Give feedback.
Replies: 20 comments 3 replies
-
You can play with the proposed abstractions and using statement Polyfill by downloading package: https://www.nuget.org/packages/ClrPro.DotNetLanguages.Proposals |
Beta Was this translation helpful? Give feedback.
-
This was already discussed going into C# 8.0 and the team rejected it because it would silently change the behavior of the compiler when iterating via I think this proposal also has to work out the issues as to what happens when a type implements more than one of these interfaces and/or |
Beta Was this translation helpful? Give feedback.
-
@HaloFour, thank you for your considerations and references. I have improved this proposal. Pattern-matching is no more mandatory thing for this proposal (but still possible). Problems that you specified has been worked out.
Even if we stay IDisposable/using as is, documentation for it should be significantly improved and give support/best practices for more use cases. Number of years saying nothing. 19 Years C# Language was without implicit scope using: using var test = new MyDisposable();
var str = "asdfasdf";
.... 16 Years BCL was without Task-like async methods, 19 years without DIM, etc. Currently IDisposable/using solution lacks the ability to monitor scope lose exception, it's most limiting thing which is require BCL/language change. The current version of proposal is very cheap in terms of implementation efforts by LDM Team, very close to implicit scope using. Current proposal can help to write far more clear code bases and get rid from IDisposable semantic mess. Which every time eats your brain if you use IDisposable/using for something other than unmanaged resource disposal. |
Beta Was this translation helpful? Give feedback.
-
I think that modifying the documentation would be a lot easier than trying to change the patterns people use. I agree that it's probably worth expanding on the definition of "disposable". There have been a couple conversations about this already and seemingly the C# team themselves often can't agree on when and how to use My point about the number of years isn't about how long we've lived without a particular feature, it's about how much of an existing ecosystem there is. Lots of projects depend on Note that I'm only arguing against adding an interface that duplicates |
Beta Was this translation helpful? Give feedback.
-
"lose" not "loose" |
Beta Was this translation helpful? Give feedback.
-
This feature didn't seem to substantively do more than what is already possible today with IDisposable and |
Beta Was this translation helpful? Give feedback.
-
I'm pretty sure the best possible outcome would be to update the documentation for Adding a new interface is effectively a non-starter because adoption would be low - most libraries wouldn't use it because of backward compatibility issues, most developers wouldn't use it because they wouldn't know about it, so it would be nothing more than a curiosity. Extending the existing Since C#8, the using keyword has been pattern based, so a type only needs to implement Having the compiler suddenly start automatically calling |
Beta Was this translation helpful? Give feedback.
-
@theunrepentantgeek are you sure about pattern-based using? I thought that was rejected except for |
Beta Was this translation helpful? Give feedback.
-
Interesting. My playing around with SharpLab only got as far as getting this error message:
which I thought proved it had been released. However, switching the branch to
So something has definitely changed. If pattern based disposal is truly dead, then we might be able to extend |
Beta Was this translation helpful? Give feedback.
-
Pattern-based is only used for ref structs. It was too much of a breaking change for us to consider for other types. |
Beta Was this translation helpful? Give feedback.
-
What other scenarios would "break" other than |
Beta Was this translation helpful? Give feedback.
-
The design for these interfaces is based on a motivating scenario. Now that these types and the corresponding language features are defined with specific semantics, there is nothing stopping a user from writing code that needs the same semantics. In other words, unmanaged resources are one example use for these interfaces, but any number of other valid examples can exist.
This is already a valid use of |
Beta Was this translation helpful? Give feedback.
-
I believe that's why many are saying that the documentation around IDisposable should be updated to make clear that usages outside of releasing unmanaged resources are acceptable and not a misuse of the interface. As things stand now, the following line in the IDisposable documentation causes purists to believe that the only usage of IDisposable should be for releasing unmanaged resources when in practice this is simply not the case.
|
Beta Was this translation helpful? Give feedback.
-
@ajdepersio Its actually this line here:
|
Beta Was this translation helpful? Give feedback.
-
@HaloFour those scenarios were enough. |
Beta Was this translation helpful? Give feedback.
-
P.S. |
Beta Was this translation helpful? Give feedback.
-
Seems like a scenario solved easily enough by requiring that pattern-based enumerators implement The ask is for pattern-based |
Beta Was this translation helpful? Give feedback.
-
I'd like to bump this discussion. It's often better to do: void /*async Task*/ Foo()
{
using var transactionScope = BeginScope();
// do stuff
} Than: void /*async Task*/ Foo()
{
using var transactionScope = BeginScope();
// do stuff
transactionScope.Commit();
} Because the method may have more than All we need here is: try
{
// main logic
}
finally(Exception? ex)
{
// we know whether an exception is bubbling
} Then: interface IExceptionDisposable { void Dispose(Exception? ex); }
interface IAsyncExceptionDisposable { ValueTask DisposeAsync(Exception? ex); } |
Beta Was this translation helpful? Give feedback.
-
I would like to make a vote that this feature goes into C# 13 |
Beta Was this translation helpful? Give feedback.
@dmitriyse
I think that modifying the documentation would be a lot easier than trying to change the patterns people use. I agree that it's probably worth expanding on the definition of "disposable". There have been a couple conversations about this already and seemingly the C# team themselves often can't agree on when and how to use
IDisposable
: #93 (comment)My point about the number of years isn't about how long we've lived without a particular feature, it's about how much of an existing ecosystem there is. Lots of projects depend on
IDispo…