Метод LINQ, который выбирает элемент на основе предыдущего


мое требование заключается в реализации способа отфильтровать набор элементов в зависимости от значения предыдущей. Например, чтобы выбрать номер, который больше, чем предыдущий. В настоящее время я не знаю ни метода LINQ, которые позволили бы мне делать такие вещи, так что я решил создать.

Это код я так далеко:

        public static IEnumerable<T> WherePrevious<T>(this IEnumerable<T> collection, Func<T, T, bool> predicate)
    {
        if (collection == null) throw new ArgumentNullException(nameof(collection));
        if (predicate == null) throw new ArgumentNullException(nameof(predicate));

        T previous = default(T);
        bool firstIteration = true;

        foreach (var item in collection)
        {
            if (firstIteration)
            {
                previous = item;
                firstIteration = false;
                yield return item;
            }
            else
            {
                if (predicate(previous, item))
                {
                    yield return item;
                }

                previous = item;
            }
        }
    }

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

Например, я не уверен, если это ясно, вызывающий метод, что входы предиката на самом деле



1274
5
задан 9 февраля 2018 в 12:02 Источник Поделиться
Комментарии
3 ответа

LINQ как молния пришло в голову для того, чтобы отобразить элементы в парах, а затем применить предикат.

public static IEnumerable<T> WherePrevious<T>(this IEnumerable<T> collection, Func<T, T, bool> predicate) {
if (collection == null) throw new ArgumentNullException(nameof(collection));
if (predicate == null) throw new ArgumentNullException(nameof(predicate));
if(!collection.Any()) yield break;

yield return collection.First();

var items = collection
.Zip(collection.Skip(1), (previous, item) => (previous, item))
.Where(zip => predicate(zip.previous, zip.item))
.Select(zip => zip.item);

foreach(var item in items)
yield return item;
}

Следующий тест был использован для продемонстрировать пример, приведенный в исходный вопрос.

[TestClass]
public class FilterTest {
[TestMethod]
public void WherePreviousTest() {
//Arrange
var numbers = new[] { 1, 5, 8, 7, 12, 8, 5 };
var expected = new[] { 1, 5, 8, 12 };

//Act
var actual = numbers.WherePrevious((first, second) => second > first).ToArray();

//Assert
actual.ShouldAllBeEquivalentTo(expected);
}
}

Тело создал метод расширения можно было сделать самостоятельно, но создать метод расширения разрешенных для чистого подхода.

Хотя я бы сказал, что это легче читать, ИМО, технически вы будете перечислять список более чем один раз, что делает это не так эффективно, как ваш один проход через.

Что тогда веди меня пересмотреть мой подход. После просмотра исходного кода Enumerable.Zip и поняв вашего подхода была правильная идея, я рефакторинг кода, чтобы использовать перечислитель коллекции.

public static IEnumerable<T> WherePrevious<T>(this IEnumerable<T> collection, Func<T, T, bool> predicate) {
if (collection == null) throw new ArgumentNullException(nameof(collection));
if (predicate == null) throw new ArgumentNullException(nameof(predicate));
return WherePreviousIterator(collection, predicate);
}

private static IEnumerable<T> WherePreviousIterator<T>(IEnumerable<T> collection, Func<T, T, bool> predicate) {
using (var e = collection.GetEnumerator()) {
if (e.MoveNext()) {
var previous = e.Current;
yield return previous;
while (e.MoveNext()) {
var item = e.Current;
if (predicate(previous, item))
yield return item;
previous = item;
}
}
yield break;
}
}

Которые фактически выполнены не намного лучше, чем предыдущее предложение, используя молнии, на одном тесте выше.

3
ответ дан 9 февраля 2018 в 03:02 Источник Поделиться

Сначала я пытался сделать свое расширение немного яснее, избегая флаг firstIteration:

public static IEnumerable<T> WherePreviousReview<T>(this IEnumerable<T> collection, Func<T, T, bool> predicate)
{
if (collection == null) throw new ArgumentNullException(nameof(collection));
if (predicate == null) throw new ArgumentNullException(nameof(predicate));
if (!collection.Any())
yield break;

T previous = collection.First();

yield return previous;

foreach (var item in collection.Skip(1))
{
if (predicate(previous, item))
{
yield return item;
}

previous = item;
}
}


Это версия, которая использует власть Where расширение:

public static IEnumerable<T> WherePrevious<T>(this IEnumerable<T> collection, Func<T, T, bool> predicate)
{
if (collection == null) throw new ArgumentNullException(nameof(collection));
if (predicate == null) throw new ArgumentNullException(nameof(predicate));
T previous = default(T);
return collection.Where((current, i) =>
{
bool result = i == 0 || predicate(previous, current);
previous = current;
return result;
});
}


Это версия, которая использует Aggregate расширение:

public static IEnumerable<T> NewWherePrevious<T>(this IEnumerable<T> collection, Func<T, T, bool> predicate)
{
if (collection == null) throw new ArgumentNullException(nameof(collection));
if (predicate == null) throw new ArgumentNullException(nameof(predicate));
if (!collection.Any())
return new List<T>();

return collection.Aggregate((0, default(T), new List<T>()), (acc, cur) =>
{
if (acc.Item1 == 0 || acc.Item1 > 0 && predicate(acc.Item2, cur))
{
acc.Item3.Add(cur);
}

acc.Item1++;
acc.Item2 = cur;

return acc;
}).Item3;
}

Последнее не означало, как лучший выбор, просто пример другого подхода. Оговоркой-это внутренний объект списка.


Обновление

Совокупный версия с именованных кортежей:

public static IEnumerable<T> NewWherePrevious<T>(this IEnumerable<T> collection, Func<T, T, bool> predicate)
{
if (collection == null) throw new ArgumentNullException(nameof(collection));
if (predicate == null) throw new ArgumentNullException(nameof(predicate));
if (!collection.Any())
return new T[0];

(int index, T prev, List<T> list) seed = (0, default(T), new List<T>());

return collection.Aggregate(seed, ((int index, T prev, List<T> list) acc, T cur) =>
{
if (acc.index == 0 || acc.index > 0 && predicate(acc.prev, cur))
{
acc.list.Add(cur);
}

acc.index++;
acc.prev = cur;

return acc;
}).list;
}

2
ответ дан 9 февраля 2018 в 07:02 Источник Поделиться


Например, чтобы выбрать номер, который больше, чем предыдущий. В настоящее время я не знаю ни метода LINQ, который позволит мне сделать такую вещь

На самом деле есть расширение для LINQ, которая позволяет сделать это. в Where расширение имеет перегрузку, которая использует значение элемента и элемента индекса:

List<int> test1 = new List<int>()
{
23,
45,
16,
8,
90,
25
};
var test2 = test1.Where((x, i) => (i > 0)?x > test1[i - 1]:false);

Один нюанс, для этого, он будет работать только для индексированных коллекций типы, которые поддерживают операторы сравнения.

1
ответ дан 9 февраля 2018 в 06:02 Источник Поделиться