How to Make List<T>.ForEach Modify the List

by Larry Spencer Tuesday, July 24, 2012 3:34 PM

Here's a gotcha that's obvious once you see it, but was the cause of a bug that lived for several months undetected in code I came across today.

Do you think this code will print modified elements or the original values?

 

var stringList = new List<string>(new[] { "a", "b", "c" });

// What effect does this have?
stringList.ForEach(element => element = element + " modified");

// Let's see!
stringList.ForEach(element => Console.WriteLine(element));

 

Here's the output. It's not what we wanted, is it!?

 

a
b
c

 

ForEach's parameter is an Action<T>. Each element in the list is passed to the Action, which can do something to the element but cannot replace it with a new value.

So how about this?

 

class MyType
{
    public string MyString { get; set; }

    public MyType(string s) 
    {
        MyString = s;
    }
}

class Program
{
    static void Main(string[] args)
    {
        var myList = new List<MyType>(new[] { new MyType("a"), new MyType("b"), new MyType("c") });

        // What effect does this have?
        myList.ForEach(element => element.MyString = element.MyString + " modified");

        // Let's see!
        myList.ForEach(element => Console.WriteLine(element.MyString));
    }
}

 

This time, we modified elements rather than attempting to replace them and we get what we'd expect:

 

a modified
b modified
c modified

 

What if we change MyType from a class to a struct, keeping the rest of the program the same? 

 

struct MyType
{
    public string MyString { get; set; }

    public MyType(string s) : this()
    {
        MyString = s;
    }
}

class Program
{
    static void Main(string[] args)
    {
        var myList = new List<MyType>(new[] { new MyType("a"), new MyType("b"), new MyType("c") });

        // What effect does this have?
        myList.ForEach(element => element.MyString = element.MyString + " modified");

        // Let's see!
        myList.ForEach(element => Console.WriteLine(element.MyString));
    }
}

 

Because a struct is a value type, a copy of each element, not a reference, gets passed to the Action and the modification is done on the copy. When the Action returns, the original is unchanged. Here's what prints.

 

a
b
c

 

A technique that will work for all of the above cases must involve making a new list. Here's a way that uses LINQ's Aggregate method (like my trick for comma-separating a List). I like this because it embeds the working list in the Aggregate statement and does not expose it to the surrounding code.

 

struct MyType
{
    public string MyString { get; set; }

    public MyType(string s) : this()
    {
        MyString = s;
    }
}

class Program
{
    static void Main(string[] args)
    {
        var myList = new List<MyType>(new[] { new MyType("a"), new MyType("b"), new MyType("c") });

        myList = myList.Aggregate(
            new List<MyType>(),
            (newList, element) => 
                {
                    element.MyString = element.MyString + " modified";
                    newList.Add(element);
                    return newList;
                });

        myList.ForEach(element => Console.WriteLine(element.MyString));
    }
}

 

Voila!

 

a modified
b modified
c modified

Tags:

All

Comments (3) -

7/24/2012 10:07:00 PM #

I liked this article, I think it makes clear a common mistake new-comers to Linq will often make.

The more "Linqy" way of doing things would be with select()

   stringList.ConvertAll(element => element + " modified");

Of course this does not return a List<string>, you could tack a ToList() on the end.  Or you could use ConvertAll like this:

   stringList.ConvertAll(element => element + " modified");

and of course to make other references to stringList point to the new list:

   stringList = stringList.ConvertAll(element => element + " modified");


Hogan United States

7/25/2012 5:24:31 PM #

@Hogan - ConvertAll is a great alternative!

Ironically, even though it is LINQy (as you said) it is not a LINQ method but a method of List<T>. The technique with Aggregate will work with any IEnumerable<T>, the title of my post notwithstanding.

Larry Spencer United States

7/30/2012 9:26:27 AM #

I just saw a typo in my post, the first code should have been

     stringList.Select(element => element + " modified");

(Which works with other IEnumerable types besides List<T>)

Also, check out this SO answer (http://stackoverflow.com/a/11673428/215752)

Inspired by your posts I used Aggregate to to give (what I believe to be) the most elegant and LINQy answer.

Hogan Long United States

Add comment

About the Author

Larry Spencer

Larry Spencer develops software with the Microsoft .NET Framework for ScerIS, a document-management company in Sudbury, MA.