开发者

Converting nested foreach loops to LINQ

I've written the following code to set the properties on various classes. It works, but one of my new year's rsolutions is to make as much use of LINQ as possible and obviously this code doesn't. Is there a way to rewrite it in a "pure LINQ" format, preferably without using the foreach loops? (Even better if it can be done in a single LINQ statement - substatements are fine.)

I tried playing around with join but that didn't get me anywhere, hence I'm asking for an answer to this question - preferably without an explanation, as I'd prefer to "decompile" the solution to figure out how it works. (As you can probably guess I'm currently a lot better at reading LINQ than writing it, but I intend to change that...)

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

Definition of th开发者_如何学编程e requisite classes follow; they are merely containers for data and I've stripped out all the bits irrelevant to my question:

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


I don't think this is actually a good candidate for conversion to LINQ - at least not in its current form.

Yes, you have a nested foreach loop - but you're doing something else in the top-level foreach loop, so it's not the easy-to-convert form which just contains nesting.

More importantly, the body of your code is all about side-effects, whether that's writing to the console or changing the values within the objects you've found. LINQ is great when you've got a complicated query and you want to loop over that to act on each item in turn, possibly with side-effects... but your queries aren't really complicated, so you wouldn't get much benefit.

One thing you could do is give Blueprint and Product a common interface containing Id and Name. Then you could write a single method to update the products and blueprints via itemsDictionary based on a query for each:

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

This is assuming you don't actually need the console output. If you do, you could always pass in the appropriate prefix and add an "else" block in the method. Note that I've used TryGetValue instead of performing two lookups on the dictionary for each iteration.


I'll be honest, I did not read your code. For me, your question answered itself when you said "code to set the properties." You should not be using LINQ to alter the state of objects / having side effects. Yes, I know that you could write extension methods that would cause that to happen, but you'd be abusing the functional paradigm poised by LINQ, and possibly creating a maintenance burden, especially for other developers who probably won't be finding any books or articles supporting your endeaver.


As you're interested in doing as much as possible with Linq, you might like to try the VS plugin ReSharper. It will identify loops (or portions of loops) that can be converted to Linq operators. It does a bunch of other helpful stuff with Linq too.

For example, loops that sum values are converted to use Sum, and loops that apply an internal filter are changed to use Where. Even string concatenation or other recursion on an object is converted to Aggregate. I've learned more about Linq from trying the changes it suggests.

Plus ReSharper is awesome for about 1000 other reasons as well :)


As others have said, you probably don't want to do it without foreach loops. The loops signify side-effects, which is the whole point of the exercise. That said, you can still LINQ it up:

  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;


What about this

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


See if this works

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

You wont find set method, for that there is an extension method created.

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

上一篇:

下一篇:

精彩评论

暂无评论...
验证码 换一张
取 消

最新问答

问答排行榜