So, as far as I know, the type of value is stored on the stack.
and thus is the basis of your confusion; this is a simplification that is extremely inaccurate. Structures can live on the stack, but they can also live:
- like a field for objects on the heap
- like fields in other structures that (etc.) fields on an object in a heap
- in a box (directly or through any of the above)
- in unmanaged memory
You are correct, though: if you passed the ref return from the method to the local one inside the method, you will violate the integrity of the stack. That is why this scenario is not allowed:
ref int RefLocal() { int i = 42; return ref i; // Error CS8168 Cannot return local 'i' by reference because it is not a ref local }
There are some scenarios where the compiler can prove that although it was saved as local, the lifetime was limited by this method; this helps not to reassign the local ref (to be honest, this check is the main reason for this restriction); this allows:
ref int RefParamViaLoval(ref int arg) { ref int local = ref arg; return ref local; }
Since ref int arg has a lifetime that is not tied to the method, our ref int local can inherit this lifetime in the assignment.
So, what can we profitably return to?
This may be a reference to the inside of the array:
ref int RefArray(int[] values) { return ref values[42]; }
This may be a field (not a property) for the object:
ref int ObjFieldRef(MyClass obj) { return ref obj.SomeField; }
This can be a field (not a property) in the structure passed by reference:
ref int StructFieldRef(ref MyStruct obj) { return ref obj.SomeField; }
This may be something received from a subsequent call if the call does not include any ref locators that are known to point to locals (which would make it impossible to prove the validity):
ref int OnwardCallRef() { ref MyStruct obj = ref GetMyStructRef(); return ref obj.SomeField; }
Here again, note that the lifetime of the local inherits the lifetime of any parameters passed to the current call; if the incoming call included ref -local with a limited lifetime, then the result would inherit this limited lifetime, and you could not return it.
And this call can be, for example, a call to structures stored in unmanaged memory:
ref int UnmanagedRef(int offset) { return ref Unsafe.AsRef<int>(ptr + offset); }
So: there are a lot of very correct and useful scripts that do not include links to the current stack frame.