Подтвердить что ты не робот

Список <IEnumerator>.All(e => e.MoveNext()) не перемещает мои счетчики на

Я пытаюсь найти ошибку в нашем коде. Я отложил его до фрагмента ниже. В приведенном ниже примере у меня есть сетка ints (список строк), но я хочу найти индексы столбцов, у которых есть 1. Реализация этого заключается в создании перечислителя для каждой строки и переходе через каждый столбец в поверните, удерживая счетчики в шаге.

class Program
{
    static void Main(string[] args)
    {
        var ints = new List<List<int>> {
            new List<int> {0, 0, 1},    // This row has a 1 at index 2
            new List<int> {0, 1, 0},    // This row has a 1 at index 1
            new List<int> {0, 0, 1}     // This row also has a 1 at index 2
        };
        var result = IndexesWhereThereIsOneInTheColumn(ints);

        Console.WriteLine(string.Join(", ", result)); // Expected: "1, 2"
        Console.ReadKey();
    }


    private static IEnumerable<int> IndexesWhereThereIsOneInTheColumn(
        IEnumerable<List<int>> myIntsGrid)
    {
        var enumerators = myIntsGrid.Select(c => c.GetEnumerator()).ToList();

        short i = 0;
        while (enumerators.All(e => e.MoveNext())) {
            if (enumerators.Any(e => e.Current == 1))
                yield return i;
            i++;

            if (i > 1000)
                throw new Exception("You have gone too far!!!");
        }
    }

}

Однако я заметил, что MoveNext() не запоминается каждый раз вокруг цикла while. MoveNext() всегда возвращает true, а Current - всегда 0. Является ли это целенаправленной особенностью Linq, чтобы сделать его более свободным от побочных эффектов?

Я заметил, что это работает:

    private static IEnumerable<int> IndexesWhereThereIsOneInTheColumn(
        IEnumerable<List<int>> myIntsGrid)
    {
        var enumerators = myIntsGrid.Select(c => 
            c.ToArray().GetEnumerator()).ToList(); // added ToArray() 

        short i = 0;
        while (enumerators.All(e => e.MoveNext())) {
            if (enumerators.Any(e => (int)e.Current == 1)) // added cast to int
                yield return i;
            i++;
        }
    }

Так это просто проблема со списком?

4b9b3361

Ответ 1

Как ответ Шрирам Сактивель говорит, что проблема связана с отсутствием бокса и случайно, что реализация перечислителя списков является struct, а не ссылочным типом. Обычно не ожидает поведения типа значения для перечислителя, поскольку большинство из них либо открыто интерфейсами IEnumerator/IEnumerator<T>, либо сами являются ссылочными типами. Быстрый способ обойти это - изменить эту строку

var enumerators = myIntsGrid.Select(c => c.GetEnumerator()).ToList();

к

var enumerators 
    = myIntsGrid.Select(c => (IEnumerator) c.GetEnumerator()).ToList();

вместо.

В приведенном выше коде будет создан список уже вложенных в нумерацию счетчиков, которые будут обрабатываться как экземпляры ссылочного типа из-за передачи интерфейса. С этого момента они должны вести себя так, как вы ожидаете от них в своем более позднем коде.


Если вам нужен общий счетчик (чтобы избежать приведения, когда последний использует свойство enumerator.Current), вы можете применить его к соответствующему универсальному интерфейсу IEnumerator<T>:

c => (IEnumerator<int>) c.GetEnumerator()

или даже лучше

c => c.GetEnumerator() as IEnumerator<int>

Говорят, что ключевое слово as работает намного лучше, чем прямые трансляции, а в случае цикла это может принести существенное преимущество в производительности. Просто будьте осторожны, чтобы as возвращал null, если при выполнении броска В соответствии с запрос Flater из комментариев:. В случае OP гарантируется, что перечисление реализует IEnumerator<int>, поэтому безопасно использовать as.

Ответ 2

Это потому, что перечислитель List<T> является struct, тогда как перечислитель Array является class.

Поэтому, когда вы вызываете Enumerable.All со структурой, копия перечислителя производится и передается как параметр в Func, поскольку структуры копируются по значению. Поэтому e.MoveNext вызывается на копии, а не на оригинале.

Попробуйте следующее:

Console.WriteLine(new List<int>().GetEnumerator().GetType().IsValueType);
Console.WriteLine(new int[]{}.GetEnumerator().GetType().IsValueType);

Он печатает:

True
False

Ответ 3

В качестве альтернативы вы можете сделать это с расширением лямбда

var ids = Enumerable.Range(0,ints.Max (row => row.Count)).
      Where(col => ints.Any(row => (row.Count>col)? row[col] == (1) : false));

или

var ids = Enumerable.Range(0,ints.Max (row=> row.Count)).
      Where(col => ints.Any (row => row.ElementAtOrDefault(col) == 1));

Ответ 4

Здесь простая реализация с использованием циклов и yield:

private static IEnumerable<int> IndexesWhereThereIsOneInTheColumn(
    IEnumerable<List<int>> myIntsGrid)
{
    for (int i=0; myIntsGrid.Max(l=>l.Count) > i;i++)
    {
        foreach(var row in myIntsGrid)
        {
            if (row.Count > i && row[i] == 1)
            {
                yield return i;
                break;
            }
        }
    }       
}

В качестве альтернативы используйте это внутри цикла for:

if (myIntsGrid.Any(row => row.Count > i && row[i] == 1)) yield return i;

Ответ 5

Просто для удовольствия, здесь аккуратный запрос LINQ, который не будет вызывать жесткие следы побочных эффектов в вашем коде:

IEnumerable<int> IndexesWhereThereIsOneInTheColumn(IEnumerable<IEnumerable<int>> myIntsGrid)
{
    return myIntsGrid
        // Collapse the rows into a single row of the maximum value of all rows
        .Aggregate((acc, x) => acc.Zip(x, Math.Max))
        // Enumerate the row
        .Select((Value,Index) => new { Value, Index })
        .Where(x => x.Value == 1)
        .Select(x => x.Index);
}

Ответ 6

Почему вы не можете просто получить такие индексы, как это:

var result = ints.Select (i => i.IndexOf(1)).Distinct().OrderBy(i => i);

Кажется, это намного проще...