0

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?

ömer hayyam
  • 173
  • 6
  • You can't, not with this code. If it is absolutely necessary (why?) then iterate the collection immediately and store the items in a List<>. Then yield the list elements. – Hans Passant Dec 12 '21 at 19:03
  • You could - instead of using `yield return` implement the `IEnumerator` object yourself and clean up in `Dispose()` – Klaus Gütter Dec 12 '21 at 19:14
  • @KlausGütter i now i can implement IEnumerator or move iteration logic inside other method, but i wondered if its possible (possibly with some magic) with built-in iterators. – ömer hayyam Dec 12 '21 at 19:26
  • @HansPassant as ive said, i dont probably want to iterate all elements (say its expensive) and solve the issue elegantly or just for curiousity with iterator blocks – ömer hayyam Dec 12 '21 at 19:28
  • @HansPassant finally managed it. see my answer below. – ömer hayyam Dec 12 '21 at 21:08

3 Answers3

1

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);
    }
}
Klaus Gütter
  • 11,151
  • 6
  • 31
  • 36
0

I think I've found a possible better solution for this issue using an IDisposable implementation. Similar to how @ömer hayyam suggested, you can encapsulate the cleanup logic in a class, but additionally you can use a Using statement to ensure that the cleanup code is executed regardless of a break from the IEnumerable:

class Cleanup : IDisposable
{
    public delegate void CleanupFunction();

    private readonly CleanupFunction _cleanup;

    public Cleanup(CleanupFunction cleanup)
    {
        _cleanup = cleanup;
    }

    public void Dispose()
    {
        _cleanup();
    }
}

class Test
{
    private static IEnumerable<int> InfGen()
    {
        var storage = new List<int>();
        using var unused = new Cleanup(() => storage.ForEach(Console.WriteLine));
        var i = 0;
        while (true)
        {
            yield return i;
            storage.Add(i);
            i++;
        }
    }

    public static void Main(string[] args)
    {
        foreach (int i in InfGen())
        {
            Console.WriteLine("Before: " + i);
            if (i > 20)
            {
                break;
            }
        }
    }
}

After the break from the foreach is executed, the IEnumerable is cancelled but the IDisposable is still called, executing whatever was passed to Cleanup (in this case just a Console.Writeline).

kouta-kun
  • 169
  • 4
-1

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;
        }
    }
}
Guru Stron
  • 102,774
  • 10
  • 95
  • 132
ömer hayyam
  • 173
  • 6