【问题标题】:Partition/split/section IEnumerable<T> into IEnumerable<IEnumerable<T>> based on a function using LINQ?基于使用 LINQ 的函数将 IEnumerable<T> 分区/拆分/部分转换为 IEnumerable<IEnumerable<T>>?
【发布时间】:2011-07-11 16:46:22
【问题描述】:

我想将 C# 中的序列拆分为使用 LINQ 的序列序列。我进行了一些调查,我发现与此稍有关联的最接近的 SO 文章是 this

但是,这个问题只问如何根据常数值对原始序列进行分区。我想根据一个操作对我的序列进行分区。

具体来说,我有一个包含小数属性的对象列表。

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&lt;IEnumerable&lt;ExampleClass&gt;&gt;,其值为TheValue,类似于:

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

我只是不知道如何实施。所以,你能帮忙吗?

我现在有一个非常丑陋的解决方案,但有一种“感觉”,即 LINQ 会增加我的代码的优雅度。

【问题讨论】:

  • 所以你需要把它分成几个单调的子序列?
  • 是什么让它以这种方式划分值?似乎没有任何明显的推理,除了它们都是升序直到最后一个。你追求单调序列吗?
  • 在这种情况下,单调意味着无聊,还是有特定的技术含义?
  • @jon- 我要解决的问题是根据 TheValue 改变其方向来划分原始序列。例如,子序列只能增加或保持相同的值,或者减少或保持相同的值。在这种情况下,集合 {0,1,0,4,5} 不是合法的子序列。这有意义吗?
  • 是的,这正是我想要做的——创建一个单调序列。另外,乔恩,感谢您对此线程的评论。你的书很棒。 :D

标签: linq data-partitioning


【解决方案1】:

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

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

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

【讨论】:

  • 效果很好。谢谢乔恩!另外,关于 Comparer.Default 的 TIL。太棒了。
  • 第 21 行需要将 comparer.Compare 更改为 keyComparer.Compare 才能编译,否则效果非常好。今天学到了一些新东西。
  • @Kilanash:谢谢,已修复。这就是匆忙写代码的问题,在即将到达的火车上,而不是试图编译和运行:)
【解决方案2】:

或者使用 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 是为了防止在输入是字符串枚举时产生歧义。

【讨论】:

    【解决方案3】:

    这是一个自定义 LINQ 运算符,它可以根据几乎任何标准拆分序列。它的参数是:

    1. xs:输入元素序列。
    2. func:接受“当前”输入元素和状态对象并作为元组返回的函数:
      • bool 说明输入序列是否应在“当前”元素之前拆分;和
      • 将传递给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产生副作用。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2012-02-24
      • 2011-02-19
      • 1970-01-01
      • 1970-01-01
      • 2018-08-04
      • 2012-09-17
      相关资源
      最近更新 更多