[C #] Analysis of ref and out parameters,
The parameter passed by reference is a major feature of C # compared with many other languages. It is not easy to understand this concept in depth, then, the value type and reference type will become even more dizzy.
It is often seen that some people confuse the transfer by reference with the reference type, which makes me a little uncomfortable. In addition to an interesting problem I encountered two days ago, I felt that I should sort out the ref and out content.
1. What is passed by reference?
Ref and out are very simple to use, that is, add a ref or out before a common parameter passed by value. The method must be added for definition and call.
Both ref and out indicate passing by reference, and CLR does not distinguish between ref and out. Therefore, the following uses ref as an example to describe.
As we all know, no matter how the parameter passed by value changes within the method, the variables outside the method will not be affected. This is what the teacher said when I learned the C language.
What should I do if I want to write a Swap method in C? Use pointers.
So what should I do in C? Although pointers can also be used, it is generally safer to use ref.
Here, we need to make it clear whether the parameter passed by value will be changed.
If the int parameter is passed, the variables outside the method must be completely unchanged. But what if a List is passed? All additions, deletions, and modifications to this List in the method are reflected in the external method header. Check the Count field outside the method to see it, right.
In this case, if List is passed, it also indicates all the reference type parameters. Does the variable outside the method remain unchanged?
Do not listen to some arguments that "the reference type is to pass the reference". If no ref is used, the reference type parameter is still to pass the "value", so the variables outside the method remain unchanged.
The above is a sentence:
Passing parameters by value is never possible to change variables outside the method. To change variables outside the method, you must pass parameters by reference.
PS: variables passed in without passing parameters can certainly be changed. This article will not discuss this situation.
Ii. What is the parameter passed?
Passing a parameter by value is a value, and passing a parameter by reference is a reference. What else can be discussed about this simple problem.
But think about it. There are four cases in which parameter passing by value and parameter passing by reference are combined on the Value Type Variable and reference type variable, in some cases, "value" and "Reference" may refer to the same thing.
Let's start with a variable. A variable is always associated with an object in memory.
For a value type variable, we can think that it always contains two pieces of information: Reference and object value. The former is a reference pointing to the latter.
Variables of the reference type can be considered to contain two types of information: one is reference and the other is reference. The former still points to the latter's reference, while the latter points to the object in the heap.
The so-called pass by value is the transfer of "two"; transfer by reference is the transfer of "one ".
That is to say, when a reference type is passed by value, the content of the passed value is a reference.
The general situation is similar to this:
When passing by value, it is like this:
We can see that no matter what changes are made to the "value" and "B reference" In the method, the information contained in the two variables will not change.
However, we can also see that the method can be used internally to modify the reference type object through "B Reference", which leads to the phenomenon mentioned above on the List.
The transfer by reference is like this:
We can see that, at this time, the method can directly modify the variable information through "Reference" and "A reference", or even this may happen:
The method implementation at this time may be as follows:
void SampleMethod(ref object obj){ //..... obj = new object(); //.....}
Iii. differences from the perspective of IL
Next, let's take a look at how IL treats parameters passed by value or by reference. For example, this section of C # code:
class Class{ void Method(Class @class) { } void Method(ref Class @class) { } // void Method(out Class @class) { }}
This piece of code can be compiled normally, but it won't work if you cancel the annotation. As mentioned above, IL does not distinguish between ref and out.
It is precisely because of the possibility of this overload that the caller must specify ref or out. Otherwise, the compiler cannot distinguish which overload version is called.
The Class IL is like this:
.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 ease of reading, I removed the original default no-argument constructor.
We can see that there is only one & Symbol difference in the IL of the two methods. The difference is also the reason that the two methods can have the same name, because their parameter types are different. The out and ref parameters are of the same type.
Add a bit of content 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; }}
The current 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
The method with ref contains an additional command "ldind. ref". The MSDN explanation of this command is as follows:
Indirectly attaches an object reference to the computing stack as an O (Object Reference) type.
Simply put, an object reference is obtained from an address. This object reference is the same as "arg.1" in the non-ref version, that is, the @ class passed in by value.
Let's take a look at it from another perspective and change the code to the following:
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 parties is even greater.
It is easy to do without the ref version. A new Class object is assigned to @ class directly.
However, in the ref version, the ref reference is obtained first for future use, and then the Class is new. Then, the Class object is assigned to the place where the ref reference points.
Let's take a look at the differences between the Caller:
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 clear. The former extracts "value" from the local variable table, and the latter extracts "Reference" from the local variable table ".
Iv. References and pointers
After talking about the reference for so long, let's take a look at the pointer that can also be used to write Swap.
Obviously, the type of the ref parameter and the pointer parameter is different, so the write can be compiled:
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 get address operator (&), while the pointer version uses the indirect addressing operator (*), which has obvious meanings, the former is the address (reference) of a variable, and the latter is a pointer type.
The more interesting thing is:
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 IL of the two methods is the same! What is the essence of reference ~?
V. this and references
This interesting problem was realized two days ago. I have never written such code before:
struct Struct{ void Method(ref Struct @struct) { } public void Test() { Method(ref this); }}
The above code can be compiled, but it won't work if it is written as follows:
Class {void Method (ref Class @ Class) {} void Test () {// "<this>" cannot be passed as the ref or out parameter, because it is a read-only Method (ref this );}}
The red part of the code reports an error as described in the comment. The only difference between the two code segments is that the former is struct (value type) and the latter is class (reference type ).
As we have already said, modifying the parameters marked by ref inside the method will affect the value of variables outside the method. Therefore, marking this with ref may cause the value of this to be changed.
Interestingly, why can't this in struct be changed while in class be changed?
The following content does not have much to do with ref, but it involves values and references, So let's continue writing: D
MSDN interprets the "this" keyword as follows:
This keyword references the current instance of the class
Here the "current instance" refers to the objects in the memory, that is, the "value" or "reference type object" in ":
If this value type is assigned, the "value" is modified, and the "current instance" is still the original Instance object, but the content has changed.
If you copy this of the reference type, the "B Reference" is modified. this figure is similar. The current "current instance" is no longer the original Instance Object, the meaning of this keyword is no longer clear. Therefore, this in the reference type should be read-only. Make sure that "this" is the "this" object to which it is directed.
In the end, I didn't expect anything to be said. So far ~