Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

IEnumerable - Update objects inside foreach loop

I have a really simple program that creates a bunch of objects and iterates through them to set each object's Priority property.

static void Main(string[] args)
{
    foreach (var obj in ObjectCreator.CreateObjectsWithPriorities())
        Console.WriteLine(String.Format("Object #{0} has priority {1}",
                                         obj.Id, obj.Priority));
}

class ObjectCreator
{
    public static IEnumerable<ObjectWithPriority> CreateObjectsWithPriorities()
    {
        var objs = new[] { 1, 2, 3 }.Select(i => new ObjectWithPriority() { Id = i });
        ApplyPriorities(objs);
        return objs;
    }

    static void ApplyPriorities(IEnumerable<ObjectWithPriority> objs)
    {
        foreach (var obj in objs)
        {
            obj.Priority = obj.Id * 10;
            Console.WriteLine(String.Format("Set priority of object #{0} to {1}", obj.Id, obj.Priority));
        }
    }
}

class ObjectWithPriority
{
    public int Id { get; set; }
    public int Priority { get; set; }
}

I'm expecting the IEnumerable in the Main method to contain objects with modified priorities. However, all of them have the default value 0. Here is the log:

Set priority of object #1 to 10 
Set priority of object #2 to 20 
Set priority of object #3 to 30 
Object #1 has priority 0 
Object #2 has priority 0 
Object #3 has priority 0

What is the reason for suche behavior and what should I change here in order to get my priorities working?

like image 709
Andre Borges Avatar asked Dec 14 '15 13:12

Andre Borges


2 Answers

When you do this:

var objs = new[] { 1, 2, 3 }.Select(i => new ObjectWithPriority() { Id = i });

You're simply creating a lazily evaluated iterator, this doesn't allocate an array/list to store the ObjectWithPriorty you project. Each time you enumerate the iterator, it will iterate the values again and project an ObjectWithPriority for each iteration, but will discard them.

What you want to do is materialize the query before you pass it, so later you'll actually modifying the already allocated list/array. This can be achieved using Enumerable.ToList or Enumerable.ToArray:

public static IEnumerable<ObjectWithPriority> CreateObjectsWithPriorities()
{
    var objs = new[] { 1, 2, 3 }.Select(i => new ObjectWithPriority() { Id = i })
                                .ToList();
    ApplyPriorities(objs);
    return objs;
}

You could additional use Enumerable.Range instead of allocating a fixed size array, which will lazily project the numbers as requested:

var objs = Enumerable.Range(1, 3).Select(i => new ObjectWithPriority { Id = i })
                                 .ToList();
like image 111
Yuval Itzchakov Avatar answered Nov 14 '22 11:11

Yuval Itzchakov


To better understand what is happening in your program, you should think of this expression

var objs = new[] { 1, 2, 3 }.Select(i => new ObjectWithPriority() { Id = i });

as a query, not as a sequence/list/collection of objects.

But it is obvious from your code that in this particular program you don't need a query. You need a collection that has a finite number of objects and that returns the same objects every time you loop through it using foreach.

So a decent thing to do would be to use ICollection<ObjectWithPriority> instead of IEnumerable<ObjectWithPriority>. This would better represent the program's logic and help avoid some mistakes/misinterpretations like the one you stumbled upon.

The code could be modified as follows:

public static ICollection<ObjectWithPriority> CreateObjectsWithPriorities()
{
    IEnumerable<ObjectWithPriority> queryThatProducesObjectsWithPriorities = new[] { 1, 2, 3 }.Select(i => new ObjectWithPriority() { Id = i }); // just for clarification
    ICollection<ObjectWithPriority> objectsWithPriorities = queryThatProducesObjectsWithPriorities.ToList();
    ApplyPriorities(objectsWithPriorities);
    return objectsWithPriorities;
}

static void ApplyPriorities(ICollection<ObjectWithPriority> objs)
{
    foreach (var obj in objs)
    {
        obj.Priority = obj.Id * 10;
        Console.WriteLine(String.Format("Set priority of object #{0} to {1}", obj.Id, obj.Priority));
    }
}
like image 43
holdenmcgrohen Avatar answered Nov 14 '22 10:11

holdenmcgrohen