CS8176: Итераторы не могут иметь локальных объектов по ссылке

#c# #ref #enumerator

#c# #ссылка #перечислитель

Вопрос:

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

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

Теперь я вижу два варианта:

  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 пересекать границу (что здесь не нужно). Или любой другой способ выполнить работу, который выглядит выполнимым (я понимаю, что то, что я представляю, сложнее реализовать, и ожидаю, что ответ будет таким: у людей из Roslyn есть дела поважнее. Опять же, без обид, совершенно правильный ответ.)

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

  1. Да, возможно, в будущем / не стоит (создайте проблему — подойдет, если вы сочтете, что это того стоит).
  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)
        {
        }
    }
}
  

Ответ №1:

Компилятор хочет переписать блоки итератора с локальными объектами в качестве полей, чтобы сохранить состояние, и вы не можете иметь ссылочные типы в качестве полей. Да, вы правы, что он не пересекает, 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;
            }
        }
    }
  

или

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

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

Комментарии:

1. @firda это зависит от вас; Я ожидаю, что это будет закрыто как «по дизайну», и: я бы поддержал это закрытие

2. Я ожидал, что это будет угловой случай, который не стоит рассматривать сейчас , но поддерживает это закрытие ? Хорошо, я ожидал отставания . (PS: я принимаю, даже не буду пытаться)

3. @firda потому что количество случаев, когда компилятор может доказать это , ограничено, и это сложно для людей

Ответ №2:

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

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

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

Как бы то ни было, версия Marc на ~ 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 |