使用LINQ更改列表中对象的属性

18

我有一个 Beam 对象列表。如何使用 LINQ 在 Width 属性大于 40 时更改梁的 IsJoist 属性?

class Beam
{
    public double Width { get; set; }
    public bool IsJoist { get; set; }
}

var bm1 = new Beam { Width = 40 };
var bm2 = new Beam { Width = 50 };
var bm3 = new Beam { Width = 30 };
var bm4 = new Beam { Width = 60 };

var Beams = new List<Beam> { bm1, bm2, bm3, bm4 };

这是我做过的,但我只得到了一个列表。我希望新列表与原始列表相同,除了某些横梁的IsJoist属性将被设置为true。

var result = Beams
    .Where(x => x.Width > 40)
    .Select(x => x.IsJoist = true)
    .ToList();

我已经按照以下方式实现了这个功能。由于 LINQ 是用于查询的,所以这样做是否可以?

var result = Beams
    .Where(x => x.Width > 40)
    .Select(x =>
    {
        x.IsJoist = true;
        return x;
    })
    .ToList();

1
你不能这样做。这些方法的整个重点就在于它们是功能性的,也就是说它们没有副作用。除非你在List<T>类型上使用“破坏性”的方法ForEach - Simon Whitehead
@SimonWhitehead 我其实找到了答案,它可以工作,但我不知道这是否是良好的实践? - Vahid
7
绝对不要使用select来充当update,这是一个极其糟糕的编程实践。使用LINQ来“提问”,而不是“进行更改”。如果您想做出更改,请使用foreach循环。 - Eric Lippert
@EricLippert 谢谢Eric,这是我最关心的问题,因为使用LINQ来做这样的事情似乎非常违反直觉。 - Vahid
6个回答

36
如果您的解决方案必须完全使用Linq,您可以这样做:
Beams.Where(x => x.Width > 40).ToList().ForEach(b => b.IsJoist = true);

然而,这不是实现此操作的理想方式(@Jacob的回答更好)。请参阅Eric Lippert关于此主题的博客文章。对我来说最重要的几行是:
第一个原因是这样做违反了所有其他序列运算符基于的函数式编程原则。显然,调用此方法的唯一目的是引起副作用。表达式的目的是计算值,而不是引起副作用。语句的目的是引起副作用。

https://ericlippert.com/2009/05/18/foreach-vs-foreach/

请注意,调用ToList()是因为List<T>提供了ForEach()方法,而Linq通常不提供这样的扩展方法,原因在Eric Lippert在那篇博客文章中阐述过。 更新 您的代码既更新了原始列表中的实体(对于某些条件将IsJoist更改为true),又返回对已更新对象的引用。如果这是您想要的功能,则代码可以正常工作。但是,Linq是以函数式编程为设计思想。在Linq表达式的上下文中引入副作用违反了扩展方法背后的函数式编程原则。

谢谢。你能看一下我的实现吗? - Vahid
1
+1 给 Lippert 先生的这句话。我记不得是谁说过类似的话,所以在我的评论中无法链接到它。 - Simon Whitehead
感谢Eric的澄清。 - Vahid

9
foreach(Beam beam in Beams.Where(x => x.Width > 40))
{
     beam.IsJoist = true;
} 

我希望它完全使用LINQ。 - Vahid
4
LINQ仅用于查询,而不是用于更改值。 - Siddharth Pandey
问题在于“beam”被声明为列表中的新变量。是的,值正在改变,但不是列表中的那个值。 - Ewald
不同意。beam是列表中当前对象的引用。如果您通过该引用更改任何属性,则会更改该对象。 - Jacob Seleznev

3
假设我想将特定对象的“Selected”属性值更改为true,那么我可以这样做:
Beams.Where(x => x.Width > 40).FirstorDefault(z=>z.Selected = true)

1
为了实现函数式纯净,你的linq不应该改变正在处理的数据。这意味着你需要使用.Select(x=> new Beam(x) {IsJoist=true})。然后你需要用结果替换原始列表。

1

我找到的最好的方法是使用 new 创建一个对象的副本,并在选择中进行修改。如果有类似 JavaScript 中的 spread 操作符就好了。

下面是一些未经测试的代码,应该能传达这个想法:


public class Cat {
   public string Name;
   public bool IsFerrel;
}

List<Cat> cats = new{
   new{Name="Tame"},
   new{Name="Wild"}
};

return cats.Select(c => new Cat{
  Name = c.Name,
  IsFerrel = c.Name.Contains("Wild")
};




本质上,这是一种不改变对象的功能性方法。

看起来C# 10添加了with运算符,可以以更简洁的形式实现这个功能:https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/operators/with-expression - oglester

0
你可以尝试这个方法来改变列表中所有项的属性(无需任何条件)。
Beams.All(b => b.IsJoist = true);

你不能这样做。 - Guilherme Flores

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接