将空值设置为列表中最接近的最后一个非空值 - LINQ

Posted

技术标签:

【中文标题】将空值设置为列表中最接近的最后一个非空值 - LINQ【英文标题】:Set null value to closest last non-null value in a list - 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 方法或语法。 【参考方案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;
...

【讨论】:

感谢您添加初始化代码以使其有效和边缘案例场景!

以上是关于将空值设置为列表中最接近的最后一个非空值 - LINQ的主要内容,如果未能解决你的问题,请参考以下文章

如何使用 LINQ 将空值发送到 ASP.NET 中的非空列

如何将空值设置为结构的默认值

sql 查询时有空值返回0怎么写

Prometheus 来源的时间序列:如何将空值设置为零?

将空值设置为缺失表字段的默认值

如何将空值附加到列表的开头? [复制]