C#: реализация SkipLast

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

    public static IEnumerable<T> SkipLast<T>(this IEnumerable<T> source)
    {
        using (IEnumerator<T> iterator = source.GetEnumerator())
        {
            if(iterator.MoveNext())
                while(true)
                {
                    var current = iterator.Current;
                    if(!iterator.MoveNext())
                        yield break;
                    yield return current;
                }
        }
    }

Мне это нужно для того, чтобы что-то сделать со всеми элементами, кроме последнего. В моем случае у меня есть последовательность объектов с различными свойствами. Затем я упорядочиваю их по дате, а затем мне нужно настроить все из них, кроме самого последнего элемента (который будет последним после заказа).

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

Я предполагаю, что более общей реализацией мог бы быть метод AllExceptMaxBy. Так как это как-то так. В MoreLinq есть метод MaxBy и MinBy, и мой метод должен выполнять то же самое, но возвращает каждый элемент, кроме максимального или минимального.


person Svish    schedule 09.06.2009    source источник


Ответы (4)


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

Альтернативным подходом было бы использование foreach, всегда возвращающее ранее возвращенное значение, если вы не были на первой итерации:

public static IEnumerable<T> SkipLast<T>(this IEnumerable<T> source)
{
    T previous = default(T);
    bool first = true;
    foreach (T element in source)
    {
        if (!first)
        {
            yield return previous;
        }
        previous = element;
        first = false;
    }
}

Другой вариант, более близкий к вашему коду:

public static IEnumerable<T> SkipLast<T>(this IEnumerable<T> source)
{
    using (IEnumerator<T> iterator = source.GetEnumerator())
    {
        if(!iterator.MoveNext())
        {
            yield break;
        }
        T previous = iterator.Current;
        while (iterator.MoveNext())
        {
            yield return previous;
            previous = iterator.Current;
        }
    }
}

Это позволяет избежать такой же глубокой вложенности (выполняя ранний выход, если последовательность пуста) и использует «настоящее» условие while вместо while(true)

person Jon Skeet    schedule 09.06.2009
comment
Что такое марковская точка остановки? В любом случае, хорошие предложения! Глубокая вложенность и while(true) были одними из причин, по которым мое решение казалось немного странным, хе-хе. - person Svish; 09.06.2009
comment
@Jon, спасибо, что разъяснили, в чем настоящая проблема с функциями Last-family. В моем случае похоже, что SkipLast(5) должен отставать на 5 элементов... что, возможно, выглядит хорошей работой для очереди. - person Roy Tinker; 16.02.2012

Ваша реализация выглядит отлично для меня - вероятно, я бы так и сделал.

Единственное упрощение, которое я мог бы предложить в отношении вашей ситуации, — упорядочить список наоборот (т. е. по возрастанию, а не по убыванию). Хотя это может не подходить для вашего кода, это позволит вам просто использовать collection.Skip(1) для получения всех элементов, кроме самого последнего.

Если это невозможно по причинам, которые вы не указали в своем посте, то ваша текущая реализация не представляет проблемы.

person Noldorin    schedule 09.06.2009
comment
Это хороший момент, да. На самом деле не думал об этом ... но мне нужна последовательность, заказанная позже. что означает, что мне придется заказать его, а затем снова отменить или что-то в этом роде. Я думаю, это тоже могло бы сработать ... но немного странно ходить туда-сюда вот так :p - person Svish; 09.06.2009

Если вы используете .NET 3.5, я думаю, вы могли бы использовать:

public static IEnumerable<T> SkipLast<T>(this IEnumerable<T> source)
{
  return source.TakeWhile((item, index) => index < source.Count() - 1))
}
person Razzie    schedule 09.06.2009
comment
Однако это дважды перечисляет коллекцию (если источник не является списком), так что это, вероятно, плохая идея! (И даже не будет работать так же в общем случае недетерминированного итератора.) - person Noldorin; 09.06.2009
comment
Это также потребует Count() вместо Count. И на самом деле он будет перечислять коллекцию много-много раз, потому что лямбда-выражение будет подсчитывать узлы каждый раз, когда оно вычисляется! - person Jon Skeet; 09.06.2009
comment
для моего собственного образования: было бы нормально, если бы подсчет был сделан один раз за пределами лямбды? Я не совсем понимаю, почему он будет перечислять коллекцию дважды. - person Razzie; 09.06.2009
comment
сделать подсчет один раз, прежде чем вы начнете, было бы намного лучше, да, но все равно перечислять дважды. один раз для подсчета и один раз для взятия. - person Svish; 09.06.2009
comment
дух - на счет. Где мой кофе?! В любом случае, я оставлю этот ответ здесь, чтобы другие люди не совершали ту же ошибку :) - person Razzie; 09.06.2009
comment
Хорошая идея. Я тоже не думал об этой проблеме лямбда-выражения, хе-хе. - person Svish; 09.06.2009

(Старый ответ удален; этот код был протестирован и работает.) Он печатает
первый
второй
ПЕРВЫЙ
ВТОРОЙ
ТРЕТИЙ


public static class ExtNum{
  public static IEnumerable skipLast(this IEnumerable source){
    if ( ! source.Any())
      yield break;
    for (int i = 0 ; i <=source.Count()-2 ; i++ )
      yield return source.ElementAt(i);
    yield break;
  }
}
class Program
{
  static void Main( string[] args )
  {
    Queue qq = new Queue();
    qq.Enqueue("first");qq.Enqueue("second");qq.Enqueue("third");
    List lq = new List();
    lq.Add("FIRST"); lq.Add("SECOND"); lq.Add("THIRD"); lq.Add("FOURTH");
    foreach(string s1 in qq.skipLast())
      Console.WriteLine(s1);
    foreach ( string s2 in lq.skipLast())
      Console.WriteLine(s2);
  }
}
person Community    schedule 09.06.2009
comment
В IEnumerable нет счетчика или индексатора. Итак, чтобы сделать это, вам нужно было бы поместить его в список или что-то в этом роде. И это означает, что вам придется пройти через элементы дважды. Один раз, чтобы поместить их в список, и один раз, чтобы выдать их. - person Svish; 10.06.2009