Допустим, у нас есть List<int>
с таким содержимым, как [0,0,0,0,1,1,1,1,0,0,0,1,2,2,0,0,2,2]
, и мы хотим иметь индекс n-го числа, который не равен нулю.
Например, GetNthNotZero(3)
должен вернуть 6.
Это было бы легко с циклом for, но я чувствую, что для этого должен быть LINQ. Возможно ли это с оператором LINQ?
Стандартного метода не существует, но задумывались ли вы о написании собственного метода расширения, чтобы обеспечить что-то похожее на LINQ FindIndex()
?
class Program
{
static void Main(string[] args)
{
var list = new List<int>{ 0, 0, 0, 0, 1, 1, 1, 1, 0, 0, 0, 1, 2, 2, 0, 0, 2, 2 };
var index = list.FindNthIndex(x => x > 0, 3);
}
}
public static class IEnumerableExtensions
{
public static int FindNthIndex<T>(this IEnumerable<T> enumerable, Predicate<T> match, int count)
{
var index = 0;
foreach (var item in enumerable)
{
if (match.Invoke(item))
count--;
if (count == 0)
return index;
index++;
}
return -1;
}
}
Я выбираю это в качестве ответа, хотя, строго говоря, он не обеспечивает стиль «использовать только что существующий LINQ», который предлагает вопрос, но я думаю, что это наиболее прямое решение и поддерживает чистоту бизнес-логики при расширении LINQ функции, которые можно использовать в кодовой базе
На самом деле вы можете сделать это со стандартным LINQ, вы можете использовать:
List<int> sequence = new List<int>{0,0,0,0,1,1,1,1,0,0,0,1,2,2,0,0,2,2};
int index = sequence.Select((x, ix) => (Item:x, Index:ix))
.Where(x => x.Item != 0)
.Skip(2) // you want the 3rd, so skip 2
.Select(x => x.Index)
.DefaultIfEmpty(-1) // if there is no third matching condition you get -1
.First(); // result: 6
Я думаю, что вы не можете пропустить, так как он может перепрыгнуть через ноль.
@CjS.: Почему бы и нет? Я не пропускаю нули, я пропускаю условия совпадения, поэтому != 0. OP хочет индекс третьего условия совпадения, поэтому я пропускаю первые два совпадения
О - я вижу - Где нули опустились - Это умно!
@CjS.: Вот сетевая рабочий пример, поиграй с ней: dotnetfiddle.net/0ZEg5e
Работает как шарм и, строго говоря, является ответом на вопрос. Хотя я считаю, что этот LINQ по-прежнему хорошо читается и понятен, я больше склоняюсь к использованию метода расширения, когда LINQ превышает некоторую сложность для довольно простой задачи. Я придумал более простое решение. Интересно посмотреть, как этого можно добиться с помощью базовых функций LINQ.
Это, безусловно, возможно, но подход Linq сделает это намного сложнее. Это один из тех случаев, когда явный цикл намного лучше.
Две существенные сложности, связанные с использованием Linq:
Решение Linq может выглядеть следующим образом (но обратите внимание, что, вероятно, существует множество различных возможных подходов с использованием Linq):
using System;
using System.Collections.Generic;
using System.Linq;
public static class Program
{
public static void Main()
{
var ints = new List<int> { 0, 0, 0, 0, 1, 1, 1, 1, 0, 0, 0, 1, 2, 2, 0, 0, 2, 2 };
Console.WriteLine(IndexOfNthNotZero(ints, 3)); // 6
Console.WriteLine(IndexOfNthNotZero(Enumerable.Repeat(0, 10), 3)); // -1
Console.WriteLine(IndexOfNthNotZero(ints, 100)); // -1
Console.WriteLine(IndexOfNthNotZero(Array.Empty<int>(), 0)); // -1
}
public static int IndexOfNthNotZero(IEnumerable<int> sequence, int n)
{
return sequence
.Select((v, i) => (value:v, index:i)) // Synthesize the value and index.
.Where(item => item.value != 0) // Choose only the non-zero value.
.Skip(n-1) // Skip to the nth value.
.FirstOrDefault((value:0, index:-1)).index; // Handle missing data by supplying a default index of -1.
}
}
Обратите внимание, что эта реализация возвращает -1
, чтобы указать, что подходящее значение не найдено.
Сравните это с реализацией простого цикла, и я думаю, вы согласитесь, что лучше использовать простой цикл!
public static int IndexOfNthNotZero(IReadOnlyList<int> sequence, int n)
{
for (int i = 0; i < sequence.Count; ++i)
if (sequence[i] != 0 && --n == 0) // If element matches, decrement n and return index if it reaches 0.
return i;
return -1;
}
Или, если вы предпочитаете (избегая предварительного декремента):
public static int IndexOfNthNotZero(IReadOnlyList<int> sequence, int n)
{
for (int i = 0, numberOfMatches = 0; i < sequence.Count; ++i)
{
if (sequence[i] != 0) // If condition matches
if (++numberOfMatches == n) // Increment number of matches, and if it reaches n
return i; // then return the current index
}
return -1;
}
Я не согласен с простым циклом из-за удобочитаемости. Игнорируя имя метода (которое можно сделать описательным в любом случае), для разбора цикла требуется больше усилий, чем для чтения цепочки LINQ построчно. Если бы вы показали мне только фрагменты без дальнейшего контекста; Я мог бы рассказать вам, что это делает быстрее с помощью фрагмента LINQ. Не то чтобы какой-либо из фрагментов непостижимо сложен; но я чувствую, что читабельность LINQ - это большой плюс, который ответ несколько не оценивает.
@Flater Это явно вопрос мнения. Мне потребовались секунды, чтобы написать этот простой цикл, и гораздо больше времени, чтобы написать Linq из-за необходимости возиться с граничными условиями. Можете ли вы взглянуть на Linq и сразу сказать, что происходит, если последовательность пуста? Или если он не содержит совпадающих элементов? Или если количество совпадающих элементов меньше n
? Вы можете сказать все эти вещи с первого взгляда в простом цикле, IMO.
Я предполагаю, что это сводится к опыту LINQ, поскольку вопросы, которые вы задаете, (на мой взгляд) тривиально видны. LINQ использует пустые коллекции настолько часто, насколько это возможно; единственные исключения — это когда вы разворачиваете коллекцию в один элемент (First
и Single
), поэтому существует вариант OrDefault
для изящной обработки. Для всех трех вопросов, которые вы задаете, один из связанных методов приводит к пустой коллекции (соответственно Select
, Where
, Skip
). В каждом случае FirstOrDefault
получает пустую коллекцию, возвращая значение по умолчанию, поскольку первого элемента нет.
Дело также не столько в том, что мне нравится синтаксис LINQ, сколько в том, что я нахожу && --n == 0
действительно нечитаемое скрытое условие счетчика. Вам нужно знать об оценке короткого замыкания и различать --n
и n--
, а при внесении изменений вам действительно нужно посчитать, когда ваше состояние правильно достигает фазы выхода. Из-за этого логика счетчика очень чувствительна к ошибке «отключение на единицу». Не то чтобы это не работало, но его сложнее разобрать и легче пропустить, что вы где-то допустили ошибку.
@flater Как я уже сказал, это вопрос мнения. Я определенно думаю, что какая-то простая реализация цикла лучше, чем решение Linq для этой конкретной проблемы.
Это действительно дело вкуса, а также консистенции. Для кодовой базы, над которой я работаю, все эти задачи реализуются с помощью некоторого LINQ, поэтому здесь я склоняюсь к согласованности. Если вы найдете LINQ или цикл более читаемым, это действительно зависит от вашего опыта. Один вопрос: ".FirstOrDefault((значение:0, индекс:-1)).index;" для меня сбой компилятора docs.microsoft.com/en-us/dotnet/csharp/misc/… (VS2019, целевая платформа .NET5), это только у меня?
@sctty FirstOrDefault()
со значением по умолчанию только .NET 6.0 или более поздней версии, и вы используете только .NET 5.0, так что это объясняет.
Да, но извлечение индекса немного усложняет ситуацию, потому что большинство операций LINQ работают с последовательностью, а не с индексами. Заметным исключением является
Select
, который позволяет нам делать что-то вродеint GetNthNotZero(IEnumerable<int> s, int ordinal) => s.Select((value, index) => (value, index)).Where(t => t.value != 0).ElementAt(ordinal - 1).index;
. Обратите внимание, что это гораздо менее эффективно, чем простой цикл, с построением промежуточных последовательностей, и, возможно, не намного проще для понимания.