【问题标题】:C# Select Overload Method With Accumulator Not WorkingC#选择累加器不工作的重载方法
【发布时间】:2017-07-07 02:54:30
【问题描述】:

我在 C# 中创建了一个 Map 函数,以在许多方面发挥作用,因为它在 JavaScript 中等同于项目对象类型。从那以后,我将这些方法重命名为“选择”以用作重载,因此它们感觉更“集成”。这是一个链条,请耐心等待,但受影响的功能看起来像这样......

    public static TResult Project<TInput, TResult>(this TInput input, Func<TInput, TResult> projectionMapping)
        => projectionMapping(input);

    public static TResult Project<TInput, TAccumulatedValue, TIncrementingValue, TResult>(this TInput input, Func<TInput, TAccumulatedValue, TResult> projectionMapping, 
        Func<TAccumulatedValue, TIncrementingValue, TAccumulatedValue> accumulator, TAccumulatedValue initialAccumulatorValue, TIncrementingValue increment)
        => projectionMapping(input, accumulator(initialAccumulatorValue, increment));

    public static IEnumerable<TResult> Select<TInput, TAccumulatedValue, TIncrementingValue, TResult>(this IEnumerable<TInput> input,
        Func<TInput, TAccumulatedValue, TResult> projectionMapping, Func<TAccumulatedValue, TIncrementingValue, TAccumulatedValue> accumulator, 
        TAccumulatedValue initialAccumulatorValue, TIncrementingValue increment)
        => input.Select(item => item.Project(projectionMapping, accumulator, initialAccumulatorValue, increment));

    // This doesn't work.
    public static IEnumerable<TResult> Select<TInput, TResult>(this IEnumerable<TInput> input,
        Func<TInput, int, TResult> projectionMapping, int initialAccumulatorValue = -1, int increment = 1)
    {
        return input.Select(projectionMapping, (acc, inc) => acc + inc,
            initialAccumulatorValue, increment);
    }

我用的是int版本的map方法,里面写了累加器,如下……

    MyList.Add(new List<MyObject>(rowValues.Map((val, headerNumber) 
            => new MyObject(headerNumber, val), 0, 10)));

问题是,headerNumber 的值永远不会改变(它总是 10) - 累加器运行一次,然后为每个 Mapping 运行,但它不记得它在运行之间的累积。我觉得我在这里遗漏了一些明显的东西,但我看不到树木的树木。

如果我输入(例如)这样的数组...

rowValues = new string[] { "Item 1", "Item 2", "Item 3" };

我希望有一个包含以下数据的 MyObject 项目列表...

  • 10 | “第 1 项”
  • 20 | “第 2 项”
  • 30 | “第 3 项”

【问题讨论】:

标签: c# linq functional-programming


【解决方案1】:

假设如下

public class MyObject
{
    public int Row { get; }
    public string Value { get; }

    public MyObject(int row, string value)
    {
        Value = value;
        Row = row;
    }
}

输入源为:

var rows = new[] { "Item 1", "Item 2", "Item 3", "Item 4" };

解决方案

要为您提供的输入实现特定结果,您可以简单地使用框架中提供的Select

var result = rows.Select((v, i) => new MyObject((i+1)*10, v)).ToList();

当然,这看起来不太好,但确实有效。

Ivan 的回答更简洁,但我会坚持上面的内容,以防有人觉得它有用。

【讨论】:

  • 看起来您没有阅读我关于关闭和Select 的帖子的结尾 :) 您的第二个和第三个选项都因关闭未重新初始化而受到影响,这可以通过枚举(或ToList-ing) result2result3 变量两次。
  • @IvanStoev Touché!我没有考虑多次迭代。我将从我的答案中删除这些选项,因为它们可能会产生很大的误导。感谢您提请我注意。
  • 这可能适用于超过一半的情况,将来需要这样的东西。我的必须对 int 和 DateTimes 进行奇怪的排序,因此是通用的核心方法。
【解决方案2】:

我首先更改了您的第三个函数,以便它只需要一个累加器函数,该函数返回序列中的下一个索引。这允许函数具有您需要计算累加器值的状态。

public static IEnumerable<TResult> Project<TInput, TAccumulatorValue, TResult>(this IEnumerable<TInput> input,
    Func<TInput, TAccumulatorValue, TResult> projectionMapping, 
    Func<TAccumulatorValue> accumulator)
{
    return input.Select(item => projectionMapping(item, accumulator()));
}

那么你的函数接受了不起作用的范围参数可以这样写,这解决了你的问题。

public static IEnumerable<TResult> Project<TInput, TResult>(this IEnumerable<TInput> input,
    Func<TInput, int, TResult> projectionMapping, int initialAccumulatorValue = 0, int increment = 1)
{
    int curValue = initialAccumulatorValue;
    return input.Project(projectionMapping, 
        () => { var ret = curValue; curValue += increment; return ret; });
}

或者

以不同的方式思考这个问题,您可以使它更通用。您真正要做的就是使用 projectionMapping 将两个序列组合在一起以组合元素。在这种情况下,第二个序列恰好包含累加器值。然后要使用它,您只需使用标准的 Linq 函数 Zip,传入累加器序列和 projectionMapping 函数。

要获得线性序列,我们可以使用 Enumerable.Range,但要获得非线性范围,我们需要编写一个像这样的 Range 生成器

public static IEnumerable<int> Range(int start, int increment)
{
    for (; ; )
    {
        yield return start;
        start += increment;
    }
}

示例

显示两种解决方案的实际效果

var items = new[] { "a", "b", "c" };

// use Project, returns: a10, b20, c30
var results = items.Project((s, i) => s + i.ToString(), 10, 10).ToList();

// use Zip with custom range, returns: a10, b20, c30
var results2 = items.Zip(Range(10, 10), (s, i) => s + i.ToString()).ToList();

// use Zip with standard range, returns: a1, b2, c3
var results3 = items.Zip(Enumerable.Range(1, int.MaxValue), (s, i) => s + i.ToString()).ToList();

【讨论】:

  • Zip 对我来说是一个新功能 - 这看起来像是另一个非常巧妙的解决方案
【解决方案3】:

问题是你总是用initialAccumulatorValue 调用accumulator

为了达到目的,你需要维护累加值,而最简单正确的方法是使用C#迭代器方法:

public static IEnumerable<TResult> Map<TInput, TAccumulatedValue, TIncrementingValue, TResult>(this IEnumerable<TInput> input,
    Func<TInput, TAccumulatedValue, TResult> projectionMapping, Func<TAccumulatedValue, TIncrementingValue, TAccumulatedValue> accumulator,
    TAccumulatedValue initialAccumulatorValue, TIncrementingValue increment)
{
    var accumulatedValue = initialAccumulatorValue;
    foreach (var item in input)
        yield return projectionMapping(item, accumulatedValue = accumulator(accumulatedValue, increment));
}

请注意,天真的尝试使用闭包和Select的组合

var accumulatedValue = initialAccumulatorValue;
return input.Select(item => projectionMapping(item, accumulatedValue = accumulator(accumulatedValue, increment)));

根本不起作用,因为accumulatedValue 将由返回的select 查询的多次执行共享,因此它们将产生不正确的结果。迭代器方法没有这样的问题,因为代码实际上是在任何时候调用GetEnumerator() 方法时执行的。

【讨论】:

  • 我相信这是我的头一直在撞的墙。谢谢伊万。我会试一试,看看它是否适合我。
  • 这对我有用 - 该方法现在就像一个魅力一样,作为一个额外的 Select 重载,它需要一个累加器,并且我有整数和时间变体工作。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2022-11-24
  • 2016-03-24
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多