After trying out the Movable<TResource> type from Move Semantics for IDisposable
I discovered a fatal flaw in its implementation: it is incompatible with struct
memberwise copy semantics.
Recall that Movable<TResource> was defined as follows:
struct Movable<TResource> : IDisposable where TResource : class, IDisposable
{
private TResource resource;
public Movable(TResource resource)
{
this.resource = resource ?? throw new ArgumentNullException(nameof(resource));
}
public readonly TResource Value => this.resource ?? throw new InvalidOperationException();
public TResource Move()
{
TResource result = this.resource ?? throw new InvalidOperationException();
this.resource = null;
return result;
}
public void Dispose()
{
if (this.resource != null)
{
this.resource.Dispose();
this.resource = null;
}
}
}
Now consider the following code:
Stream CreateStream()
{
Stream stream = ...;
using Movable<Stream> s1 = new Movable<Stream>(stream);
using Movable<Stream> s2 = wrapWithDecorator switch {
true => new Movable<Stream>(new StreamDecorator(s1.Move())),
false => s1,
};
return s2.Move();
}
With this code, if wrapWithDecorator is false, both s1 and s2
have non-null references to stream. At the end of the CreateStream()
function, s1 will call stream.Dispose(), and thus the function ends
up returning a disposed stream, which is invalid.
One way to fix this is to never copy Movables and write code like this:
Stream CreateStream()
{
Stream stream = ...;
using Movable<Stream> s1 = new Movable<Stream>(stream);
using Movable<Stream> s2 = wrapWithDecorator switch {
true => new Movable<Stream>(new StreamDecorator(s1.Move())),
false => new Movable<Stream>(s1.Move()),
};
return s2.Move();
}
However, this is extremely error-prone. I initially looked for ways
to define a non-copyable struct, but I was reminded of
Eric Lippert’s words:
The relevant feature of value types is that they have the semantics of being copied by value, not that sometimes their deallocation can be optimized by the runtime.
Therefore, I changed Movable<TResource> to use reference copy semantics
by changing it to a class:
class Movable<TResource> : IDisposable where TResource : class, IDisposable
{
// everything else as before
}