-2

I have dictionary indices and want to add several keys to it from another dictionary using LINQ.

var indices = new Dictionary<string, int>();
var source = new Dictionary<string, int> { { "1", 1 }, { "2", 2 } };
source.Select(name => indices[name.Key] = 0); // doesn't work
var res = indices.Count; // returns 0

Then I replace Select with Min and everything works as expected, LINQ creates new keys in my dictionary.

source.Min(name => indices[name.Key] = 0); // works!!!
var res = indices.Count; // returns 2

Question

All I want to do is to initialize dictionary without foreach. Why dictionary keys disappear when LINQ is executed? What iterator or aggregator I could use instead of Min to create keys for a dictionary declared outside of LINQ query?

Update #1

Decided to go with System.Interactive extension.

Update #2

I appreciate and upvote all answers, but need to clarify that, purpose of the question is not to copy a dictionary, but to execute some code in a LINQ query. To add more sense to it, I actually have hierarchical structure of classes with dictionaries and at some point they need to be synchronized, so I want to create flat, non-hierarchical dictionary, used for tracking, that includes all hierarchical keys.

class Account
{
   Dictionary<string, User> Users;
}

class User
{
   Dictionary<string, Activity> Activities;
}

class Activity
{
   string Name;
   DateTime Time;
}

Now I want to sync all actions by time, so I need a tracker that will help me to align all actions by time, and I don't want to create 3 loops for Account, User, and Activity. Because that would be considered a hierarchical hell of loops, the same as async or callback hell. With LINQ I don't have to create loop inside loop, inside loop, etc.

Accounts.ForEach(
  account => account.Value.Users.ForEach(
    user => user.Value.Activities.ForEach(
      activity => indices[account.Key + user.Key + activity.Key] = 0));

Also, having loops where it can be replaced with LINQ can be considered as a code smell, not my opinion, but I totally agree, because having too many loops you will probably end up in duplicated code.

https://jasonneylon.wordpress.com/2010/02/23/refactoring-to-linq-part-1-death-to-the-foreach/

You can say that LINQ is used for querying, not for setting a variable, I would say I'm querying ... the KEYS.

Anonymous
  • 1,823
  • 2
  • 35
  • 74
  • 3
    what is wrong with `foreach`? – trailmax Jul 23 '18 at 08:41
  • 3
    `Min` (as well as `ToList`, `Count` etc.) *materializes* the result (you get a value, collection, `int`) when `Select` alone *does nothing* (deferred execution). I doubt if you should avoid plain and easy to read `foreach` but expoit the *side effect* of `Min` – Dmitry Bychenko Jul 23 '18 at 08:48
  • 1
    Go NuGet Microsoft's "System.Interactive" extensions and then you can do `source.ForEach(name => indices[name.Key] = 0);`. – Enigmativity Jul 23 '18 at 08:49
  • Possible duplicate of [Convert Linq Query Result to Dictionary](https://stackoverflow.com/questions/953919/convert-linq-query-result-to-dictionary) – Jimbot Jul 23 '18 at 09:08
  • 1
    `source.Keys.ToList().ForEach(key => indices[key] = 0);` . Or `source.Keys.ForEach(key => indices[key] = 0);` if you add https://www.nuget.org/packages/morelinq/ to your project. – mjwills Jul 23 '18 at 09:17
  • 1
    Interessing question!. Give you 1 upvote – Antoine V Jul 23 '18 at 10:20
  • `All I want to do is to initialize dictionary without foreach` Are you aware that LINQ is effectively using foreach internally? You can't access all members of a collection without iterating over the collection. That's literally the intention of iterating over the collection. – Flater Jul 23 '18 at 12:01
  • Linq is for querying data structures, not for settings values. – Magnus Jul 23 '18 at 12:05

2 Answers2

2

Linq is not intended to be used to mutate the elements of a sequence. Rather, it is intended to be used to traverse, filter and project elements of a sequence. In this respect, it is intended to be used more in a "functional programming" style.

As you have discovered, Linq can be used in other than a functional programming style - but by using it in that way you are really misusing it.

Technically, the reason that source.Min() has the effect you were looking for is that it has to visit each of the elements of your sequence in order to determine the minimum element.

Because your selector for Min() has a side-effect (i.e. indices[name.Key] = 0) then a side-effect of finding the minimum value is to add each element's key to indices, but with a value of zero rather than the original value.

(I suspect you might have meant to put indices[name.Key] = name.Value...)

The reason that your use of Select() has no effect is that it has not been used to traverse the sequence - it uses "deferred execution".

You can force it to traverse the sequence by counting the elements, like so:

source.Select(name => indices[name.Key] = 0).Count();

However, that is also counter-intuitive and is a misuse of Linq.

The correct solution is to use foreach. This expresses your intent clearly and unambiguously.

An alternative approach is to write an AddRange() extension method for Dictionary like so:

public static class DictionaryExt
{
    public static Dictionary<TKey, TValue> AddRange<TKey, TValue>(
        this Dictionary<TKey, TValue> self, 
        IEnumerable<KeyValuePair<TKey, TValue>> items)
    {
        foreach (var item in items)
        {
            self[item.Key] = item.Value;
        }

        return self;
    }
}

Then you can just call indices.AddRange(source); to achieve your aim.

Interestingly, the ImmutableDictionary type does already have an AddRange() method that you could use like so:

var indices = ImmutableDictionary.Create<string, int>();
var source  = new Dictionary<string, int> { { "1", 1 }, { "2", 2 } };

indices = indices.AddRange(source);

Console.WriteLine(indices.Count);

But I wouldn't recommend you change over to using ImmutableDictionary just so you can use its AddRange().

Also note that ImmutableDictionary is, well, immutable - so you can't just do indices.AddRange(source);; you have to assign the result back as in indices = indices.AddRange(source); (like when you modify a string using ToUpper()).

Matthew Watson
  • 104,400
  • 10
  • 158
  • 276
  • I like the explanation, and consider it an answer, but prefer System.Interactive extension as was suggested in comments – Anonymous Jul 23 '18 at 13:51
  • @Anonymous I still think that using `foreach` is the better way to go - [here's a blog post about why a `ForEach` extension method isn't a good idea](https://blogs.msdn.microsoft.com/ericwhite/2009/04/08/why-i-dont-use-the-foreach-extension-method/). But it's up to you. A significant number of people do use such an extension! :) – Matthew Watson Jul 23 '18 at 14:24
2

You wrote:

All I want to do is to initialize dictionary without foreach

Do you want to replace the values in your indices dictionary with the values in source? Use Enumerable.ToDictionary

indices = (KeyValuePair<string, int>)source  // regard the items in the dictionary as KeyValuePairs
    .ToDictionary(pair => pair.Key,          // the key is the key from original dictionary
                  pair => pair.Value);       // the value is the value from the original

Or do you want to add the values from source to the already existing values in indices? If you don't want a foreach you'll have to take the current values from both dictionaries and Concat them to the values from source. Then use the ToDictionary to create a new Dictionary.

indices = (KeyValuePair<string, int>) indices
   .Concat(KeyValuePair<string, int>) source)
   .ToDictionary(... etc)

However this would be a waste of processing power.

Consider creating extension functions for Dictionary. See Extension Methods Demystified

public static Dictionary<TKey, TValue> Copy>Tkey, TValue>(
    this Dictionary<TKey, TValue> source)
{
     return source.ToDictionary(x => x.Key, x => x.Value);
}

public static void AddRange<TKey, TValue>(
    this Dictionary<TKey, TValue> destination,
    Dictionary<TKey, TValue> source)
{
     foreach (var keyValuePair in source)
     {
         destination.Add(keyValuePair.Key, keyValuePair.Value);
         // TODO: decide what to do if Key already in Destination
     }
}

Usage:

// initialize:
var indices = source.Copy();
// add values:
indices.AddRange(otherDictionary);
Harald Coppoolse
  • 28,834
  • 7
  • 67
  • 116