【问题标题】:Set null value to closest last non-null value in a list - LINQ将空值设置为列表中最接近的最后一个非空值 - LINQ
【发布时间】:2020-10-28 19:12:41
【问题描述】:

我有一个 DataPoint 对象列表(只读),其中一些具有值,而另一些为空。我想生成一个新的 DataPoint 对象列表,其中任何 null DataPoint 都设置为最接近的先前非 null 值(左侧)。如果空值之前没有非空值,则默认为 0。

在下面的示例中,前 2 个空值变为 0,因为它们之前没有非空值,最后两个空值变为 5,因为 5 是最靠近它们左侧的非空值。

    public class DataPoint
    {
        public DataPoint(int inputValue)
        {
            this.Value = inputValue;
        }
        
        public int Value {get;}
    }

Input:

    List<DataPoint> inputList = new List<DataPoint>
            {null, 
             null, 
             new DataPoint(1), 
             new DataPoint(2), 
             new DataPoint(3), 
             null, 
             null, 
             new DataPoint(4), 
             new DataPoint(5), 
             null, 
             null};

Expected Output:

    foreach (var item in outputList)
    {
        Console.WriteLine(item.Value);
    }

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

我能否了解如何在 LINQ 中以优雅的方式实现这一目标?谢谢

更新:为避免歧义,我已将 inputList 更新为包含 null,而不是包含 null 值的 DataPoint 实例。

【问题讨论】:

  • 最后两个 null 转换为 5 不符合您的规则,它们没有以前的非 null 值。编辑 - 等等我可能读错了。是的。没关系。
  • 你应该添加你到目前为止尝试过的内容。
  • 为什么一定要使用 linq?使用它似乎不是一个场景
  • ^ +1,为什么要使用 LINQ?您是否需要延迟执行,或者您只是对如何将 LINQ 应用于此用例感到好奇?
  • 在 LINQ 中无法优雅地实现这一点,我指的是现有的内置 LINQ 方法或语法。

标签: c# linq


【解决方案1】:

使用辅助扩展方法,它是我的 APL 扫描运算符的 LINQ 实现的变体(如 Aggregate,但返回中间结果),它使用辅助函数来启动结果流:

// First PrevResult is TRes seedFn(T FirstValue)
// TRes combineFn(TRes PrevResult, T CurValue)
public static IEnumerable<TRes> Scan<T, TRes>(this IEnumerable<T> items, Func<T, TRes> seedFn, Func<TRes, T, TRes> combineFn) {
    using (var itemsEnum = items.GetEnumerator()) {
        if (itemsEnum.MoveNext()) {
            var prev = seedFn(itemsEnum.Current);

            while (itemsEnum.MoveNext()) {
                yield return prev;
                prev = combineFn(prev, itemsEnum.Current);
            }
            yield return prev;
        }
    }
}

您可以沿初始List&lt;DataPoint&gt; 扫描并返回之前的结果以查找任何空值,使用第一个值进行初始化,如果为空,则返回 0:

var ans = InputList.Scan(firstDP => firstDP ?? 0, (prevRes, curDP) => curDP ?? prevRes).ToList();

注意:如果您不想使用辅助方法,并且愿意通过使用外部状态(例如辅助变量)来稍微滥用 LINQ,您可以这样做:

var prevNonNull = new DataPoint(0);
var ans2 = InputList.Select(n => prevNonNull = n ?? prevNonNull).ToList();

【讨论】:

  • 您的ScanPairWithHelper 非常适合此类应用程序。竖起大拇指。我的答案中的.Take(i) 将非常庞大。
  • @asawyer 简化了我的回答 :) 想了想之后,我意识到帮助者太过分了——标准的 Scan 带有之前的结果(很像 Aggregate)就是这样需要。
  • 我们的生活中都需要更多的 APL。
  • @buntuoba 让 linq 方法改变状态被认为是“一种不好的做法”,这有点违背 linq 的设计理念。但这里没有性能下降或类似情况。
  • @buntuoba 是的,第一个 linq 中的示例确实是一个不好的做法,没有引号。我同意 Eric - 想要改变状态,使用循环。引入 linq 并不是说​​循环突然变得过时了。然而,在你的例子中,它并不是那么简单,因为你想产生一个新的集合,本着 linq 的精神,恰好选择算法需要一个临时变量(至少在最直接的方法中)
【解决方案2】:

你可以试试这样的:

 static void Main(string[] args)
    {
        List<int?> inputList = new List<int?>() { null, null, 1, 2, 3, null, null, 4, 5, null, null };
        var result = Enumerable.Range(0, inputList.Count - 1)
            .Select(i => inputList[i] ?? GetPrevious(i))
            .ToList();

        int GetPrevious(int index)
            => index == 0 ? 0 : inputList[index - 1] ?? GetPrevious(index - 1);
    }

【讨论】:

  • 很好的答案先生!
  • 谢谢,好主意!
【解决方案3】:

假设DataPoint.Value 的实际属性类型是int? 而不是int,这样的事情应该可以工作。

var outputList = inputList.Select((l,i)=> new DataPoint()
{
    Value = l?.Value ?? inputList.Take(i).LastOrDefault(t=>t?.Value.HasValue ?? false)?.Value ?? 0
});

我没有检查,但我确信性能特征很糟糕。

完整的 linqpad -

void Main()
{
    var inputList = new List<DataPoint>()
    {
        null, null, 1, 2, 3, null, null, 4, 5, null, null
    };
    var outputList = inputList.Select((l,i)=> new DataPoint()
    {
        Value = l?.Value ?? inputList.Take(i).LastOrDefault(t=>t?.Value.HasValue ?? false)?.Value ?? 0
    });
    outputList.Dump();
}

public class DataPoint
{
    public int? Value { get; set; }
    //added to make building the inputList easier
    public static implicit operator DataPoint(int? value) => 
        new DataPoint(){ Value = value };
}

输出

IEnumerable<DataPoint> (11 items)
0
0
1
2
3
3
4
5
5
5

如果 DataPoint.Value 实际上是 int 并且 inputList 包含空值,而不是具有空值的 DataPoint 实例,则需要稍作调整:

var outputList = inputList.Select((l,i)=> new DataPoint()
{
    Value = l?.Value ?? inputList.Take(i).LastOrDefault(t=>t!=null)?.Value ?? 0
});

...
public static implicit operator DataPoint(int? value) 
    => value.HasValue ? new DataPoint(){ Value = value } : (DataPoint)null;
...

【讨论】:

  • 感谢您添加初始化代码以使其有效和边缘案例场景!
猜你喜欢
  • 1970-01-01
  • 2018-05-14
  • 2015-07-22
  • 1970-01-01
  • 2015-09-17
  • 1970-01-01
  • 2017-07-19
  • 1970-01-01
  • 2020-06-08
相关资源
最近更新 更多