Оператор `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 шт):

Автор решения: Pavel Mayorov

А чего вы, собственно, ожидали-то? Вы же делите ресурс (MagicVariable) между фабрикой (ProducerFactory) и экземпляром (Instance), при этом время жизни ресурса управляется фабрикой, которая уничтожается раньше экземпляра. Так делать не надо.

У каждого ресурса есть область видимости и время жизни, и в хорошем коде они должны соответствовать. Иными словами, нельзя передавать ресурс туда, где ссылка на него может пережить сам ресурс. Да, в языках со сборкой мусора это правило не такое строгое как в языках без неё, но неработающая программа всё ещё остаётся неработающей программой, пусть и безопасно неработающей.

Рассмотрим простой пример:

Resource res;

{
    using var tmp = new Resource();
    res = tmp;
}

res.Access(); // вот тут возникнет исключение

Вроде бы очевидно, что так писать не следует? Так почему же вы думаете, что обернув переменную tmp в обёртку ProducerFactory, а переменную res - в Instance и Producer, вы можете избежать этого исключения?


И так, что же делать с фабрикой и ресурсом. А варианта всего три:

  1. Если фабрика владеет ресурсом, который разделяется всеми порождёнными объектами - значит, фабрику нельзя освобождать пока живы эти самые объекты.

    Ваш 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}");
            }
        }
    }
    
  2. Если такой вариант совершенно не устраивает - значит, фабрика не должна владеть ресурсом. Создавайте отдельный ресурс для каждого порождённого объекта.

  3. Если этот вариант снова не подходит, и ресурс не может не быть общим - значит, настала пора подсчёта ссылок. Считайте сколько объектов владеет ресурсом, и освобождайте его только когда это число достигнет нуля.

→ Ссылка