3

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?

mjwills
  • 23,389
  • 6
  • 40
  • 63
JohnChris
  • 1,360
  • 15
  • 29
  • `public void ChangeName(Person person) {...}` - you can drop `ref` here – Dmitry Bychenko Jun 07 '19 at 08:54
  • yes that will allow it, but that is not the question @DmitryBychenko – JohnChris Jun 07 '19 at 08:54
  • @HimBromBeere yes i set it to expect a `ref` to illustrate my question... – JohnChris Jun 07 '19 at 08:56
  • 2
    "but technically its still a reference" - the difference here is "a reference" vs "a reference to a reference"; but just to add confusion, c# "latest" supports `ref foreach`, where the l-value **is a reference** (i.e. a `ref Person` in this case), in which case it *would* work! Note: most people will never need to know or care about `ref foreach` :) – Marc Gravell Jun 07 '19 at 08:58
  • 1
    Suppose we put `public void ChangeName(ref Person person) {person = null;}`. What is the desired `forloop` behaviour? In case of `intermediate`, it's `intermediate == null` while loop variable (`person`) is not changed. – Dmitry Bychenko Jun 07 '19 at 08:58
  • 1
    So the question is "*Why does the compiler not allow `myPeople.ChangeName(ref person)` but allows `myPeople.ChangeName(ref intermediate)` when `person = intermediate`?*", right? – Rafalon Jun 07 '19 at 08:59
  • @DmitryBychenko your comment is completely wrong and further illustrates my question, both will become `null` – JohnChris Jun 07 '19 at 08:59
  • Just to be explicit: you don't need `ref` here - I think you know that, but I just wanted to call it out – Marc Gravell Jun 07 '19 at 09:00
  • @MarcGravell yes i know:p – JohnChris Jun 07 '19 at 09:00
  • @Rafalon correct – JohnChris Jun 07 '19 at 09:00
  • 2
    `//this is intermediate variable is still a reference to the iteration variable` No it isn't. It is a reference to the same object that `iteration` references. That distinction may seem subtle, but it is important. – mjwills Jun 07 '19 at 09:00
  • @mjwills hmm, need to think about this – JohnChris Jun 07 '19 at 09:03
  • Before we discuss further, please clarify whether you understand why https://dotnetfiddle.net/WNJxQU doesn't compile. _Note that if you understand that, it becomes reasonably clear why it can't be a `ref` parameter, which is why it is important to use this as a starting point._ – mjwills Jun 07 '19 at 09:05
  • 1
    And please re-read MSDN about ref- and out-keywords. – MakePeaceGreatAgain Jun 07 '19 at 09:14
  • 1
    @mjwills As to your edit, I think the reason for OP's confusion is precisely that `var intermediate = person;` means "*the `person` variable points to the same object as the `intermediate` variable*" and **not** *the `person` variable **is the same** as the `intermediate` variable*" – Rafalon Jun 07 '19 at 09:16
  • Agreed @Rafalon. My earlier comment (and everyone else's) thankfully helped the OP come / move to that realisation. The edit was just to make the question more useful for future readers. – mjwills Jun 07 '19 at 09:16
  • thanks for all the insight guys – JohnChris Jun 07 '19 at 09:19

2 Answers2

4

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.

Marc Gravell
  • 1,026,079
  • 266
  • 2,566
  • 2,900
1

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.

Thiago Barcala
  • 6,463
  • 2
  • 20
  • 23
  • I didn’t get what you mean by “both will change”, can you be a bit more specific? – Thiago Barcala Jun 07 '19 at 09:07
  • u pass a reference to both implictly, by changing intermediate, you change the property on the intermediate variable and iteration variable – JohnChris Jun 07 '19 at 09:08
  • 3
    Exactly, you are able to change the properties of the object pointed by “person”, but not which object “person” is pointing to. – Thiago Barcala Jun 07 '19 at 09:10
  • Did you look at https://dotnetfiddle.net/WNJxQU yet @JohnChris? You understand why it doesn't compile? – mjwills Jun 07 '19 at 09:30
  • @mjwills yeah i did, because you are trying to replace the iteration variable with null, not a property on it. That is answered by a post a linked in my question above. My question, ill admit not worded well, you can pass the iterator to a method to get properties changed on it where it's implicitly referenced but if i expliclty set `ref` it wont allow it. My goal was never to replace the object but alter its properties. – JohnChris Jun 07 '19 at 09:33
  • `My goal was never to replace the object but alter its properties.` Please read https://stackoverflow.com/questions/45380539/alternative-to-using-ref-in-foreach @JohnChris. **You do not need `ref` to do that.** Just as Dmitry said earlier. – mjwills Jun 07 '19 at 09:35
  • @mjwills i know i dont need `ref` or `in` , was just trying to understand the c# compiler logic, which Marc answered... – JohnChris Jun 07 '19 at 09:36