Список <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++;
}
}
Так это просто проблема со списком?
Ответы
Ответ 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);
Кажется, это намного проще...