简体   繁体   English

如何评估IEnumerable <T> 同时对C#中的第一个和/或最后一个元素进行特殊处理?

[英]How can I evaluate an IEnumerable<T> while giving special treatment to its first and/or last element in C#?

This is a more general form of the question, how can I do something special with the first and last element of a list? 这是问题的更一般形式,如何对列表的第一个和最后一个元素进行特殊处理? The more specific question is easy to answer. 更具体的问题很容易回答。 We know the index of the first and last element, so we can simply access them directly or test an index variable against those values. 我们知道第一个元素和最后一个元素的索引,因此我们可以直接访问它们或针对这些值测试索引变量。 For example: 例如:

for (int i = 0; i < values.Count; ++i)
{
  if (i == values.Count - 1)
  {
    // do something with last element
  }
  else
  {
    // do something else
  }
}

But sometimes I need to do a variation of this with an IEnumerable<T> . 但是有时我需要使用IEnumerable<T>对此进行变体。 For example: 例如:

public static Bar TransformFoo(Foo value)
{
  if (isLast /* how do we know this? */)
  {
    // do something with the last element
  }
  else
  {
    // do something else
  }    
}

public static IEnumerable<Bar> TransformFooSequence(IEnumerable<Foo> source)
{
  return source.Select(TransformFoo);
}

Since this is a common pattern, I'd like to solve it in a general way (rather than write a custom for loop for each case, as I have in the past). 由于这是一种常见的模式,所以我想以一种通用的方式解决它(而不是像过去那样为每种情况编写自定义的for循环)。 One option would be to convert the sequence to a list using ToList() or count the elements using Count() . 一种选择是使用ToList()将序列转换为列表,或者使用Count()对元素进行Count() The problem in both cases is that the solution involves evaluating the entire sequence, which can be prohibitively expensive. 两种情况下的问题都是解决方案涉及评估整个序列,这可能会非常昂贵。

So the question is, how can I evaluate an IEnumerable<T> sequence while giving special treatment to its first and/or last element while maintaining lazy evaluation of that sequence in a general way? 因此,问题是,如何在对IEnumerable<T>序列进行第一个和/或最后一个元素特殊处理的同时,以一种通用的方式保持对该序列的惰性评估,而又如何对其进行评估?

One way to solve this is to create a new extension method for IEnumerable<T> that returns the elements of the source sequence along with semantic information about their positions. 解决此问题的一种方法是为IEnumerable<T>创建新的扩展方法,该方法将返回源序列的元素以及有关其位置的语义信息。 If the elements of the source sequence have type T , then the extension method will return tuples of type (T, PositionFlags) . 如果源序列的元素的类型为T ,则扩展方法将返回类型为(T, PositionFlags)元组。 Here's the code: 这是代码:

[Flags]
enum PositionFlags
{
    None = 0,
    First = 1,
    Last = 2
}    

public static IEnumerable<(T value, PositionFlags flags)> WithPositions<T>(
    this IEnumerable<T> source)
{
    using (var enumerator = source.GetEnumerator())
    {
        if (!enumerator.MoveNext())
        {
            yield break;
        }

        T value = enumerator.Current;
        PositionFlags flags = PositionFlags.First;

        while (enumerator.MoveNext())
        {
            yield return (value, flags);

            value = enumerator.Current;
            flags = PositionFlags.None;
        }

        flags |= PositionFlags.Last;

        yield return (value, flags);
    }
}

We can then pass along the position information to give special treatment to the first and/or last item in a sequence. 然后,我们可以传递位置信息以对序列中的第一项和/或最后一项进行特殊处理。 For example: 例如:

Bar TransformFoo(Foo value, bool isLast)
{
    if (isLast)
    {
        // do something with the last element
     }
    else
    {
        // do something else
    }     
}

IEnumerable<Bar> TransformFooSequence(IEnumerable<Foo> source)
{
  return source
      .WithPositions()
      .Select(entry => TransformFoo(
          entry.value,
          (entry.flags & PositionFlags.Last) == PositionFlags.Last));
}

There is an Enumerable.Select method that passes an element's index into the selector Func along with the element itself. 有一个Enumerable.Select方法,它将元素的索引与元素本身一起传递到选择器Func You can pass the current index into the function along with the total items (or last index value) so that the function has the information required to handle the special items. 您可以将当前索引与总项(或最后一个索引值)一起传递给函数,以便函数具有处理特殊项所需的信息。

var source = {some IEnumerable<Foo>};
var count = source.Count();

source.Select( ( item, i ) => TransformFoo( item, i, count ) );

public static Bar TransformFoo( Foo item, int index, int totalItems )
{
    if( 0 == index )
    {
        // first item handling
    }
    else if( ( index + 1 ) == totalItems )
    {
        // last item handling
    }
    else
    {
        // default item handling
    }
}

Or calculate the first/last flags in the select expression: 或计算选择表达式中的第一个/最后一个标志:

source.Select( ( item, i ) => TransformFoo( item, i == 0, i == ( totalItems - 1 ) );

public static Bar TransformFoo( Foo item, bool isFirst, bool isLast )
...

An IEnumerable<> may be an unordered set. IEnumerable<>可以是无序集合。 Properly designed, your function should require an IList<> to indicate that the input will be treated as an ordered set. 经过适当设计,您的函数应要求IList<>以指示输入将被视为有序集。

If for some reason you must expose the parameter as an IEnumerable<> , in 99% of cases you should simply convert it to a List<> for processing. 如果由于某种原因必须将参数公开为IEnumerable<> ,则在99%的情况下,应将其简单地转换为List<>进行处理。 Your team should be focusing on its core business rather than writing the most fancy code for little details like this one. 您的团队应该专注于其核心业务,而不是为此类细节编写最花哨的代码。

If you have landed in an amazingly rare situation where you must take an input parameter that is defined as an IEnumerable<> AND it is extremely costly to convert to a list, you can use the enumerator directly to get the elements and treat them any way you want. 如果您遇到了极为罕见的情况,必须使用定义为IEnumerable<>的输入参数,并且转换为列表的成本非常高,则可以直接使用枚举器获取元素并以任何方式对其进行处理你要。

This method will iterate a generic list and call first() , middle() , and last() on the element depending on its position. 此方法将迭代通用列表,并根据元素的位置在元素上调用first()middle()last()

public static bool DoSomething<T>(IEnumerable<T> source, Action<T> first, Action<T> middle, Action<T> last)
{
    T current = default(T);
    var enumerator = source.GetEnumerator();
    bool ok = enumerator.MoveNext();
    if (!ok) return false; //There were no elements
    var firstElement = enumerator.Current;
    ok = enumerator.MoveNext();
    if (!ok) return false; //There was only 1 element
    first(firstElement);
    while (ok)
    {
        current = enumerator.Current;
        ok = enumerator.MoveNext();
        if (ok) middle(current);
    }
    last(current);
    return true; 
}

Example calling it: 调用它的示例:

DoSomething
(
    myList,
    e => Console.WriteLine(string.Format("First: {0}", e)),  //This will run only for the first element
    e => Console.WriteLine(string.Format("Middle: {0}", e)), //This will be output several times
    e => Console.WriteLine(string.Format("Last: {0}", e))  //This will run only on the last element
);

This method handles any type and allows you to pass a delegate for each of the first, middle, and last elements. 此方法可以处理任何类型,并允许您为第一个,中间和最后一个元素传递一个委托。 You didn't say how to handle the case if there aren't enough elements (you need three or more). 如果没有足够的元素(您需要三个或更多),您没有说如何处理该案件。 In this example, if the set doesn't have enough elements to process in this fashion, none of them are processed, and the method returns false . 在此示例中,如果集合中没有足够的元素以这种方式处理,则不会处理任何元素,并且该方法返回false Otherwise it returns true. 否则,它返回true。

You can see it run with several test cases in my working example on DotNetFiddle 您可以在DotNetFiddle上的工作示例中看到它运行有几个测试用例

I would probably wrap this sort of logic in a class. 我可能会将此类逻辑包装在一个类中。 For example, if you were writing a piece of code to process large flat files that had a header, detail, and footer section, maybe you'd write a base class like this, and inherit your file processors from it: 例如,如果您正在编写一段代码来处理具有页眉,详细信息和页脚节的大型平面文件,则可能要编写这样的基类,并从中继承文件处理器:

internal abstract class ReportBase
{
    protected readonly IEnumerable<string> _file;

    public ReportBase(IEnumerable<string> file)
    {
        _file = file;   
    }

    public bool Process()
    {
        return ProcessInternal(_file, ProcessHeader, ProcessDetail, ProcessFooter);
    }

    protected bool ProcessInternal<T>(IEnumerable<T> source, Action<T> first, Action<T> middle, Action<T> last)
    {
        T current = default(T);
        var enumerator = source.GetEnumerator();
        bool ok = enumerator.MoveNext();
        if (!ok) return false; //There were no elements
        var firstElement = enumerator.Current;
        ok = enumerator.MoveNext();
        if (!ok) return false; //There was only 1 element
        first(firstElement);
        while (ok)
        {
            current = enumerator.Current;
            ok = enumerator.MoveNext();
            if (ok) middle(current);
        }
        last(current);
        return true; //At l
    }

    abstract protected void ProcessHeader(string header);

    abstract protected void ProcessDetail(string header);

    abstract protected void ProcessFooter(string header);
}

On the other hand, you could avoid this whole mess if you asked the caller to pass you the elements separately, since it presumably knows which is which. 另一方面,如果您要求调用者分别向您传递元素,则可以避免整个混乱,因为它大概知道哪个是哪个。

public void KeepItSimpleStupid<T>(T firstItem, IEnumerable<T> middleItems, T lastItem)
{
    firstItem.Foo();
    foreach (var item in middleItems) item.Bar();
    lastItem.Foo();
}

Alternative to handle the last element separately : 另一种方法是分别处理最后一个元素:

var last = source.Aggregate((previous, current) => {
    HandleNotLast(previous); 
    return current; }); 

HandleLast(last); 

There isn't easy way to handle both first and last separately using LINQ extensions. 没有一个简单的方法可以使用LINQ扩展分别处理第一个和最后一个。 For that you will need enumerator. 为此,您将需要枚举器。 There are MoreLINQ extensions such as TagFirstLast , but most of them seem to get the count of elements before enumerating. 有诸如TagFirstLast类的MoreLINQ扩展,但是其中大多数似乎在枚举之前已经得到了元素的数量。

You wrote: 你写了:

public static Bar TransformFoo(Foo value)
{
    if (isLast /* how do we know this? */)

You can't know this, because you didn't specify that Foo is always an object in a sequence 您不知道这一点,因为您没有指定Foo始终是序列中的对象

You could change the meaning of Foo into: a Foo is an object that ..., which is always a part of a sequence of Foos , in which case it would be expected that every Foo has functionality to get access to sequence that it belongs to. 您可以将Foo的含义更改为: Foo是...的对象,它始终是Foos序列的一部分 ,在这种情况下,可以预期每个Foo都具有访问其所属序列的功能。至。

class Foo
{
    ...
    IEnumerable<Foo> GetSequence(); // returns the sequence that this Foo belongs to
}

If you think it is quite normal for Foo objects not to be part of a sequence, than you should not add this function. 如果您认为Foo对象不属于序列是很正常的,则不应添加此功能。 Instead you should add functionality to the sequence of Foos . 相反,您应该向Foos序列添加功能。

Let's extend IEnumerable with a new LINQ function. 让我们用新的LINQ函数扩展IEnumerable See Extension Methods Demystified 参见神秘的扩展方法

public static IEnumerable<TResult> Transform<TSource, Tresult>(
   this IEnumerable<TSource> source,
   Func<TSource, TResult> transformFuncNonLastElement,
   Func<TSource, TResult> transformFuncLansElement)
{
     // for every element: check if it is the last one,
     // if not, yield return transformFuncNonLastElement
     // if last: yield return transformFuncLastElement
     IEnumerator<TSource> enumerator = source.GetEnumerator();

     if (enumerator.MoveNext())
     {   // There is at least one element.
         TSource current = enumerator.Current;

         // while current is not the last one: transformFuncNonLastElement
         while (enumerator.MoveNext())
         {
             // current is not the last one
             TResult transformedNonLastValue = transformFuncNonLastElement(current);
             yield return transformedNonLastValue;

             current = enumerator.Current;
         }

         // if here: there are no more elements. current is the last one
         TResult transformedLastValue = transformFuncLastElement(current);
         yield return transformedLastValue;
     }
     // else: input sequence empty: return empty
}

Usage: 用法:

IEnumerable<Foo> myFoos = ...
IEnumerable<Bar> result = myFoors.Transform(
   foo => ToNonLastBar(foo),
   foo => ToLastBar(foo));

Where: 哪里:

Bar ToNonLastBar(Foo foo) {...}
Bar ToLastBar(Foo foo) {...}

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM