Foreach loop passing the iteration variable to a method with and without ref - dont get why it wont allow it with ref

Viewed 1015

This morning I've struggled to understand the following and I am hoping someone has a good explanation of why this wont work. I made an example on LinqPad that replicates my issue:

void Main()
{
  var myPeople = new People();
  myPeople.MyPeople.Add(new Person { Name = "Joe", Surname = "Doe" });

  foreach (var person in myPeople.MyPeople)
  {
      //this is intermediate variable is still a reference to the iteration variable
       var intermediate = person;
       myPeople.ChangeName(ref intermediate);

      //both names were changed, so why cant i just pass in 'person' directly since its still changing it.

      intermediate.Name.Dump();
      person.Name.Dump();
  }

}

 public class People
 {
     public People()
     {
        MyPeople = new List<Person>();
     }

    public List<Person> MyPeople { get; set; }

    public void ChangeName(ref Person person)
    {
       person.Name = "Changed";
    }
}

public class Person
{
   public string Name { get; set; }

   public string Surname { get; set; }
}

So this post explains the foreach variable is a read only variable that is why you can't replace it with another- makes sense, but if i pass it into the method above ChangeName with a ref, compiler wont allow it CS1657. But if i remove the ref on the ChangeName method it will allow me to pass in the foreach variable, but technically its still a reference without explicitly saying its a ref right?

So why is this happening? Furthermore if I assign it to an intermediate variable above, then it will allow me to pass it in as a ref even though its still a reference to the original, so technically pointing to the same memory location, meaning same object right?

So my question really is why the C# compiler wont let me pass in iteration variable to a method with a ref

Further clarity on question by Rafalon: Why does the compiler not allow myPeople.ChangeName(ref person) but allows myPeople.ChangeName(ref intermediate) when the person variable points to the same object as the intermediate variable?

2 Answers

Ultimately, the notes on CS1657 explains all of this; the l-value of a regular foreach is considered "readonly" - you can't do:

person = new Person();

for example. The ref usage requires the parameter not be read-only, otherwise we have violated the promise of read-only-less, because the method could modify something that is meant to be read-only. In this case, since Person is a class, this only relates to the actual reference itself, not the underlying object.

There is good news, though: we now have in, which is like ref, but for read-only scenarios, so: change public void ChangeName(ref Person person) to public void ChangeName(in Person person) and it should work:

myPeople.ChangeName(in person);

The restriction on this is that you then, in your ChangeName method, can't do things like:

public void ChangeName(in Person person)
{
    person = new Person(); // invalid
}

Emphasis: this in usage isn't really intended for classes; it is intended for struct, and in particular readonly struct - to avoid defensive stack copies of complex and large value-types.

In latest versions of C#, there is also a ref readonly concept, by-which the l-value is a ref Foo (for whatever type Foo). This requires a custom iterator with a public ref Foo Current {get;} accessor instead of a public Foo Current {get;} accessor, i.e. it is explicitly tied into ref-return. When the l-value is a ref, you can use it in a method that takes a ref.


Important emphasis, although I realize OP knows this: you don't need ref in this scenario since Person is a class itself; the code would work just fine without the ref, and the object would be updated correctly.

When you pass ref intermediate, you pass a reference to the variable intermediate, not to the object pointed by it. If you assign a new Person to the person variable inside ChangeName, intermediate would get changed.

That’s why you are not allowed to pass ref person in the foreach, because you would be able to change the value of the foreach variable.

Related