Home > Enterprise >  Short circuit yield return & cleanup/dispose
Short circuit yield return & cleanup/dispose

Time:12-13

Take this pseudo example code:

    static System.Runtime.InteropServices.ComTypes.IEnumString GetUnmanagedObject() => null;
static IEnumerable<string> ProduceStrings()
{
    System.Runtime.InteropServices.ComTypes.IEnumString obj = GetUnmanagedObject();
    var result = new string[1];
    var pFetched = Marshal.AllocHGlobal(sizeof(int));
    while(obj.Next(1, result, pFetched) == 0)
    {
        yield return result[0];
    }
    Marshal.ReleaseComObject(obj);
}

static void Consumer()
{
    foreach (var item in ProduceStrings())
    {
        if (item.StartsWith("foo"))
            return;
    }
}

Question is if i decide to not enumerate all values, how can i inform producer to do cleanup?

CodePudding user response:

Even if you are after a solution using yield return, it might be useful to see how this can be accomplished with an explicit IEnumerator<string> implementation.

IEnumerator<T> derives from IDisposable and the Dispose() method will be called when foreach is left (at least since .NET 1.2, see here)

static IEnumerable<string> ProduceStrings()
{
    return new ProduceStringsImpl();
}

This is the class implementing IEnumerable<string>

class ProduceStringsImpl : IEnumerable<string>
{
    public IEnumerator<string> GetEnumerator()
    {
        return new EnumProduceStrings();
    }
    IEnumerator IEnumerable.GetEnumerator()
    {
        return GetEnumerator();
    }
}

And here we have the core of the solution, the IEnumerator<string> implementation:

class EnumProduceStrings : IEnumerator<string>
{
    private System.Runtime.InteropServices.ComTypes.IEnumString _obj;
    private string[] _result;
    private IntPtr _pFetched;
    
    public EnumProduceStrings()
    {
        _obj = GetUnmanagedObject();
        _result = new string[1];
        _pFetched = Marshal.AllocHGlobal(sizeof(int));
    }
    
    public bool MoveNext()
    {
        return _obj.Next(1, _result, _pFetched) == 0;
    }
    
    public string Current => _result[0];
    
    void IEnumerator.Reset() => throw new NotImplementedException();
    object IEnumerator.Current => Current;
    
    public void Dispose()
    {
        Marshal.ReleaseComObject(_obj);
        Marshal.FreeHGlobal(_pFetched);
    }
}

CodePudding user response:

I knew i can! Despite guard, Cancel is called only one time in all circumtances.

You can instead encapsulate logic with a type like IterationResult<T> and provide Cleanup method on it but its essentially same idea.

public class IterationCanceller
{
    Action m_OnCancel;
    public bool Cancelled { get; private set; }
    public IterationCanceller(Action onCancel)
    {
        m_OnCancel = onCancel;
    }
    public void Cancel()
    {
        if (!Cancelled)
        {
            Cancelled = true;
            m_OnCancel();
        }
    }
}
static IEnumerable<(string Result, IterationCanceller Canceller)> ProduceStrings()
{
    var pUnmanaged = Marshal.AllocHGlobal(sizeof(int));
    IterationCanceller canceller = new IterationCanceller(() =>
    {
        Marshal.FreeHGlobal(pUnmanaged);
    });
    for (int i = 0; i < 2; i  ) // also try i < 0, 1
    {
        yield return (i.ToString(), canceller);
    }
    canceller.Cancel();
}

static void Consumer()
{
    foreach (var (item, canceller) in ProduceStrings())
    {
        if(item.StartsWith("1")) // also try consuming all values
        {
            canceller.Cancel();
            break;
        }
    }
}
  • Related