CS8176: Итераторы не могут иметь локальные переменные по ссылке.

Есть ли реальная причина для этой ошибки в данном коде, или просто это может пойти не так при обычном использовании, когда потребуется ссылка на шаг интератора (что неверно в данном случае)?

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 – карта индекса имен, регистронезависимая
Prop.next – указывает на следующий узел для повторения (-1 в качестве признака конца; поскольку dict нечувствительна к регистру, а это связано -list был добавлен для разрешения конфликтов путем поиска с учетом регистра с откатом к первому).

Я вижу сейчас два варианта:

  1. Реализовать пользовательский итератор/перечислитель, mscs/Roslyn сейчас недостаточно хорош, чтобы хорошо видеть и выполнять свою работу. (Здесь нет виноватых, я понимаю, не такая уж и важная функция.)
  2. Отбросьте оптимизацию и просто проиндексируйте ее дважды (один раз для name и второй раз для next). Возможно, компилятор все равно это поймет и создаст оптимальный машинный код. (Я создаю скриптовый движок для Unity, это действительно критично для производительности. Может быть, он просто проверяет границы один раз и в следующий раз использует доступ, подобный ссылке/указателю, бесплатно.)

И, может быть, 3. (2b, 2+1/2) Просто скопируйте структуру (32B на x64, три ссылки на объекты и два целых числа, но может расти, будущее не видно). Вероятно, это не очень хорошее решение (либо я забочусь и пишу итератор, либо он так же хорош, как 2.)

Что я понимаю:

ref var p не может жить после yield return, потому что компилятор строит итератор - конечный автомат, ref нельзя передать следующему IEnumerator.MoveNext(). Но это не тот случай.

Чего я не понимаю:

Почему применяется такое правило, вместо того, чтобы пытаться фактически сгенерировать итератор/перечислитель, чтобы увидеть, нужно ли такому ref var пересекать границу (что здесь не нужно). Или любой другой способ выполнить работу, которая выглядит выполнимой (я понимаю, что то, что я себе представляю, сложнее реализовать, и ожидаю, что ответ будет таким: у ребят из Рослина есть дела поважнее. Опять же, без обид, вполне верный ответ.)

Ожидаемые ответы:

  1. Да, может быть в будущем/не стоит (создайте Issue - сделаю, если сочтете, что оно того стоит).
  2. Есть лучший способ (пожалуйста, поделитесь, мне нужно решение).

Если вам нужен дополнительный контекст, он для этого проекта: https://github.com/evandisoft/RedOnion/tree/master/RedOnion.ROS/Descriptors/Reflect (Reflected.cs и Members.cs)

Воспроизводимый пример:

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)
        {
        }
    }
}

person firda    schedule 03.09.2020    source источник


Ответы (2)


Компилятор хочет переписать блоки итераторов с локальными переменными в качестве полей, чтобы сохранить состояние, а вы не можете использовать ref-типы в качестве полей. Да, вы правы в том, что он не пересекает yield, поэтому технически его можно переписать, чтобы повторно объявить его по мере необходимости, но это делает его очень сложным правила, которые люди должны помнить, когда простые на вид изменения нарушают код. Одеяло нет гораздо легче грок.

Обходной путь в этом сценарии (или аналогично с методами async) обычно является вспомогательным методом; Например:

    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);
            }
        }
    }

Локальная функция не связана правилами блока итератора, поэтому вы можете использовать ref-locals.

person Marc Gravell    schedule 03.09.2020
comment
@firda решать тебе; Я ожидаю, что он будет закрыт по замыслу, и: я бы поддержал это закрытие - person Marc Gravell; 03.09.2020
comment
Я ожидал, что это будет крайний случай, который не стоит рассматривать сейчас, но поддерживать это закрытие? Хорошо, я ожидал задержки. (P.S.: согласен, даже пробовать не буду) - person firda; 03.09.2020
comment
@firda, потому что количество случаев, когда компилятор может доказать это, ограничено, и это сложно для людей - person Marc Gravell; 03.09.2020

ссылка не может быть передана следующему IEnumerator.MoveNext(). Но это не тот случай.

Компилятор создает класс конечного автомата для хранения данных, необходимых во время выполнения для перехода к следующей итерации. Это этот класс, который не может содержать элемент ref.

Компилятор может обнаружить, что переменная нужна только в ограниченной области и не нуждается в добавлении в этот класс состояния, но, как говорит Марк в своем ответе, это дорогая функция с небольшим дополнительным преимуществом. Помните, что функции начинаются с -100 баллов. Таким образом, вы можете попросить его, но обязательно объясните его использование.

Как бы то ни было, версия Марка примерно на 4% быстрее (согласно BenchmarkDotNet) для этой установки:

public class StructArrayAccessBenchmark
{
    struct Prop
    {
        public string name;
        public int next;
    }

    private readonly Prop[] _props = 
    {
        new Prop { name = "1-1", next = 1 }, // 0
        new Prop { name = "1-2", next = -1 }, // 1

        new Prop { name = "2-1", next = 3 }, // 2
        new Prop { name = "2-2", next = 4 }, // 3
        new Prop { name = "2-2", next = -1 }, // 4
    };

    readonly Dictionary<string, int> _dict = new Dictionary<string, int>
    {
        { "1", 0 },
        { "2", 2 },
    };

    private readonly Consumer _consumer = new Consumer();

    // 95ns
    [Benchmark]
    public void EnumerateRefLocalFunction() => enumerateRefLocalFunction().Consume(_consumer);

    // 98ns
    [Benchmark]
    public void Enumerate() => enumerate().Consume(_consumer);

    public IEnumerable<string> enumerateRefLocalFunction()
    {
        (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, next) = GetNext(i);
            yield return name;

            while (next >= 0)
            {
                (name, next) = GetNext(next);
                yield return name;
            }
        }
    }

    public IEnumerable<string> enumerate()
    {
        foreach (int i in _dict.Values)
        {
            var p = _props[i];
            int next = p.next;
            yield return p.name;
            while (next >= 0)
            {
                p = _props[next];
                next = p.next; 
                yield return p.name;
            }
        }
    }

Полученные результаты:

|                    Method |      Mean |    Error |   StdDev |
|-------------------------- |----------:|---------:|---------:|
| EnumerateRefLocalFunction |  94.83 ns | 0.138 ns | 0.122 ns |
|                 Enumerate |  98.00 ns | 0.285 ns | 0.238 ns |
person CodeCaster    schedule 03.09.2020