-1

Consider this code

class Program
{
    static void Main(string[] args)
    {
        var lst = new List<MyObj>()
        {
            new MyObj() {name = "Old1", weight = 1},
            new MyObj() {name = "Old2", weight = 2}
        };

        lst.Join(GetList(), p => p.weight, q => q, (p, q) =>
        {
            p.name = "new";
            return p;
        });

        lst.ForEach(p => Console.WriteLine(p.name));
    }
    public class MyObj
    {
        public string name { get; set; }
        public int weight { get; set; }
    }

    public static List<int> GetList()
    {
        return new List<int>() {1,2};
    }
}

The output of this code is Old1, Old2. I don't understand why the "name" is not updated inside the resultSelector function. This is my thinking: MyObj instance is a reference type, we are joining it, and collecting it in variable p (which I guess is a reference type, i.e. contains the reference to the original MyObj). So, inside the function to create result, it would update the "name" property (as again a reference would be passed) in the memory location where the original MyObj exists. Why isn't this the case? Where am I going wrong?

Pavel Anikhouski
  • 21,776
  • 12
  • 51
  • 66
Sahil Gupta
  • 103
  • 9
  • Calling LINQ methods like `Join()` or `Where()` without actually using the return value results in no effect. You have to _evaluate_ the result before anything actually happens. See duplicate. See also topics like https://stackoverflow.com/questions/17699877/how-does-the-following-linq-statement-work and https://stackoverflow.com/questions/51972590/ienumerable-performs-differently-on-array-vs-list – Peter Duniho Apr 14 '21 at 05:59

2 Answers2

2

You didn't assigned the Join result to any variable, but it returns IEnumerable<T>. Try to update your code a little bit, save the result of join and iterate over it

var result = lst.Join(GetList(), p => p.weight, q => q, (p, q) =>
{
    p.name = "new";
    return p;
}).ToList();

result.ForEach(p => Console.WriteLine(p.name));

In your current code you look through the source lst, which remains unchanged.

If you want to see an updated name and changes in original objects, just materialize your query using ToList() (because Join implemented by using a deferred execution) to get a changes made by p.name = "new";

lst.Join(GetList(), p => p.weight, q => q, (p, q) =>
{
    p.name = "new";
    return p;
}).ToList();
Pavel Anikhouski
  • 21,776
  • 12
  • 51
  • 66
  • 1
    I did see this while fixing the code, but I still don't understand this behavior. If a reference to the original objects is passed to the result selector function, why won't the data in memory at those locations update without catching the result, like you show above? – Sahil Gupta Jun 16 '20 at 20:05
  • 1
    @SahilGupta adding `.ToList()` will make the expected behaviour, because `Join` returns `IEnumerable`, which should be materialized – Pavel Anikhouski Jun 16 '20 at 20:10
  • Wow! This is magic for me :D. Is linq doing some sort of lazy evaluations under the hood? It seems like .ToList() forces it to run the computations. – Sahil Gupta Jun 16 '20 at 20:14
  • @SahilGupta Methods in C# are pass-by-value, regardless of wether it's of a reference type or not (unless marked with the ref keyword). In the case of a reference type, the method will simply create a copy of the reference, so it can't override the original reference with a new assignment. It would be possible for a method of it's type to modify the underlying structure howerever. The reason it isn't implemented in this case, is because ```Enumerable.Join``` is an extension method. A *static* method not part of the class, that can be used with the dot notation. [...] – Jirajha Jun 16 '20 at 20:15
  • @SahilGupta [...] If it was implemented with changing the parameter, it would violate the Single Responsibility principle of object-oriented programming. Or rather: It could violate it in most circumstances. – Jirajha Jun 16 '20 at 20:15
  • 1
    @Jirajha I am not sure what you mean by, "Methods in C# are pass-by-value". I am coming from this: https://jonskeet.uk/csharp/parameters.html. This article says there are no pass-by-values in C# (contrary to what you might have in C++). Sure, the method is going to create a copy (a new reference basically), but to the same underlying object (in the memory). Moreover, I am also not sure what you mean by .Join being an extension method as what PavelAnikhouski said seems to work. Can you point me to any links related to this? I am relatively new to C# and also advanced OOP. Thanks! – Sahil Gupta Jun 16 '20 at 20:21
  • 1
    Linq indeed does lazy evaluations: https://www.softwire.com/blog/2012/08/06/lazy-linqing/index.html – Sahil Gupta Jun 16 '20 at 20:24
  • @SahilGupta let's tackle the extension method first, because it's what's actually at play here: Normally you would have to call ```System.Linq.Enumerable.Join(list1, list2);``` If you look at the documentation of the method https://learn.microsoft.com/en-us/dotnet/api/system.linq.enumerable.join , you will find it's *first* parameter marked with the ```this``` keyword. It''s a static method in a static class. This allows you to write ```list1.Join(list2);```. https://learn.microsoft.com/dotnet/csharp/programming-guide/classes-and-structs/extension-methods – Jirajha Jun 16 '20 at 20:46
  • Let us [continue this discussion in chat](https://chat.stackoverflow.com/rooms/216090/discussion-between-jirajha-and-sahil-gupta). – Jirajha Jun 16 '20 at 20:55
0

The Join() method returns a new IEnumerable<>, it does not modify the original list.

https://learn.microsoft.com/en-us/dotnet/api/system.linq.enumerable.join?view=netcore-3.1

marc_s
  • 732,580
  • 175
  • 1,330
  • 1,459
Jacob Huckins
  • 368
  • 2
  • 15