基于LINQ中的函数,将IEnumerable<T>分割成IEnumerable<IEnumerable<T>>?

5
我希望使用LINQ在C#中将一个序列分成多个子序列。我已经进行了一些调查,并且找到了与此略微相关的最接近的SO文章是这篇文章
然而,这个问题只是询问如何基于常量值对原始序列进行分区。我想根据操作来分割我的序列。
具体来说,我有一个包含十进制属性的对象列表。
public class ExampleClass
{
    public decimal TheValue { get; set; }
}

假设我有一系列的ExampleClass,而对应的TheValue值的序列是:

{0,1,2,3,1,1,4,6,7,0,1,0,2,3,5,7,6,5,4,3,2,1}

我想将原始序列划分为一个IEnumerable<IEnumerable<ExampleClass>>,其中TheValue的值类似于:
{{0,1,2,3}, {1,1,4,6,7}, {0,1}, {0,2,3,5,7}, {6,5,4,3,2,1}}

我不知道如何实现这个。所以,你能帮忙吗?

目前我的解决方案非常丑陋,但是我有一种“感觉”,LINQ会使我的代码更加优雅。


1
它是什么使得它以那种方式对值进行分区?除了它们都是升序序列直到最后一个之外,似乎没有任何明显的推理。你是在寻找单调序列吗? - Jon Skeet
1
@jon- 我试图解决的问题是基于TheValue改变方向来对原始序列进行分区。例如,子序列只能按值递增或保持不变,或按值递减或保持不变。在这种情况下,集合{0,1,0,4,5}不是合法的子序列。明白我的意思吗? - a developer
1
是的,这正是我想做的——创建一个单调序列。此外,Jon,感谢您在这个帖子上发表评论。你的书很棒。 :D - a developer
@Jon:是的,那就是我想说的,谢谢。 - Dyppl
@generalt:没错,知道方向变化是改变的信号是重要的一部分 :) 我会回答... - Jon Skeet
显示剩余7条评论
3个回答

6

好的,我认为我们可以做到这一点...


public static IEnumerable<IEnumerable<TElement>>
    PartitionMontonically<TElement, TKey>
    (this IEnumerable<TElement> source,
     Func<TElement, TKey> selector)
{
    // TODO: Argument validation and custom comparisons
    Comparer<TKey> keyComparer = Comparer<TKey>.Default;

    using (var iterator = source.GetEnumerator())
    {
        if (!iterator.MoveNext())
        {
            yield break;
        }
        TKey currentKey = selector(iterator.Current);
        List<TElement> currentList = new List<TElement> { iterator.Current };
        int sign = 0;
        while (iterator.MoveNext())
        {
            TElement element = iterator.Current;
            TKey key = selector(element);
            int nextSign = Math.Sign(keyComparer.Compare(currentKey, key));

            // Haven't decided a direction yet
            if (sign == 0)
            {
                sign = nextSign;
                currentList.Add(element);
            }
            // Same direction or no change
            else if (sign == nextSign || nextSign == 0)
            {
                currentList.Add(element);
            }
            else // Change in direction: yield current list and start a new one
            {
                yield return currentList;
                currentList = new List<TElement> { element };
                sign = 0;
            }
            currentKey = key;
        }
        yield return currentList;
    }
}

完全未经测试,但我认为它可能有效...

工作得非常好。谢谢Jon!同时,今天我还学到了关于Comparer<T>.Default的内容,太棒了。 - a developer
第21行需要将comparer.Compare更改为keyComparer.Compare才能编译,否则运行良好。今天学到了新东西。 - Kilanash
@Kilanash:谢谢,已修复。这就是匆忙写代码的问题,坐在即将到站的火车上,不费心去尝试编译和运行 :) - Jon Skeet

1

或者使用linq运算符和一些对.net闭包的引用滥用。

public static IEnumerable<IEnumerable<T>> Monotonic<T>(this IEnumerable<T> enumerable)
{
  var comparator = Comparer<T>.Default;
  int i = 0;
  T last = default(T);
  return enumerable.GroupBy((value) => { i = comparator.Compare(value, last) > 0 ? i : i+1; last = value; return i; }).Select((group) => group.Select((_) => _));
}

从一些随机的实用程序代码中获取,用于将IEnumerable分区为临时表以进行日志记录。如果我记得正确,奇怪的结尾Select是为了防止输入为字符串枚举时出现歧义。

1
这是一个自定义的LINQ运算符,可以根据几乎任何条件拆分序列。它的参数如下:
  1. xs:输入元素序列。
  2. func:一个接受“当前”输入元素和状态对象的函数,并返回一个元组:
    • 一个布尔值,指示是否在“当前”元素之前拆分输入序列;以及
    • 将传递给下一次调用func的状态对象。
  3. initialState:在其第一次调用func时传递给它的状态对象。
这是它的代码,以及一个辅助类(必需的,因为yield return显然不能嵌套):
public static IEnumerable<IEnumerable<T>> Split<T, TState>(
                  this IEnumerable<T> xs,
                  Func<T, TState, Tuple<bool, TState>> func, 
                  TState initialState)
{
    using (var splitter = new Splitter<T, TState>(xs, func, initialState))
    {
        while (splitter.HasNext)
        {
            yield return splitter.GetNext();
        }
    }
}

internal sealed class Splitter<T, TState> : IDisposable
{
    public Splitter(IEnumerable<T> xs, 
                    Func<T, TState, Tuple<bool, TState>> func, 
                    TState initialState)
    {
        this.xs = xs.GetEnumerator();
        this.func = func;
        this.state = initialState;
        this.hasNext = this.xs.MoveNext();
    }

    private readonly IEnumerator<T> xs;
    private readonly Func<T, TState, Tuple<bool, TState>> func;
    private bool hasNext;
    private TState state;

    public bool HasNext { get { return hasNext; } }

    public IEnumerable<T> GetNext()
    {
        while (hasNext)
        {
            Tuple<bool, TState> decision = func(xs.Current, state);
            state = decision.Item2;
            if (decision.Item1) yield break;
            yield return xs.Current;
            hasNext = xs.MoveNext();
        }
    }

    public void Dispose() { xs.Dispose(); }
}

注意:以下是关于Split方法的一些设计决策:

  • 它应该只在序列上进行一次遍历。
  • 状态被明确表示,以便将副作用排除在func之外。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接