如何使用流畅的界面构建序列?

How to build a sequence using a fluent interface?

我正在尝试使用流畅的界面来构建一个集合,类似于这个(简化的)示例:

   var a = StartWith(1).Add(2).Add(3).Add(4).ToArray();
   /* a = int[] {1,2,3,4};  */

我能想到的最佳解决方案是将 Add() 添加为:

  IEnumerable<T> Add<T>(this IEnumerable<T> coll, T item)
  {
     foreach(var t in coll) yield return t;
     yield return item;
  }

这似乎增加了很多开销,每次调用都会重复这些开销。

有没有更好的方法?

更新: 在匆忙中,我过度简化了这个例子,遗漏了一个重要的要求。现有 coll 中的最后一项会影响下一项。所以,一个稍微不那么简单的例子:

   var a = StartWith(1).Times10Plus(2).Times10Plus(3).Times10Plus(4).ToArray();
   /* a = int[] {1,12,123,1234};  */

public static IEnumerable<T> StartWith<T>(T x)
{
    yield return x;
}

static public  IEnumerable<int> Times10Plus(this IEnumerable<int> coll, int item)
{
    int last = 0;
    foreach (var t in coll)
    {
        last = t;
        yield return t;
    }
    yield return last * 10 + item;
}

您可以执行以下操作:

public static class MySequenceExtensions
{
    public static IReadOnlyList<int> Times10Plus(
        this IReadOnlyList<int> sequence, 
        int value) => Add(sequence, 
                          value,
                          v => sequence[sequence.Count - 1] * 10 + v);

    public static IReadOnlyList<T> Starts<T>(this T first)
        => new MySequence<T>(first);

    public static IReadOnlyList<T> Add<T>(
        this IReadOnlyList<T> sequence,
        T item,
        Func<T, T> func)
    {
        var mySequence = sequence as MySequence<T> ?? 
                         new MySequence<T>(sequence);
        return mySequence.AddItem(item, func);
    }

    private class MySequence<T>: IReadOnlyList<T>
    {
        private readonly List<T> innerList;

        public MySequence(T item)
        {
            innerList = new List<T>();
            innerList.Add(item);
        }

        public MySequence(IEnumerable<T> items)
        {
            innerList = new List<T>(items);
        }

        public T this[int index] => innerList[index];
        public int Count => innerList.Count;

        public MySequence<T> AddItem(T item, Func<T, T> func)
        {
            Debug.Assert(innerList.Count > 0);
            innerList.Add(func(item));
            return this;
        }

        public IEnumerator<T> GetEnumerator() => innerList.GetEnumerator();
        IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
    }
}

请注意,我使用 IReadOnlyList 可以以高效的方式对列表进行索引,并能够在需要时获取最后一个元素。如果您需要惰性枚举,那么我认为您坚持了最初的想法。

果然,以下内容:

var a = 1.Starts().Times10Plus(2).Times10Plus(3).Times10Plus(4).ToArray();

产生预期的结果 ({1, 12, 123, 1234}),我认为是合理的性能。

你可以这样做:

public interface ISequence
{
    ISequenceOp StartWith(int i);
}

public interface ISequenceOp
{
    ISequenceOp Times10Plus(int i);
    int[] ToArray();
}

public class Sequence : ISequence
{
    public ISequenceOp StartWith(int i)
    {
        return new SequenceOp(i);
    }
}

public class SequenceOp : ISequenceOp
{
    public List<int> Sequence { get; set; }

    public SequenceOp(int startValue)
    {
        Sequence = new List<int> { startValue };
    }

    public ISequenceOp Times10Plus(int i)
    {
        Sequence.Add(Sequence.Last() * 10 + i);
        return this;
    }

    public int[] ToArray()
    {
        return Sequence.ToArray();
    }
}

然后就是:

var x = new Sequence();
var a = x.StartWith(1).Times10Plus(2).Times10Plus(3).Times10Plus(4).ToArray();

这个聚会有点晚了,但这里有一些想法。

首先,考虑解决更一般的问题:

public static IEnumerable<A> AggregateSequence<S, A>(
  this IEnumerable<S> items,
  A initial,
  Func<A, R, A> f) 
{
  A accumulator = initial;
  yield return accumulator;
  foreach(S item in items)
  {
    accumulator = f(accumulator, item);
    yield return accumulator;
  }
}

现在你的程序只是 new[]{2, 3, 4}.AggregateSequence(1, (a, s) => a * 10 + s).ToArray()

然而,它缺少您想要的 "fluency",它假定 相同的 操作应用于序列中的每个元素。

你注意到深度嵌套的迭代器块是有问题的,你是对的;它们在时间和堆栈的线性消耗方面具有二次性能,两者都很糟糕。

这是一种有效实施解决方案的有趣方式。

问题是您需要 both 便宜地访问序列的 "right" 末端,以便对 most 进行操作最近添加了 元素,但您还需要廉价访问序列的 left 末尾以枚举它。普通队列和堆栈只能廉价访问 one 端。

因此:首先要实现一个高效的不可变双端队列。这是一个 迷人的 数据类型;我这里有一个使用手指树的实现:

https://blogs.msdn.microsoft.com/ericlippert/2008/01/22/immutability-in-c-part-10-a-double-ended-queue/

https://blogs.msdn.microsoft.com/ericlippert/2008/02/12/immutability-in-c-part-eleven-a-working-double-ended-queue/

一旦你有了它,你的操作就是一行:

static IDeque<T> StartWith<T>(T t) => Deque<T>.Empty.EnqueueRight(t);
static IDeque<T> Op<T>(this IDeque<T> d, Func<T, T> f) => d.EnqueueRight(f(d.PeekRight()));
static IDeque<int> Times10Plus(this IDeque<int> d, int j) => d.Op(i => i * 10 + j);

修改 IDeque<T>Deque<T> 以明显的方式实现 IEnumerable<T> 然后您可以免费获得 ToArray。或者作为扩展方法:

static IEnumerable<T> EnumerateFromLeft(this IDeque<T> d)
{
  var c = d;
  while (!c.IsEmpty) 
  { 
    yield return c.PeekLeft(); 
    c = c.DequeueLeft(); 
  }
}