I was inspecting the Jon Skeet's MoreLinq and I became curious about the acquire extension source code
The implementation is as follows
/// <summary>
/// Ensures that a source sequence of <see cref="IDisposable"/>
/// objects are all acquired successfully. If the acquisition of any
/// one <see cref="IDisposable"/> fails then those successfully
/// acquired till that point are disposed.
/// </summary>
/// <typeparam name="TSource">Type of elements in <paramref name="source"/> sequence.</typeparam>
/// <param name="source">Source sequence of <see cref="IDisposable"/> objects.</param>
/// <returns>
/// Returns an array of all the acquired <see cref="IDisposable"/>
/// object and in source order.
/// </returns>
/// <remarks>
/// This operator executes immediately.
/// </remarks>
public static TSource[] Acquire<TSource>(this IEnumerable<TSource> source)
where TSource : IDisposable
{
if (source == null) throw new ArgumentNullException("source");
var disposables = new List<TSource>();
try
{
disposables.AddRange(source);
return disposables.ToArray();
}
catch
{
foreach (var disposable in disposables)
disposable.Dispose();
throw;
}
}
From my understanding it receives a IEnumerable<IDisposable>
and it creates a List<IDisposable>
.
I can't grasp whatever can go wrong here.
Can anyone explain it to me, and possibly provide an example where this extension can be useful?
The call to AddRange
iterates over source
. If for any reason it encounters an exception, any that had previously been acquired will be disposed. Consider this example:
var filenames = new[] { "file1.xml", "file2.xml", "doesnotexist.xml" };
var disposables = filenames.Select(fn => File.OpenRead(fn));
var fileStreams = disposables.Acquire();
No exception will be thrown when you're assigning disposables
, because of lazy evaluation. However, when the call to AddRange
inside Aquire
reaches the third element (where it tries to open "doesnotexist.xml"
), a FileNotFoundException
will be thrown. When this happens, Acquire
will safely dispose the previous streams. A simple ToList
/ ToArray
would leave the first two file streams open.
In essence, Acquire
is there to ensure that either all the files in filenames
is safely opened, or none of them are.
Assume you have code which creates and returns disposable objects one by one:
public IEnumerable<FileStream> GetFiles()
{
yield return File.OpenRead("file1");
yield return File.OpenRead("file2"); // does not exist
yield return File.OpenRead("file3");
}
You need to get all of the disposable objects, but if in the middle of acquisition there is an exception, then the objects which were already yielded will stay in memory and not disposed. So, Acquire
either acquires all streams and returns them, or, upon failing, it disposes all already acquired streams and rethrows the exception.
FileStream[] streams = GetFiles().Acquire();
Remember that most of the IEnumerable
collections you get using LINQ are evaluated in lazy way, e.g. you get just the recipe how to generate the list. The code gets actually executed only when you iterate over the collection, which in this case happens in disposables.AddRange(source)
. If this call fails, then you end up with a partial collection of objects that should be disposed, which happens here:
foreach (var disposable in disposables)
disposable.Dispose();
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With