【问题标题】:Converting nested foreach loops to LINQ将嵌套的 foreach 循环转换为 LINQ
【发布时间】:2011-06-07 15:48:32
【问题描述】:

我编写了以下代码来设置各种类的属性。它有效,但我的新年解决方案之一是尽可能多地使用 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; }
}

【问题讨论】:

  • 我对 .Net 并不熟悉,但我广泛使用它的同事对一个名为 ReSharper(JetBrains 出品)的工具赞不绝口,它有助于将 foreach 循环重构为 Linq 语句,以及许多其他美妙的东西.通过查看 ReSharper 的建议,他们对 Linq 有了更深入和更好的理解。可能想试试看?
  • @kander JFYI,我刚试过 Resharper。它不建议将其转换为 LINQ。

标签: c# linq foreach


【解决方案1】:

我不认为这实际上是转换为 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,而不是在每次迭代时对字典进行两次查找。

【讨论】:

    【解决方案2】:

    老实说,我没有阅读您的代码。对我来说,当您说“设置属性的代码”时,您的问题就得到了回答。您不应该使用 LINQ 来改变对象的状态/产生副作用。是的,我知道您可以编写会导致这种情况发生的扩展方法,但是您会滥用 LINQ 提供的功能范式,并可能造成维护负担,尤其是对于可能找不到任何书籍的其他开发人员或支持您的努力的文章。

    【讨论】:

      【解决方案3】:

      由于您有兴趣尽可能多地使用 Linq,您可能想尝试 VS 插件ReSharper。它将识别可以转换为 Linq 运算符的循环(或循环的一部分)。它还对 Linq 做了很多其他有用的事情。

      例如,将求和值的循环转换为使用Sum,将应用内部过滤器的循环更改为使用Where。甚至对象上的字符串连接或其他递归也会转换为Aggregate。通过尝试它所建议的更改,我了解了更多关于 Linq 的信息。

      加上 ReSharper 很棒,还有大约 1000 个其他原因 :)

      【讨论】:

        【解决方案4】:

        正如其他人所说,如果没有 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;
        

        【讨论】:

          【解决方案5】:

          这个呢

              (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);
          

          【讨论】:

            【解决方案6】:

            看看这是否有效

              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;
                    }
                }
            

            【讨论】:

              猜你喜欢
              • 2017-02-19
              • 1970-01-01
              • 2020-11-26
              • 2019-11-22
              • 1970-01-01
              • 1970-01-01
              • 1970-01-01
              • 2023-03-17
              • 1970-01-01
              相关资源
              最近更新 更多