Move Semantics for IDisposable Part 2
.NET .NET csharp
Published: 2024-11-11
Move Semantics for IDisposable Part 2

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:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
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:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
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:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
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:

1
2
3
4
class Movable<TResource> : IDisposable where TResource : class, IDisposable
{
	 // everything else as before
}