将嵌套的foreach循环转换为LINQ

3
我已经编写了以下代码来设置各个类的属性。它可以工作,但我新年的决心是尽可能多地使用LINQ,而这段代码显然不是。有没有一种方法可以用“纯LINQ”格式重写它,最好不使用foreach循环?(如果可以在一个LINQ语句中完成,那就更好了 - 子语句也可以。)
我试着玩join,但没什么效果,所以我要求回答这个问题 - 最好不要解释,因为我更愿意“反编译”解决方案以弄清其工作原理。 (你可能已经猜到,我现在比编写LINQ代码更擅长阅读它,但我打算改变这一点……)
 public void PopulateBlueprints(IEnumerable<Blueprint> blueprints)
 {
   XElement items = GetItems();
   // item id => name mappings
   var itemsDictionary = (
     from item in items
     select new
     {
       Id = Convert.ToUInt32(item.Attribute("id").Value),
       Name = item.Attribute("name").Value,
     }).Distinct().ToDictionary(pair => pair.Id, pair => pair.Name);

  foreach (var blueprint in blueprints)
  {
    foreach (var material in blueprint.Input.Keys)
    {
      if (itemsDictionary.ContainsKey(material.Id))
      {
        material.Name = itemsDictionary[material.Id];
      }
      else
      {
        Console.WriteLine("m: " + material.Id);
      }
    }

    if (itemsDictionary.ContainsKey(blueprint.Output.Id))
    {
      blueprint.Output.Name = itemsDictionary[blueprint.Output.Id];
    }
    else
    {
      Console.WriteLine("b: " + blueprint.Output.Id);
    }
  }
}

下面是必需类的定义; 它们仅仅是数据的容器,我已经去除了与我的问题无关的所有部分:

public class Material
{
  public uint Id { get; set; }

  public string Name { get; set; }
}

public class Product
{
  public uint Id { get; set; }

  public string Name { get; set; }
}

public class Blueprint
{
  public IDictionary<Material, uint> Input { get; set; }

  public Product Output { get; set; }
}

1
我不熟悉 .Net,但是我的同事们经常使用一个叫做 ReSharper(由 JetBrains 开发)的工具,并对其赞不绝口。该工具可以帮助将 foreach 循环重构为 Linq 语句,还有许多其他很棒的功能。通过查看 ReSharper 建议的内容,他们对 Linq 的理解更加深入和全面。你可能想要尝试一下这个工具? - kander
@kander 顺便说一下,我刚试了一下Resharper。它没有建议将其转换为LINQ。 - Harvey Kwok
6个回答

6

我认为这并不是适合转换为LINQ的好例子 - 至少在它目前的形式下不是。

是的,你有一个嵌套的foreach循环 - 但你在顶层foreach循环中做了其他事情,所以它不是易于转换的形式,它只包含嵌套。

更重要的是,你的代码主体都涉及到副作用,无论是写入控制台还是改变你找到的对象中的值。当你有一个复杂的查询并想要循环遍历每个项进行操作时,可能会带有副作用,此时LINQ非常有用...但你的查询其实并不复杂,所以你不会得到太多好处。

你可以做的一件事是给BlueprintProduct提供一个包含IdName的公共接口。然后,你可以编写一个单独的方法通过itemsDictionary来更新产品和蓝图,基于每个查询:

UpdateNames(itemsDictionary, blueprints);
UpdateNames(itemsDictionary, blueprints.SelectMany(x => x.Input.Keys));

...

private static void UpdateNames<TSource>(Dictionary<string, string> idMap,
    IEnumerable<TSource> source) where TSource : INameAndId
{
    foreach (TSource item in source)
    {
        string name;
        if (idMap.TryGetValue(item.Id, out name))
        {
            item.Name = name;
        }
    }
}

假设您实际上不需要控制台输出。如果需要,您可以始终传入适当的前缀并在方法中添加“else”块。请注意,我已经使用了TryGetValue而不是为每次迭代在字典上执行两次查找。

3

说实话,我没有阅读你的代码。对我来说,当你说“编写代码来设置属性”时,你的问题已经回答了自己。你不应该使用LINQ来改变对象状态或产生副作用。是的,我知道你可以编写扩展方法来实现这一点,但你会滥用LINQ所提倡的函数式范式,并可能为其他开发人员创建维护负担,特别是那些可能找不到任何支持你努力的书籍或文章的开发人员。


1

如果您对使用Linq做尽可能多的事情感兴趣,那么您可能想尝试一下VS插件ReSharper。它将识别可以转换为Linq运算符的循环(或循环部分)。它还可以在Linq中执行其他有用的操作。

例如,将值求和的循环转换为使用Sum,并将应用内部过滤器的循环更改为使用Where。甚至将字符串连接或对象上的其他递归转换为Aggregate。我从尝试它建议的更改中学到了更多关于Linq的知识。

此外,ReSharper还有大约1000个其他原因使其变得非常棒 :)


0

正如其他人所说,您可能不想在没有foreach循环的情况下完成它。循环表示副作用,这是整个练习的重点。话虽如此,您仍然可以使用LINQ:

  var materialNames =
      from blueprint in blueprints
      from material in blueprint.Input.Keys
      where itemsDictionary.ContainsKey(material.Id)
      select new { material, name = itemsDictionary[material.Id] };

  foreach (var update in materialNames)
      update.material.Name = update.name;

  var outputNames =
      from blueprint in blueprints
      where itemsDictionary.ContainsKey(blueprint.Output.Id)
      select new { blueprint, name = itemsDictionary[blueprint.Output.Id] };

  foreach (var update in outputNames)
      update.Output.Name = update.name;

0

这个怎么样?

    (from blueprint in blueprints
     from material in blueprint.Input.Keys
     where itemsDictionary.ContainsKey(material.Id)
     select new { material, name = itemsDictionary[material.Id] })
     .ToList()
     .ForEach(rs => rs.material.Name = rs.name);

    (from blueprint in blueprints
     where itemsDictionary.ContainsKey(blueprint.Output.Id)
     select new { blueprint, name = itemsDictionary[blueprint.Output.Id] })
     .ToList()
     .ForEach(rs => rs.blueprint.Output.Name = rs.name);

0

看看这个是否工作

  var res = from blueprint in blueprints
     from material in blueprint.Input.Keys
     join  item in items on 
     material.Id equals Convert.ToUInt32(item.Attribute("id").Value)
     select material.Set(x=> { Name = item.Attribute("id").Value; });

您不会找到set方法,因为已经创建了一个扩展方法来实现它。

 public static class LinqExtensions
    {
        /// <summary>
        /// Used to modify properties of an object returned from a LINQ query
        /// </summary>
        public static TSource Set<TSource>(this TSource input,
            Action<TSource> updater)
        {
            updater(input);
            return input;
        }
    }

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