Search code examples
c#ref

What kinds of objects can "return by reference" in C#


I'm reading <Essential C# 7.0>.
As for "return by reference", it says

There are two important restrictions on return by reference—both due to object lifetime: Object references shouldn’t be garbage collected while they’re still referenced, and they shouldn’t consume memory when they no longer have any references. To enforce these restrictions, you can only return the following from a reference-returning function:
• References to fields or array elements
• Other reference-returning properties or functions
• References that were passed in as parameters to the by-reference returning function

I did some experiments, and the results are certainly as the book says.

namespace App
{
    public class App
    {
        static void Main()
        {

        }
        class Test
        {
            public int x;
        }
        ref int RefReturn()
        {
            int[] a = { 1, 2 };
            return ref a[0];
        }
        ref int RefReturn2()
        {
            Test t = new Test();
            return ref t.x;
        }
        ref int RefReturnError()
        {
            int a = 1;
            return ref a; //Error
        }
        ref Test RefReturnError2()
        {
            Test t = new Test();
            return ref t; //Error
        }
    }
}

I can not quite understand this.
(the 3 kinds can return by reference in the book, why these 3 kinds?)
For example, I can ref return a field of the class, but not the class.
("ref int RefReturn2()" and "ref Test RefReturnError2()" in my code)

I think it should be something about "object lifetime" & "garbage collection" in C#, which I don't know much about.

I also would like to know typical situations of using return by reference.
I think typical situations can also help understanding.


Solution

  • The key take-away for the rules for managed references (ref) is: a managed reference must not point to a local variable, or to part of one (in the case of a struct), because the reference can outlive the life of the location it points to. It must point to a non-stack location.

    Let's take each version one-by-one


            ref int RefReturn()
            {
                int[] a = { 1, 2 };
                return ref a[0];
            }
    

    In the above example, the returned reference points to the interior of the array, it does not point to the local variable. The interior of an array is effectively a field of a heap object. The array will outlive the life of the function.


            ref int RefReturn2()
            {
                Test t = new Test();
                return ref t.x;
            }
    

    In this one, Test is a reference-type, and therefore lives on the heap. The reference points to the field x of the object contained in t, this also lives on the heap. The fact that t is a local variable is immaterial, the reference does not point to t.


            ref int RefReturnError()
            {
                int a = 1;
                return ref a; //Error
            }
    

    In this case, the reference points to the actual location of the local variable, this lives on the stack, and the location will disappear at the end of the function.

    Note that the same problem is visible when taking a reference to a field of a struct, when the struct's location is a local variable.

            ref int RefReturnError1A()
            {
                MyStruct a = new MyStruct();
                return ref a.x; //Error
            }
    

            ref Test RefReturnError2()
            {
                Test t = new Test();
                return ref t; //Error
            }
    

    In this one, although t is a reference-type and itself points to a heap object, our reference does not point to that object which t points to. It points to the location of t itself which contains that object reference.


    Note that a reference to a boxed struct is disallowed for a different reason: due to C#'s unboxing rules, unboxing (logically) creates a copy, therefore you cannot change it in place. Coding in IL directly (or in C++/CLI) you can perfectly verifiably do the equivalent of:

            ref int RefReturnBox()
            {
                object a = (object)1;
                return ref (int)a;    // CS0445: Cannot modify the result of an unboxing conversion
            }