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.
classNativeFileHandle : IDisposable
{
publicstatic NativeFileHandle Open(string path, /*additional args*/)
{
// Opens up the file and returns a class which wraps the operating// system file handle }
// ...}
classNativeFileStream : 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)
{
usingNativeFileHandle 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.
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 NativeFileStreamreturn 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:
NativeFileStream OpenFile(string fileName)
{
usingvar 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()
{
usingvar resource1 = new Movable<Resource1>(new Resource1());
usingvar resource2 = new Movable<Resource2>(new Resource2(resource1.Move()));
usingvar resource3 = new Movable<Resource3>(new Resource3(resource2.Move()));
returnnew 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:
usingMovable<Socket> m = default;
try{
if (something)
{
...
return ...
}
else {
m.Assign(new Socket(...)); // m takes ownership of Socket ...
returnnew 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.