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

See also Move Semantics for IDisposable Part 2

C++ 11 introduced the concept of move semantics to model transfer of ownership. Rust includes transfer of ownership as a key component of its type system. C# could benefit from something similar for IDisposable types. This blog post explores some options on how to handle this.

Consider the following code:

 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
28
class NativeFileHandle : IDisposable
{
	public static NativeFileHandle Open(string path, /*additional args*/)
	{
		// Opens up the file and returns a class which wraps the operating
		// system file handle
	}

	// ...
}

class NativeFileStream : Stream
{
	public NativeFileStream(NativeFileHandle fh)
	{
		// Take ownerhsip of fh
	}

	// ...
}

NativeFileStream OpenFile(string fileName)
{
	NativeFileHandle fileHandle = NativeFileHandle.Open(fileName, ...);
	ConfigureFileHandle(fileHandle); // Uses fileHandle but does not take ownership
	NativeFileStream fileStream = new NativeFileStream(fileHandle);
	return fileStream;
}

Now consider what happens if ConfigureFileHandle() throws an exception. fileHandle.Dispose() will not be called, which means that the operating system file handle will not be closed until the class is garbage collected.

Let’s try to fix this with using():

1
2
3
4
5
6
7
NativeFileStream OpenFile(string fileName)
{
	using NativeFileHandle fileHandle = NativeFileHandle.Open(fileName, ...);
	ConfigureFileHandle(fileHandle); // Uses fileHandle but does not take ownership
	NativeFileStream fileStream = new NativeFileStream(fileHandle);
	return fileStream;
}

This fix has a fatal flaw: fileHandle is disposed in the success case (i.e. no exceptions were thrown). We need to model the transfer of ownership of fileHandle to the NativeFileStream class.

The CA2000 warning from .NET 8 suggests we should use the following pattern instead:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
NativeFileStream OpenFile(string fileName)
{
	NativeFileHandle? fileHandle = NativeFileHandle.Open(fileName, ...);
	try
	{
		ConfigureFileHandle(fileHandle); // Uses fileHandle but does not take ownership
		NativeFileStream fileStream = new NativeFileStream(fileHandle);
		fileHandle = null; // Ownership of fileHandle has been passed to NativeFileStream
		return fileStream;
	}
	finally
	{
		fileHandle?.Dispose(); // Only dispose if ownership was _not_ transferred
	}
}

This works fine, but it gets ugly very quickly if there are multiple consecutive transfers of ownership:

 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
28
29
30
31
32
Resource4 BuildResource()
{
	Resource1? resource1 = new Resource1();
	try
	{
		Resource2? resource2 = new Resource2(resource1); // Take ownership of resource1
		resource1 = null;
		try
		{
			Resource3? resource3 = new Resource3(resource2); // Take ownership of resource2
			resource2 = null;
			try
			{
				Resource4 resource4 = new Resource4(resource3); // Take ownership of resource3
				resource3 = null;
				return resource4;
			}
			finally
			{
				resource3?.Dispose();
			}
		}
		finally
		{
			resource2?.Dispose();
		}
	}
	finally
	{
		resource1?.Dispose();
	}
}

Eliminating ugly code is precisely what the using keyword was designed to solve; we’ve lost something along the way.

The .NET runtime proposal New API of “move semantics” for IDisposable suggests solving this problem by creating a Moveable<T> type:

 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;
		}
	}
}

Let’s apply Movable<T> to our original example:

1
2
3
4
5
6
7
NativeFileStream OpenFile(string fileName)
{
	using var fileHandle = new Movable<NativeFileHandle>(NativeFileHandle.Open(fileName, ...));
	ConfigureFileHandle(fileHandle.Value); // Uses fileHandle but does not take ownership
	NativeFileStream fileStream = new NativeFileStream(fileHandle.Move());
	return fileStream;
}

Not bad! How about the other example?

1
2
3
4
5
6
7
Resource4 BuildResource()
{
	using var resource1 = new Movable<Resource1>(new Resource1());
	using var resource2 = new Movable<Resource2>(new Resource2(resource1.Move()));
	using var resource3 = new Movable<Resource3>(new Resource3(resource2.Move()));
	return new Resource4(resource3.Move());
}

Much, much better!

However, in the same proposal, Stephen Toub notes that Movable<T> doesn’t help with the following pattern, which only creates a resource sometimes, and needs to perform exception remapping:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
Socket socket = null;
try
{
    if (something)
    {
        ...
        return ...
    }
    else
    {
        socket = new Socket(...);
        ...
        return new NetworkStream(socket);
    }
}
catch (Exception e)
{
    socket?.Dispose();
    throw TranslateException(e);
}

With Movable<T>, the above code becomes:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
using Movable<Socket> m = default;
try
{
    if (something)
    {
        ...
        return ...
    }
    else
    {
        m.Assign(new Socket(...)); // m takes ownership of Socket
        ...
        return new NetworkStream(m.Move());
    }
}
catch (Exception e)
{
    throw TranslateException(e);
}

Which arguably isn’t any better. Furthermore, Toub argues that it is easy to mistakenly use .Value when you should have used .Move(), and that “there’s nothing here that really helps with ownership and transferring/borrowing of ownership, which would really need to be achieved at a language level.”

Toub’s objections are valid, of course, but I’m going to adopt Movable<T> in my code until something better comes along.