将空值设置为列表中最接近的最后一个非空值 - 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<DataPoint>
扫描并返回之前的结果以查找任何空值,使用第一个值进行初始化,如果为空,则返回 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的主要内容,如果未能解决你的问题,请参考以下文章