How do I assign by "reference" to a class field in C#?
Asked Answered
C

4

32

I am trying to understand how to assign by "reference" to a class field in C#.

I have the following example to consider:

 public class X
 {

     public X()
     {
         string example = "X";

         new Y(ref example);

         new Z(ref example);

         System.Diagnostics.Debug.WriteLine(example);
     }

 }

 public class Y
 {

     public Y( ref string example )
     {
         example += " (Updated By Y)";
     }

 }

 public class Z
 {
     private string _Example;

     public Z(ref string example)
     {

         this._Example = example;

         this._Example += " (Updated By Z)";
     }
 }

 var x = new X();

When running the above code the output is:

X (Updated By Y)

And not:

X (Updated By Y) (Updated By Z)

As I had hoped.

It seems that assigning a "ref parameter" to a field loses the reference.

Is there a way to keep hold of the reference when assigning to a field?

Croesus answered 5/6, 2010 at 13:11 Comment(0)
A
9

No. ref is purely a calling convention. You can't use it to qualify a field. In Z, _Example gets set to the value of the string reference passed in. You then assign a new string reference to it using +=. You never assign to example, so the ref has no effect.

The only work-around for what you want is to have a shared mutable wrapper object (an array or a hypothetical StringWrapper) that contains the reference (a string here). Generally, if you need this, you can find a larger mutable object for the classes to share.

 public class StringWrapper
 {
   public string s;
   public StringWrapper(string s)
   {
     this.s = s;
   }

   public string ToString()
   {
     return s;
   }
 }

 public class X
 {
  public X()
  {
   StringWrapper example = new StringWrapper("X");
   new Z(example)
   System.Diagnostics.Debug.WriteLine( example );
  }
 }

 public class Z
 {
  private StringWrapper _Example;
  public Z( StringWrapper example )
  {
   this._Example = example;
   this._Example.s += " (Updated By Z)";
  }
 }
Adamis answered 5/6, 2010 at 13:14 Comment(3)
Would you mind expanding the second paragraph a bit? "a larger mutable object " is not entirely clear for me. Also, how would a StringWrapper work if there is no way to hold the string reference in a field?Analects
I've given an example which should clarify. As I said, in a real design StringWrapper would probably be a business object holding more than just a string.Adamis
Thanks for the idea. My example was probably over simplified because I do actually have an object (not a string) in my real code which I am trying to assign by reference. The behaviour I am looking for is to allow a user of an object to be able to assign null to said object, or even assign a new instance of that type from it's methods. I pass the object in via the constructor as part of some dependency injection and so the other methods can only see the object by accessing a field. It would seem what I want cannot be achieved which is a shame.Croesus
M
85

As others have noted, you cannot have a field of "ref to variable" type. However, just knowing that you cannot do it is probably unsatisfying; you probably also want to know first, why not, and second, how to get around this restriction.

The reason why is because there are only three possibilities:

1) Disallow fields of ref type

2) Allow unsafe fields of ref type

3) Do not use the temporary storage pool for local variables (aka "the stack")

Suppose we allowed fields of ref type. Then you could do

public ref int x;
void M()
{
    int y = 123;
    this.x = ref y;
}

and now y can be accessed after M completes. This means that either we're in case (2) -- accessing this.x will crash and die horribly because the storage for y no longer exists -- or we're in case (3), and the local y is stored on the garbage collected heap, not the temporary memory pool.

We like the optimization that local variables be stored on the temporary pool even if they are being passed by ref, and we hate the idea that you could leave a time bomb around that could make your program crash and die later. Therefore, option one it is: no ref fields.

Note that for local variables that are closed-over variables of anonymous functions we choose option (3); those local variables are not allocated out of the temporary pool.

Which then brings us to the second question: how do you get around it? If the reason you want a ref field is to make a getter and setter of another variable, that's perfectly legal:

sealed class Ref<T>
{
    private readonly Func<T> getter;
    private readonly Action<T> setter;
    public Ref(Func<T> getter, Action<T> setter)
    {
        this.getter = getter;
        this.setter = setter;
    }
    public T Value { get { return getter(); } set { setter(value); } }
}
...
Ref<int> x;
void M()
{
    int y = 123;
    x = new Ref<int>(()=>y, z=>{y=z;});
    x.Value = 456;
    Console.WriteLine(y); // 456 -- setting x.Value changes y.
}

And there you go. y is stored on the gc heap, and x is an object that has the ability to get and set y.

Note that the CLR does support ref locals and ref returning methods, though C# does not. Perhaps a hypothetical future version of C# will support these features; I have prototyped it and it works well. However, this is not real high on the priority list, so I wouldn't get my hopes up.

UPDATE: The feature mentioned in the paragraph above was finally implemented for real in C# 7. However, you still cannot store a ref in a field.

Marris answered 5/6, 2010 at 20:46 Comment(15)
Why are the getter and setter required - is the closure important to get y to remain boxed? Would it be possible to simply use Ref<T> { public T Value { get; set; } } instead?Lusatia
@ChrisMoschini: Sure, that would be perfectly possible. But then you are passing around a reference to a value, and not a variable. The whole point of the question is "how do I store a reference to a variable?" Suppose instead of local variable y, you wanted to store a "reference" to the 12th element of an array, so that mutating the reference mutated the 12th element of an array. Your proposed type would not help in that scenario.Marris
True, although neither would passing .someMethod(ref array[12]), which seems to be more to the spirit of the question. Neither of our proposals gets you a value type that's boxed and remains boxed, unfortunately. Nullable<T> might actually be the closest match to what the asker was attempting.Lusatia
That classes cannot have fields of ref type would not imply that structures could not safely do so, though the set of things one could do with such a struct would be constrained to the most restrictive of its members. Such structures would have a significant use case if byrefs were subdivided into returnable and non-returnable ones. Functions could only return returnable byrefs, and local variables could only be passed as non-returnable. Passing a non-returnable byref as a returnable-byref parameter would cause the return value of that call to be non-returnable.Shaer
Presently, when passing a lambda, there's no way of controlling how long the recipient will kept a reference to any closed-over variables. If one could pass a byref to a quasi-generic structure of byrefs, a recipient that was coded to take such a thing [rather than a delegate] would have genuine byrefs to all the variables but would be guaranteed not to persist them once it had returned.Shaer
i've hit this problem recently. i feel obliged to disagree with your statement that there are only 3 choices. i'm very sure that 2) can be modified to never be unsafe so that the functionality is there, but bad use like the code your provide in your example would produce a compile error. (continued)Scrimp
the compiler has more than enough information to work out that y belongs to the current scope. the only way to create a reference to such a variable is inside its scope (any others must necessairy be assigned from such a reference, or another reference that was assigned from one etc...) i can't imagine a situation where this would not be true. as a concrete example in C++ compilers this usually comes as a warning 'taking address of local or temporary variable' etc. ... thanks for the example class though. :)Scrimp
@jheriko: You make a good point, but consider: class C { static ref int z; public static void N(ref int q) { this.z = q; } ... } and then in another assembly M() { int y = 123; C.N(ref y); } If y is on the temporary pool and z is used after M() returns then we are in case 2. Your proposal is to produce a compile error. Where? Which line of code contains the error and how does the compiler know that it is an error? Assume that the assembly containing M is compiled after the assembly containing N, since obviously there is a dependency relationship.Marris
good counterexample :) i didn't really think about the dynamic linking case at all, which is embarrassing. i'm sure this can still be repaired by adding more bloat, e.g. using a different keyword for stack bound refs and distinguishing them in their type signature -but this is a hacky fix i came up with on the spot - there may well be more problems as well so its enough to make it not a simple proposition as i originally believed.Scrimp
there could be something cleaner though - the problem is definitely localised to N(ref int q) { this.z = q; }, q is similar to a local variable on the stack and the assignment to a member is what allows it to /potentially/ leak from its scope. i'll need to think about this properly... thanks :)Scrimp
@jheriko: You might want to read this before you ponder, as there is an interesting hole in the type system for returning references to dead locals as well. This hole is easier to patch. \ blogs.msdn.com/b/ericlippert/archive/2011/06/23/…Marris
Brilliant. By-reference-by-lambda.Sauger
And in 2017, ref returns are a thing.Interline
@NetMage: Right, but you still can't put one in a field.Marris
i came back to this to share the discussion and realised the solution to the counterexample is actually quite straightforwards if i follow my original example. M() being a function, y being a local variable, the line making the call taking the reference of y is where the compile error happens. this is exactly the same as how c++ handles this case.Scrimp
A
9

No. ref is purely a calling convention. You can't use it to qualify a field. In Z, _Example gets set to the value of the string reference passed in. You then assign a new string reference to it using +=. You never assign to example, so the ref has no effect.

The only work-around for what you want is to have a shared mutable wrapper object (an array or a hypothetical StringWrapper) that contains the reference (a string here). Generally, if you need this, you can find a larger mutable object for the classes to share.

 public class StringWrapper
 {
   public string s;
   public StringWrapper(string s)
   {
     this.s = s;
   }

   public string ToString()
   {
     return s;
   }
 }

 public class X
 {
  public X()
  {
   StringWrapper example = new StringWrapper("X");
   new Z(example)
   System.Diagnostics.Debug.WriteLine( example );
  }
 }

 public class Z
 {
  private StringWrapper _Example;
  public Z( StringWrapper example )
  {
   this._Example = example;
   this._Example.s += " (Updated By Z)";
  }
 }
Adamis answered 5/6, 2010 at 13:14 Comment(3)
Would you mind expanding the second paragraph a bit? "a larger mutable object " is not entirely clear for me. Also, how would a StringWrapper work if there is no way to hold the string reference in a field?Analects
I've given an example which should clarify. As I said, in a real design StringWrapper would probably be a business object holding more than just a string.Adamis
Thanks for the idea. My example was probably over simplified because I do actually have an object (not a string) in my real code which I am trying to assign by reference. The behaviour I am looking for is to allow a user of an object to be able to assign null to said object, or even assign a new instance of that type from it's methods. I pass the object in via the constructor as part of some dependency injection and so the other methods can only see the object by accessing a field. It would seem what I want cannot be achieved which is a shame.Croesus
W
3

You forgot to update the reference in the Z class:

public class Z {
    private string _Example;

    public Z(ref string example) {
        example = this._Example += " (Updated By Z)";
    }
}

Output: X (Updated By Y) (Updated By Z)

Point to keep in mind is that the += operator for a string calls the String.Concat() method. Which creates a new string object, it doesn't update the value of a string. String objects are immutable, the string class doesn't have any methods or fields that lets you change the value. Very different from the default behavior of a regular reference type.

So if you use a string method or operator, you always have to assign the return value back to a variable. This is pretty natural syntax, value types behave the same way. Your code would be very similar if you used an int instead of a string.

Workmanlike answered 5/6, 2010 at 13:31 Comment(1)
Thanks for the idea Hans. My example is at fault - to keep it simple I made the update in the constructor. In reality the field would be set in the constructor but the update happen in another method after the object was instantiated.Croesus
Y
0

As of 2023, .NET 8 is going to introduce ref fields, but only for structs.

If you need one in a class, the proper way would be using StrongBox instead of writing your own wrapper class as others have suggested.

Yonina answered 3/10, 2023 at 21:57 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.