Analysis of ref and out parameters

The parameter passed by reference is a major feature of C ා compared with many other languages. It's not easy to understand this concept in depth. It's even more dizzy to mix the value type and reference type.
I often see people confuse passing by reference with reference type, which makes me a little annoyed. Plus an interesting question I met two days ago, I think it's time to sort out ref and out.



1, What is pass by reference

Ref and out are very simple to use, that is, add a ref or out before the ordinary parameters passed by value, which must be added when defining and calling methods.
Ref and out refer to passing by reference, and CLR doesn't distinguish ref or out at all, so let's take ref as an example.

As we all know, no matter how the parameters passed by value are changed inside the method, the variables outside the method will not be affected, which has been heard from the teacher since learning C language.
How to write a Swap method in C? Use the pointer.
So what should I do in C? Although pointers can also be used, the more common and safer way is to use ref.

At this point, it needs to be clear whether the parameters passed by value will be changed.
If the int parameter is passed, the variables outside the method must be completely unchanged. But what if the List parameter is passed? All the additions and deletions to this List within the method will be reflected outside the method. You can see it by checking Count outside the method.
So the case of transferring List also represents the case of all reference type parameters. Does the variable outside the method change?
Don't listen to some arguments that "reference type is pass reference". When ref is not used, the reference type parameter still passes "value", so the variables outside the method remain unchanged.


The above is a sentence:
The method of passing parameters by value can never change the variables outside the method. To change the variables outside the method, the parameters must be passed by reference.

PS: variables that are not passed in by parameter can be changed. This article will not discuss this situation.


2, What are the parameters passing

What's there to discuss about such a simple problem.
But think about it. There are four cases in which the combination of value type variable and reference type variable is to pass parameters by value and by reference. In some cases, "value" and "reference" may refer to the same thing.

Let's start with variables. A variable is always associated with an object in memory.
For a variable of value type, it can be considered that it always contains two information, one is reference, the other is the value of the object. The former is a reference to the latter.
For variables of reference type, it can be considered that it also contains two information, one is reference, the other is reference. The former is still a reference to the latter, while the latter is an object in the heap.

The so-called transfer by value is the "two" of transfer, and the transfer by reference is the "one" of transfer.
That is, when passing a reference type by value, the content of the value passed is a reference.

The general situation is similar to this:

Passing by value is like this:

As you can see, no matter what changes are made to "value" and "B reference" within the method, the information contained in the two variables will not change.
However, it can also be seen that "reference type object" can be modified through "B reference" inside the method, which leads to the phenomenon that occurs on the List as mentioned above.
Passing by reference is like this:

It can be seen that at this time, the information of variables can be modified directly through "reference" and "A reference" inside the method, and even this may happen:

The method implementation at this time may be as follows:

void SampleMethod(ref object obj)
{
    //.....
    obj = new object();
    //.....
}

 

3, Differences in IL

Let's see how IL treats parameters passed by value or by reference. For example, this C code:

class Class
{
    void Method(Class @class) { }
    void Method(ref Class @class) { }
    // void Method(out Class @class) { }
}

This section of code can be compiled normally, but not uncomment. As mentioned earlier, IL does not distinguish ref from out.
Because of this possibility of overloading, the caller must also write ref or out, otherwise the compiler cannot distinguish which overloaded version is called.
The IL of Class is as follows:

.class private auto ansi beforefieldinit CsConsole.Class
    extends [mscorlib]System.Object
{
    // Methods
    .method private hidebysig static 
        void Method (
            class CsConsole.Class 'class'
        ) cil managed 
    {
        // Method begins at RVA 0x20b4
        // Code size 1 (0x1)
        .maxstack 8

        IL_0000: ret
    } // end of method Class::Method

    .method private hidebysig static 
        void Method (
            class CsConsole.Class& 'class'
        ) cil managed 
    {
        // Method begins at RVA 0x20b6
        // Code size 1 (0x1)
        .maxstack 8

        IL_0000: ret
    } // end of method Class::Method
} // end of class CsConsole.Class

For the convenience of reading, I have removed the original default parameterless constructor.
We can see that there is only one IL of the two methods & the difference of the symbol, which is also the reason why the two methods can have the same name, because their parameter types are different. The out and ref parameters are of the same type.
Now add something to the code to make the difference more obvious:

class Class
{
    int i;

    void Method(Class @class)
    {
        @class.i = 1;
    }
    void Method(ref Class @class)
    {
        @class.i = 1;
    }
}

Now the IL is like this:

.class private auto ansi beforefieldinit CsConsole.Class
    extends [mscorlib]System.Object
{
    // Fields
    .field private int32 i

    // Methods
    .method private hidebysig 
        instance void Method (
            class CsConsole.Class 'class'
        ) cil managed 
    {
        // Method begins at RVA 0x20b4
        // Code size 8 (0x8)
        .maxstack 8

        IL_0000: ldarg.1
        IL_0001: ldc.i4.1
        IL_0002: stfld int32 CsConsole.Class::i
        IL_0007: ret
    } // end of method Class::Method

    .method private hidebysig 
        instance void Method (
            class CsConsole.Class& 'class'
        ) cil managed 
    {
        // Method begins at RVA 0x20bd
        // Code size 9 (0x9)
        .maxstack 8

        IL_0000: ldarg.1
        IL_0001: ldind.ref
        IL_0002: ldc.i4.1
        IL_0003: stfld int32 CsConsole.Class::i
        IL_0008: ret
    } // end of method Class::Method
} // end of class CsConsole.Class

 

There is an instruction in the method with ref“ ldind.ref ”, about this Directive MSDN The explanation is as follows:

Indirectly loads an object reference as an O (object reference) type onto the evaluation stack.

In short, it takes an object ref erence from an address, which is similar to the unreef version of the“ arg.1 ”Same, @ class passed in by value.
Another way to look at it is to change the code to this:

class Class
{
    void Method(Class @class)
    {
        @class = new Class();
    }
    void Method(ref Class @class)
    {
        @class = new Class();
    }
}

IL is like this:

.class private auto ansi beforefieldinit CsConsole.Class
    extends [mscorlib]System.Object
{
    // Methods
    .method private hidebysig 
        instance void Method (
            class CsConsole.Class 'class'
        ) cil managed 
    {
        // Method begins at RVA 0x20b4
        // Code size 8 (0x8)
        .maxstack 8

        IL_0000: newobj instance void CsConsole.Class::.ctor()
        IL_0005: starg.s 'class'
        IL_0007: ret
    } // end of method Class::Method

    .method private hidebysig 
        instance void Method (
            class CsConsole.Class& 'class'
        ) cil managed 
    {
        // Method begins at RVA 0x20bd
        // Code size 8 (0x8)
        .maxstack 8

        IL_0000: ldarg.1
        IL_0001: newobj instance void CsConsole.Class::.ctor()
        IL_0006: stind.ref
        IL_0007: ret
    } // end of method Class::Method
} // end of class CsConsole.Class

This time the difference between the two sides is even greater.
What the non ref version does is very simple. new assigns a class object to @ class directly.
However, if there is a ref version, it takes the ref reference first and leaves it for later use, then new Class, and then assigns the Class object to the place the ref reference points to.
Let's see the difference between callers:


class Class
{
    void Method(Class @class) { }
    void Method(ref Class @class) { }

    void Caller()
    {
        Class @class = new Class();
        Method(@class);
        Method(ref @class);
    }
}

 

.method private hidebysig 
    instance void Caller () cil managed 
{
    // Method begins at RVA 0x20b8
    // Code size 22 (0x16)
    .maxstack 2
    .locals init (
        [0] class CsConsole.Class 'class'
    )

    IL_0000: newobj instance void CsConsole.Class::.ctor()
    IL_0005: stloc.0
    IL_0006: ldarg.0
    IL_0007: ldloc.0
    IL_0008: call instance void CsConsole.Class::Method(class CsConsole.Class)
    IL_000d: ldarg.0
    IL_000e: ldloca.s 'class'
    IL_0010: call instance void CsConsole.Class::Method(class CsConsole.Class&)
    IL_0015: ret
} // end of method Class::Caller

The difference is very clear, the former takes "value" from the local variable table, and the latter takes "reference" from the local variable table.


4, References and pointers

After talking about references for so long, take a look at the pointers that can also be used to write Swap.
Obviously, the type of ref parameter is different from that of pointer parameter, so it can be compiled as follows:

unsafe struct Struct
{
    void Method(ref Struct @struct) { }
    void Method(Struct* @struct) { }
}

The IL of these two methods is very interesting:

.class private sequential ansi sealed beforefieldinit CsConsole.Struct
    extends [mscorlib]System.ValueType
{
    .pack 0
    .size 1

    // Methods
    .method private hidebysig 
        instance void Method (
            valuetype CsConsole.Struct& 'struct'
        ) cil managed 
    {
        // Method begins at RVA 0x2050
        // Code size 1 (0x1)
        .maxstack 8

        IL_0000: ret
    } // end of method Struct::Method

    .method private hidebysig 
        instance void Method (
            valuetype CsConsole.Struct* 'struct'
        ) cil managed 
    {
        // Method begins at RVA 0x2052
        // Code size 1 (0x1)
        .maxstack 8

        IL_0000: ret
    } // end of method Struct::Method

} // end of class CsConsole.Struct

The ref version is marked by the address operator (&), while the pointer version is marked by the indirect addressing operator (*), which has obvious meaning. The former passes in the address (i.e. reference) of a variable, and the latter passes in a pointer type.
What's more interesting is this:

unsafe struct Struct
{
    void Method(ref Struct @struct)
    {
        @struct = default(Struct);
    }
    void Method(Struct* @struct)
    {
        *@struct = default(Struct);
    }
}
.class private sequential ansi sealed beforefieldinit CsConsole.Struct
    extends [mscorlib]System.ValueType
{
    .pack 0
    .size 1

    // Methods
    .method private hidebysig 
        instance void Method (
            valuetype CsConsole.Struct& 'struct'
        ) cil managed 
    {
        // Method begins at RVA 0x2050
        // Code size 8 (0x8)
        .maxstack 8

        IL_0000: ldarg.1
        IL_0001: initobj CsConsole.Struct
        IL_0007: ret
    } // end of method Struct::Method

    .method private hidebysig 
        instance void Method (
            valuetype CsConsole.Struct* 'struct'
        ) cil managed 
    {
        // Method begins at RVA 0x2059
        // Code size 8 (0x8)
        .maxstack 8

        IL_0000: ldarg.1
        IL_0001: initobj CsConsole.Struct
        IL_0007: ret
    } // end of method Struct::Method

} // end of class CsConsole.Struct

The two method is as like as two peas! IL. Can you imagine what the essence of quotation is?


5, this and references

This interesting question was realized two days ago. I have never written code like this before:

struct Struct
{
    void Method(ref Struct @struct) { }

    public void Test()
    {
        Method(ref this);
    }
}

The above code can be compiled, but it will not work if it is written as follows:

class Class
{
    void Method(ref Class @class) { }

    void Test()
    {
        // Unable to“<this>"As ref or out Parameter passed because it is read-only
        Method(ref this);
    }
}

The red part of the code will report an error as described in the comment. The only difference between the two pieces of code is that the former is struct (value type) and the latter is class (reference type).
As has been said before, the parameter modification of ref tag inside the method will affect the variable value outside the method, so using ref tag this incoming method may cause the value of this to be changed.
Interestingly, why is this allowed to be changed in struct and this in class?

The following content has nothing to do with ref, but it involves value and reference, so keep writing: D

MSDN The "this" keyword is interpreted as follows:

this keyword references the current instance of the class

The current instance here refers to the object in memory, that is, the value or reference type object in the following figure:

If this of value type is assigned, then "value" is modified, and "current instance" is still the original instance object, but the content has changed.
If this of reference type is assigned, then "B reference" is modified, which is similar toThis picture In this case, the current instance is no longer the original instance object, and the meaning of this keyword is no longer clear. Therefore, this in the reference type should be read-only, and make sure that "this" is the "this" object pointed to.



In the end, I didn't expect to say anything more. Let's stop~

Keywords: C

Added by skovela on Mon, 18 May 2020 04:25:18 +0300