I'm looking for best practices / standard on how to deal with this situation.
We have our code (MyClass) that consumes another class (ItemGenerator). ItemGenerator is a blackbox for us so we don't know the implementation (we do but we don't want to rely on that because it could change from underneath).
ItemGenerator has a method, GetItems(), that returns an IEnumerable of Item. Item class implements IDisposable so we should dispose of the object when we are done.
When we (MyClass) iterates through the list of items, if an exception (any exception) occurs, we want to stop processing and release control (bubble up the exception).
My question is this:
Should we keep iterating through the items in order to dispose of all of them? It might seem silly but what happens with the rest of the items if they are not disposed of?
At the same time, based on the code below, we should definitely not iterate through the rest of the items because they are yield return. So why generate them just so we can dispose of them (it could significantly affect the performance).
The problem is that we do not know if GetItems() returns the items on demand (yield) or not. And I don't think we should care, right?
So how should we handle the situation when an exception occurs in the middle of the list (for example)?
Below is an example of the code that illustrates the gist of it.
This is our code:
public class MyClass
{
public void VerifyAllItems()
{
ItemGenerator generator = new ItemGenerator();
foreach (Item item in generator.GetItems())
{
try
{
// Do some work with "item" here. Though an exception could occur.
// If an exception occurs, we don't care about processing the rest of the items and just want to bubble up the exception
}
finally
{
// Always dispose of the
item?.Dispose();
}
}
}
}
And this is the blackbox code
public class ItemGenerator
{
private long _itemsToGenerate = 0;
public ItemGenerator()
{
_itemsToGenerate = new Random().Next(10, 100);
}
public IEnumerable<Item> GetItems()
{
while (_itemsToGenerate > 0)
{
yield return HeavyWork();
_itemsToGenerate--;
}
}
private Item HeavyWork()
{
// Doing a lot of work here
return new Item();
}
}
public class Item : IDisposable
{
private bool _isDisposed = false;
public virtual void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
private void Dispose(bool isDisposing)
{
if (!_isDisposed)
{
if (isDisposing)
{
// Dispose of any resources
}
_isDisposed = true;
}
}
}
CodePudding user response:
The problem is worse than you state. Not only can you not be sure whether to enumerate the collection, you can't be sure whether to dispose of any of the items at all. Just because something implements IDisposable
doesn't mean you should be disposing it, e.g. if you have a factory that always returns the same instance of something.
This kind of a problem is exactly why the code that allocates something is generally held responsible for deallocating it. In this case, the ItemGenerator creates the items, so it should dispose them.
class ItemGenerator : IDiposable
{
protected readonly List<Item> _instances = new List<Item>();
IEnumerable<Item> GetItems()
{
for ( some; condition; here; )
{
var item = new Item();
_instances.Add(item);
yield return item;
}
}
public void Dispose()
{
foreach (var item in _instances) item.Dispose();
}
}
Now all you have to do is put your ItemGenerator in a using block and you're good to go.
public void VerifyAllItems()
{
using (ItemGenerator generator = new ItemGenerator())
{
foreach (Item item in generator.GetItems())
{
try
{
// Do some work with "item" here. Though an exception could occur.
// If an exception occurs, we don't care about processing the rest of the items and just want to bubble up the exception
}
finally
{
//Don't need to dispose anything here
}
}
} //Disposal happens here because of the using statement
}
With this patterns, any items that were allocated by the ItemGenerator will get disposed when you exit the using block.
Now the caller doesn't have to care about the implementation of the item generator at all, or worry about disposing anything other than the generator itself. And of course you should dispose the generator if you're the one who allocated it.
CodePudding user response:
Slightly bizarre, but...
internal class DisposingEnumerator : IEnumerator<Item>
{
private readonly List<IDisposable> deallocationQueue = new List<IDisposable>();
private readonly IEnumerable<Item> source;
private IEnumerator<Item> sourceEnumerator;
public DisposingEnumerator(IEnumerable<Item> source)
{
this.source = source;
}
public bool MoveNext()
{
if (sourceEnumerator == null)
{
sourceEnumerator = source.GetEnumerator();
}
bool hasNext = sourceEnumerator.MoveNext();
if (hasNext)
{
deallocationQueue.Add(Current);
}
return hasNext;
}
public Item Current => sourceEnumerator.Current;
object IEnumerator.Current => Current;
public void Reset()
{
throw new NotSupportedException();
}
// Will be called within "foreach" statement
// You can implement IDisposable in ItemCollection as well
public void Dispose()
{
foreach (var item in deallocationQueue)
{
item.Dispose();
}
}
}
public class ItemCollection : IEnumerable<Item>
{
private IEnumerator<Item> enumerator;
public ItemCollection(IEnumerable<Item> source)
{
this.enumerator = new DisposingEnumerator(source);
}
public IEnumerator<Item> GetEnumerator()
{
return enumerator;
}
IEnumerator IEnumerable.GetEnumerator()
{
return GetEnumerator();
}
}
Usage:
var items = generator.GetItems();
// Equivalent of using statement
foreach (var item in new ItemCollection(items))
{
}