代码之家  ›  专栏  ›  技术社区  ›  Peter Ruderman

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

  •  2
  • Peter Ruderman  · 技术社区  · 7 年前

    这是一个更一般形式的问题,我怎样才能对列表的第一个和最后一个元素做一些特殊的事情?更具体的问题很容易回答。我们知道第一个和最后一个元素的索引,所以我们可以直接访问它们,或者根据这些值测试索引变量。例如:

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

    但有时我需要用一个 IEnumerable<T> . 例如:

    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);
    }
    

    因为这是一个常见的模式,所以我想用一种通用的方法来解决它(而不是像过去那样为每个情况编写一个定制的for循环)。一种方法是使用 ToList() 或者使用 Count() . 这两种情况下的问题是,解决方案涉及评估整个序列,这可能会非常昂贵。

    所以问题是,我如何评估 IEnumerable<T> 在对其第一个和/或最后一个元素进行特殊处理的同时保持对该序列的惰性评估?

    5 回复  |  直到 7 年前
        1
  •  3
  •   spender    7 年前

    解决这个问题的一种方法是为 IEnumerable<T> 返回源序列的元素及其位置的语义信息。如果源序列的元素具有类型 T ,则扩展方法将返回类型为的元组 (T, PositionFlags)

    [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);
        }
    }
    

    然后我们可以传递位置信息,对序列中的第一个和/或最后一个项进行特殊处理。例如:

    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));
    }
    
        2
  •  1
  •   Moho    7 年前

    Func 以及元素本身。可以将当前索引与总项(或最后一个索引值)一起传递到函数中,以便函数具有处理特殊项所需的信息。

    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
        }
    }
    

    source.Select( ( item, i ) => TransformFoo( item, i == 0, i == ( totalItems - 1 ) );
    
    public static Bar TransformFoo( Foo item, bool isFirst, bool isLast )
    ...
    
        3
  •  1
  •   John Wu    7 年前

    IEnumerable<> 可能是无序集。如果设计得当,您的功能应该需要 IList<>

    如果出于某种原因,必须将参数公开为 IEnumerable<&燃气轮机; ,在99%的情况下,您只需将其转换为 List<>

    如果您遇到了一个非常罕见的情况,您必须接受一个定义为 IEnumerable<&燃气轮机;

    此方法将迭代泛型列表并调用 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; 
    }
    

    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
    );
    

    此方法处理任何类型,并允许您为第一个、中间个和最后一个元素传递委托。你没有说如果没有足够的元素(你需要三个或更多)如何处理这个案子。在本例中,如果集合没有足够的元素以这种方式处理,则不会处理任何元素,并且方法将返回 false . 否则返回true。

    在我的工作示例中,您可以看到它与几个测试用例一起运行 DotNetFiddle

    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);
    }
    

    另一方面,如果您让调用者分别将元素传递给您,则可以避免整个混乱,因为它可能知道哪个是哪个。

    public void KeepItSimpleStupid<T>(T firstItem, IEnumerable<T> middleItems, T lastItem)
    {
        firstItem.Foo();
        foreach (var item in middleItems) item.Bar();
        lastItem.Foo();
    }
    
        4
  •  0
  •   Slai    7 年前

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

    使用LINQ扩展不容易分别处理第一个和最后一个。为此,您将需要枚举器。还有更多的linq扩展,比如 TagFirstLast

        5
  •  0
  •   Harald Coppoolse    7 年前

    你写道:

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

    您不可能知道这一点,因为您没有指定Foo始终是序列中的对象

    你可以把Foo的意思改成: ,在这种情况下 Foo

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

    如果你认为这是很正常的 Foos .

    IEnumerable 使用新的LINQ函数。看到了吗 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
    }
    

    用法:

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

    哪里:

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