Оператор `using` для фабрики высвобождает ресурс, делегируемый фабрикой в экземпляры
Это минимально воспроизводимый пример:
namespace IterableDisposeTest
{
interface IInstance
{
int ID { get; }
}
class Instance : IInstance
{
private static int p_counter = 0;
public int ID { get; }
public Instance(MagicVariable _magicVariable)
{
ID = p_counter++;
Console.WriteLine(_magicVariable.MagicString);
}
}
interface IProducer
{
public IEnumerable<IInstance> GetNextInstance();
}
class Producer : IProducer
{
private MagicVariable p_magicVariable;
public Producer(MagicVariable _magicVariable)
{
p_magicVariable = _magicVariable;
}
public IEnumerable<IInstance> GetNextInstance()
{
for (int i = 0; i < 42; i++)
yield return new Instance(p_magicVariable);
}
}
class ProducerFactory: IDisposable
{
private bool disposedValue;
private MagicVariable p_magicVariable = new MagicVariable();
public IEnumerable<Producer> GetNextProducer()
{
while (true)
yield return new Producer(p_magicVariable);
}
protected virtual void Dispose(bool disposing)
{
if (!disposedValue)
{
Console.WriteLine("ProducerFactory Disposed");
if (disposing)
{
}
p_magicVariable.Dispose();
disposedValue = true;
}
}
~ProducerFactory()
{
Dispose(disposing: false);
}
public void Dispose()
{
Dispose(disposing: true);
GC.SuppressFinalize(this);
}
}
class MagicVariable: IDisposable
{
private string p_magicString = "I'm MAGIC!";
private bool p_disposedValue;
public string MagicString
{
get
{
ObjectDisposedException.ThrowIf(p_disposedValue, typeof(string));
return p_magicString;
}
}
protected virtual void Dispose(bool disposing)
{
if (!p_disposedValue)
{
Console.WriteLine("MagicVariable Disposed");
if (disposing)
{
}
p_disposedValue = true;
}
}
~MagicVariable()
{
Dispose(disposing: false);
}
public void Dispose()
{
Dispose(disposing: true);
GC.SuppressFinalize(this);
}
}
static class SuperProducer
{
public static IEnumerable<IProducer> GetNextProducer()
{
using ProducerFactory factory = new ProducerFactory();
foreach(var producer in factory.GetNextProducer())
yield return producer;
}
}
internal class Program
{
static void Main(string[] args)
{
IProducer? producer = SuperProducer.GetNextProducer().FirstOrDefault((IProducer?)null);
if (producer != null)
{
// Thrown ObjectDisposedException there:
foreach (IInstance instance in producer.GetNextInstance())
Console.WriteLine($"Instance id is: {instance.ID}");
}
}
}
}
Он вызывает исключение ObjectDisposedException
, потому, что фабрика освобождается по выходе из оператора using
в строке 124.
Дилемма следующая - ошибка исчезнет только, если не создавать фабрику в операторе using
, но это противоречит RAII концепции и фабрика не будет высвобождена. А если создавать фабрику в using
- то она высвобождается до возвращения IProducer
из цикла в строке 126.
Подскажите, пожалуйста, в чём архитектурная ошибка этого примера? Как можно решить её?
Ответы (1 шт):
А чего вы, собственно, ожидали-то? Вы же делите ресурс (MagicVariable) между фабрикой (ProducerFactory) и экземпляром (Instance), при этом время жизни ресурса управляется фабрикой, которая уничтожается раньше экземпляра. Так делать не надо.
У каждого ресурса есть область видимости и время жизни, и в хорошем коде они должны соответствовать. Иными словами, нельзя передавать ресурс туда, где ссылка на него может пережить сам ресурс. Да, в языках со сборкой мусора это правило не такое строгое как в языках без неё, но неработающая программа всё ещё остаётся неработающей программой, пусть и безопасно неработающей.
Рассмотрим простой пример:
Resource res;
{
using var tmp = new Resource();
res = tmp;
}
res.Access(); // вот тут возникнет исключение
Вроде бы очевидно, что так писать не следует? Так почему же вы думаете, что обернув переменную tmp в обёртку ProducerFactory, а переменную res - в Instance и Producer, вы можете избежать этого исключения?
И так, что же делать с фабрикой и ресурсом. А варианта всего три:
Если фабрика владеет ресурсом, который разделяется всеми порождёнными объектами - значит, фабрику нельзя освобождать пока живы эти самые объекты.
Ваш SuperProducer нарушает это правило. Фабрику надо создавать уровнем выше:
static class SuperProducer { public static IEnumerable<IProducer> GetNextProducer(ProducerFactory factory) { foreach(var producer in factory.GetNextProducer()) yield return producer; } } internal class Program { static void Main(string[] args) { using ProducerFactory factory = new ProducerFactory(); IProducer? producer = SuperProducer.GetNextProducer(factory ).FirstOrDefault((IProducer?)null); if (producer != null) { foreach (IInstance instance in producer.GetNextInstance()) Console.WriteLine($"Instance id is: {instance.ID}"); } } }
Если такой вариант совершенно не устраивает - значит, фабрика не должна владеть ресурсом. Создавайте отдельный ресурс для каждого порождённого объекта.
Если этот вариант снова не подходит, и ресурс не может не быть общим - значит, настала пора подсчёта ссылок. Считайте сколько объектов владеет ресурсом, и освобождайте его только когда это число достигнет нуля.