Как правильно высвобождать частично созданные объекты в фабричном методе?

У меня есть класс MyClass, который требует создания экземпляров A, B и C для инициализации. Все три класса наследуют IDisposable. Я не владею исходным кодом классов A, B и C, однако их конструкторы могут вызвать исключение при определённых комбинациях параметров. Я знаю, что создавать исключение в конструкторе - это плохая практика, потому что в памяти может остаться класс в несогласованном состоянии (нарушение инварианта). Поэтому вместо использования прямого конструктора я использую фабричный метод MyClass.Create() внутри которого происходит инициализация A, B и C. Функция Create генерирует исключение, если хотя-бы один из классов A, B и C сгенерировал исключение. Я знаю, что если я не смог создать B, то я должен как минимум освободить класс A и поднять исключение. Также я знаю, что если я не смог создать класс C, то я должен, по меньшей мере, освободить ранее созданные A и B, а затем поднять исключение в функции Create.

Скажите пожалуйста, есть ли хороший паттерн или пример кода, как я могу очистить ранее инициализированные части в случае, когда мне не удалось собрать класс MyClass?
Что если частей будет больше трёх? Я думаю об использовании стека Stack<IDisposable> для очистки частей.


Добавляю минимально воспроизводимый пример по требованию
@user7860670. Похоже, что текст слишком сложен для восприятия:

class A: IDisposable
{
  private readonly int p_value;

  public A(int value)
  {
    ArgumentOutOfRangeException.ThrowIfNegative(value);
    p_value = value;
  }

  public void Print()
  {
    Console.WriteLine($"Value is: {p_value}");
  }

  public void Dispose()
  {
    Console.WriteLine("A disposed");
  }
}


class B : IDisposable
{
  private readonly int p_value;

  public B(int value)
  {
    ArgumentOutOfRangeException.ThrowIfLessThan(value, 10);
    p_value = value;
  }

  public void Print()
  {
    Console.WriteLine($"Value is: {p_value}");
  }

  public void Dispose()
  {
    Console.WriteLine("B disposed");
  }
}


class C : IDisposable
{
  private readonly int p_value;

  public C(int value)
  {
    ArgumentOutOfRangeException.ThrowIfLessThan(value, 20);
    p_value = value;
  }

  public void Print()
  {
    Console.WriteLine($"Value is: {p_value}");
  }

  public void Dispose()
  {
    Console.WriteLine("C disposed");
  }
}

class MyClass
{
  private readonly A a;
  private readonly B b;
  private readonly C c;

  protected MyClass(A _a, B _b, C _c) { a = _a; b = _b; c = _c;}

  public MyClass Create(int _value)
  {
    // Maybe throw error
    A a = new A(_value);

    // Maybe throw error
    // Need free A
    B b = new B(_value);

    // Maybe throw error
    // Need free A
    // Need free B
    C c = new C(_value);

    return new (a, b, c);
  }
}

Ответы (1 шт):

Автор решения: Alexander Petrov

Как насчёт такого кода?

public static MyClass Create(int value)
{
    A a = null!;
    B b = null!;
    C c = null!;

    try
    {
        a = new A(value);
        b = new B(value);
        c = new C(value);
    }
    catch
    {
        a?.Dispose();
        b?.Dispose();
        c?.Dispose();

        throw new Exception("it's bad");
    }
    return new(a, b, c);
}

Но ещё лучше выкинуть фабричный метод и выполнять инициализацию в конструкторе:

class MyClass
{
    private readonly A a;
    private readonly B b;
    private readonly C c;

    public MyClass(int value)
    {
        try
        {
            a = new A(value);
            b = new B(value);
            c = new C(value);
        }
        catch
        {
            a?.Dispose();
            b?.Dispose();
            c?.Dispose();

            throw new Exception("it's bad");
        }
    }
}

P.S. Сам класс MyClass стоит сделать IDisposable, и в его методе Dispose освобождать a, b, c.

class MyClass : IDisposable
{
    ...

    public void Dispose()
    {
        a?.Dispose();
        b?.Dispose();
        c?.Dispose();
    }
}
→ Ссылка