Search code examples
c#refenumerator

CS8176: Iterators cannot have by-reference locals


Is there real reason for this error in given code, or just It could go wrong in general usage where the reference would be needed across interator steps (which is not true in this case)?

IEnumerable<string> EnumerateStatic()
{
    foreach (int i in dict.Values)
    {
        ref var p = ref props[i]; //< CS8176: Iterators cannot have by-reference locals
        int next = p.next;
        yield return p.name;
        while (next >= 0)
        {
            p = ref props[next];
            next = p.next;
            yield return p.name;
        }
    }
}

struct Prop
{
    public string name;
    public int next;
    // some more fields like Func<...> read, Action<..> write, int kind
}
Prop[] props;
Dictionary<string, int> dict;

dict is name-index map, case-insensitive
Prop.next is to point-to next node to be iterated over (-1 as terminator; because dict is case-insensitive and this linked-list was added to resolve conflicts by case-sensitive search with fallback to first).

I see two options now:

  1. Implement custom iterator/enumerator, mscs/Roslyn is just not good enough now to see well and do its job. (No blame here, I can understand, not so important feature.)
  2. Drop the optimisation and just index it twice (once for name and second time for next). Maybe the compiler will get it and produce optimal machine code anyway. (I am creating scripting engine for Unity, this really is performance critical. Maybe it just checks the bounds once and uses ref/pointer-like access with no cost next time.)

And maybe 3. (2b, 2+1/2) Just copy the struct (32B on x64, three object references and two integers, but may grow, cannot see future). Probably not good solution (I either care and write the iterator or it is as good as 2.)

What I do understand:

The ref var p cannot live after yield return, because the compiler is constructing the iterator - a state machine, the ref cannot be passed to next IEnumerator.MoveNext(). But that is not the case here.

What I do not understand:

Why is such a rule enforced, instead of trying to actually generate the iterator/enumerator to see if such ref var needs to cross the boundary (which it does not need here). Or any other way to do the job which looks doable (I do understand that what I imagine is harder to implement and expect answer to be: Roslyn folks have better things to do. Again, no offense, perfectly valid answer.)

Expected answers:

  1. Yes, maybe in the future / not worth it (create an Issue - will do if you find it worth it).
  2. There is better way (please share, I need solution).

If you want/need more context, it is for this project: https://github.com/evandisoft/RedOnion/tree/master/RedOnion.ROS/Descriptors/Reflect (Reflected.cs and Members.cs)

The reproducible example:

using System.Collections.Generic;

namespace ConsoleApp1
{
    class Program
    {
        class Test
        {
            struct Prop
            {
                public string name;
                public int next;
            }
            Prop[] props;
            Dictionary<string, int> dict;
            public IEnumerable<string> Enumerate()
            {
                foreach (int i in dict.Values)
                {
                    ref var p = ref props[i]; //< CS8176: Iterators cannot have by-reference locals
                    int next = p.next;
                    yield return p.name;
                    while (next >= 0)
                    {
                        p = ref props[next];
                        next = p.next;
                        yield return p.name;
                    }
                }
            }
        }
        static void Main(string[] args)
        {
        }
    }
}

Solution

  • The compiler wants to rewrite iterator blocks with locals as fields, to retain the state, and you cannot have ref-types as fields. Yes, you're right that it doesn't cross the yield so technically it could probably be rewritten to re-declare it as-needed, but that makes for very complex rules for humans to remember, where simple-looking changes break the code. A blanket "no" is much easier to grok.

    The workaround in this scenario (or similarly with async methods) is usually a helper method; for example:

        IEnumerable<string> EnumerateStatic()
        {
            (string value, int next) GetNext(int index)
            {
                ref var p = ref props[index];
                return (p.name, p.next);
            }
            foreach (int i in dict.Values)
            {
                (var name, var next) = GetNext(i);
                yield return name;
                while (next >= 0)
                {
                    (name, next) = GetNext(next);
                    yield return name;
                }
            }
        }
    

    or

        IEnumerable<string> EnumerateStatic()
        {
            string GetNext(ref int next)
            {
                ref var p = ref props[next];
                next = p.next;
                return p.name;
            }
            foreach (int i in dict.Values)
            {
                var next = i;
                yield return GetNext(ref next);
                while (next >= 0)
                {
                    yield return GetNext(ref next);
                }
            }
        }
    

    The local function is not bound by the iterator-block rules, so you can use ref-locals.