Follow

Keep Up to Date with the Most Important News

By pressing the Subscribe button, you confirm that you have read and are agreeing to our Privacy Policy and Terms of Use
Contact

Passing instance-tied method to list foreach

Compare the following uses of ForEach to grow a bounding box from the contained points (Unity, but that’s immaterial). One works, the other compiles fine but fails.

Good – explicit lambda:

Bounds ret = new(center, Vector3.zero);
endPositions.ForEach(pos => ret.Encapsulate(pos)); // endPositions is List<Vector3>
return ret;

Bad – passing a method group (I think):

MEDevel.com: Open-source for Healthcare and Education

Collecting and validating open-source software for healthcare, education, enterprise, development, medical imaging, medical records, and digital pathology.

Visit Medevel

Bounds ret = new(center, Vector3.zero);
endPositions.ForEach(ret.Encapsulate);
return ret;

I would expect the second one to do the same as the first, as it refers to a method on an instance. But it just silently leaves ret as it was. The one difference I see is that on the second form, the parameter to ForEach is a method group rather than a specific overload.

The overload resolution set is this:

public void Encapsulate(Vector3 point);
public void Encapsulate(Bounds bounds);

However, from the definition of List<T>.ForEach(Action<T>) I would expect that only the 1st method, accepting Vector3, would be suitable. However, it seems like the second one (accepting Bounds) is chosen instead.

My only theory is that it has to do with how and where the this parameter is bound, but that’s where I’m stuck. What, exactly, is going on here?

>Solution :

This is such an interesting problem. I went ahead and created a sample that highlights the difference here. Turns out, what you are seeing is due to the fact that Bounds is a value-type. It has nothing to do with the 2 different overloads with Bounds and Vector3.

Take a look at this sample code:

List<int> numbers = [1, 2, 3];

var myStruct = new MyStruct();
numbers.ForEach(myStruct.Accumulate);
Console.WriteLine($"Method group struct sum: {myStruct.Sum}");

var myStruct2 = new MyStruct();
numbers.ForEach(number => myStruct2.Accumulate(number));
Console.WriteLine($"Lambda struct sum: {myStruct2.Sum}");

var myClass = new MyClass();
numbers.ForEach(myClass.Accumulate);
Console.WriteLine($"Method group class sum: {myClass.Sum}");

var myClass2 = new MyClass();
numbers.ForEach(number => myClass2.Accumulate(number));
Console.WriteLine($"Lambda class sum: {myClass2.Sum}");

public struct MyStruct
{
    public int Sum;

    public void Accumulate(int value) => this.Sum += value;
}

public class MyClass
{
    public int Sum;

    public void Accumulate(int value) => this.Sum += value;
}

The results are as follows:

Method group struct sum: 0

Lambda struct sum: 6

Method group class sum: 6

Lambda class sum: 6

When the instance is a struct, there is a difference between passing the call as a method group vs a direct lambda, whereas when using a class it works the same. Apparently, when passing via method group, the struct is passed by value so it is copied and you don’t get the aggregated results as they were aggregated in a separate copied instance. When you pass it via a lambda, the lambda captures the instance by reference (probably boxing it) and you do get the updated results back.

Obviously, when using a class none of this applies, since it is always passed by reference.

What puzzles me the most in this though is that Visual Studio actually suggests passing via method group in all cases:

enter image description here

So there is a static analysis bug right there… they are definitely not the same.

I didn’t know of this difference between method group vs lambda when using value types myself so this was an interesting exercise.

Looks like you should keep the lambda approach for your use case there.

EDIT:
Confirmed. The IDE0200 suggestion above is a known bug and is documented here:

Add a comment

Leave a Reply

Keep Up to Date with the Most Important News

By pressing the Subscribe button, you confirm that you have read and are agreeing to our Privacy Policy and Terms of Use

Discover more from Dev solutions

Subscribe now to keep reading and get access to the full archive.

Continue reading