Ref in C # has been released. Maybe you don’t know it anymore


1: Background

1. Tell a story

Recently, looking through the NETCORE source code, I found that many codes in the framework have been modified by Ref. I’ll go. Is this still the ref I know? Take span for example, the code is as follows:

public readonly ref struct Span
        public ref T GetPinnableReference()
            ref T result = ref Unsafe.AsRef(null);
            if (_length != 0)
                result = ref _pointer.Value;
            return ref result;

        public ref T this[int index]
                return ref Unsafe.Add(ref _pointer.Value, index);

Are there refs everywhere, in struct, local variable, method signature, method call, attribute and return? It’s almost everything. Great. Let’s talk about this wonderful ref in this article.

2: Ref code analysis under various scenarios

1. Motivation

I wonder if you have found that after c# 7.0, the language team has paid unprecedented attention to performance, and has specially provided various types and underlying support, such as span, memory, valuetask, and ref to be introduced in this article.

In our traditional understanding, ref is used in method parameters to reference and pass values to value types. One is to avoid the performance overhead caused by the copy of value types. I don’t know who has a big brain hole and applies ref to all parts of the code you know, The ultimate goal is to improve performance as much as possible.

2. Ref struct analysis

Since childhood, the value type has been allocated on the stack, and the reference type is on the heap. This is also problematic, because the value type can also be allocated on the heap, such as the location of the following code.

public class Program
        public static void Main(string[] args)
            Var person = new person() {name = "Zhang San", location = new point() {x = 10, y = 20}};


    public class Person
        public string Name { get; set; }

        Public point location {get; set;} // allocate on heap

    public struct Point
        public int X { get; set; }
        public int Y { get; set; }

In fact, this is where many novice friends are confused about learning value types. You can use WinDbg to find the managed heapPersonAsk and see the following code:

0:000> !dumpheap -type Person
         Address               MT     Size
0000010e368aadb8 00007ffaf50c2340       32     

0:000> !do 0000010e368aadb8
Name:        ConsoleApp2.Person
MethodTable: 00007ffaf50c2340
EEClass:     00007ffaf50bc5e8
Size:        32(0x20) bytes
File:        E:\net5\ConsoleApp1\ConsoleApp2\bin\Debug\netcoreapp3.1\ConsoleApp2.dll
              MT    Field   Offset                 Type VT     Attr            Value Name
00007ffaf5081e18  4000001        8        System.String  0 instance 0000010e368aad98 k__BackingField
00007ffaf50c22b0  4000002       10    ConsoleApp2.Point  1 instance 0000010e368aadc8 k__BackingField

0:000> dp 0000010e368aadc8
0000010e`368aadc8  00000014`0000000a 00000000`00000000

14 and a in the last line 00000014 ` 0000000a of the above code are the values of Y and X, which are stored in the heap stably. If you don’t believe it, look at the range of GC generation 0 heap.

0:000> !eeheap -gc
Number of GC Heaps: 1
generation 0 starts at 0x0000010E368A1030
generation 1 starts at 0x0000010E368A1018
generation 2 starts at 0x0000010E368A1000
ephemeral segment allocation context: none
         segment             begin         allocated              size
0000010E368A0000  0000010E368A1000  0000010E368B55F8  0x145f8(83448)

It can be seen from the last line that 00000 1e368aadc8 is indeed in generation 0 heap0x0000010E368A1030 - 0000010E368B55F8Within the scope of.

The next question is whether we can make a restriction on struct. Just like generic constraints, struct is not allowed to be allocated on the heap. Is there any way? The solution is to add a ref limit, as shown in the following figure:

From the error prompt, it can be seen that the operation of intentionally allocating struct to the heap is strictly prohibited. If you want to think that the compiler can only change the class person to ref struct person, that is, span and this [int index] at the beginning of the article, you can imagine the motivation. Everything is for performance.

3. Ref method analysis

I think many friends are familiar with passing reference addresses to method parameters, such as the following:

public static int GetNum(ref int i)
            return i;

Now you can try to jump out of the mindset. Since you can still quote an address in the method, can you throw a reference address out of the method? If this can also be implemented, it will be more interesting. I can return reference addresses to some data in the collection. These return values can still be modified outside the method. After all, the reference addresses are passed around, as shown in the following code:

public class Program
        public static void Main(string[] args)
            var nums = new int[3] { 10, 20, 30 };

            ref int num = ref GetNum(nums);

            num = 50;

            Console.WriteLine($"nums= {string.Join(",",nums)}");


        public static ref int GetNum(int[] nums)
            return ref nums[2];

As you can see, the last value of the array has been30 -> 50Some friends may be surprised at how this is played. Don’t think that the reference address floats everywhere. If you don’t believe it, look at the IL code.

.method public hidebysig static 
	int32& GetNums (
		int32[] nums
	) cil managed 
	// Method begins at RVA 0x209c
	// Code size 13 (0xd)
	.maxstack 2
	.locals init (
		[0] int32&

	// {
	IL_0000: nop
	// return ref nums[2];
	IL_0001: ldarg.0
	IL_0002: ldc.i4.2
	IL_0003: ldelema [System.Runtime]System.Int32
	IL_0008: stloc.0
	// (no C# code)
	IL_0009: br.s IL_000b

	IL_000b: ldloc.0
	IL_000c: ret
} // end of method Program::GetNums

.method public hidebysig static 
	void Main (
		string[] args
	) cil managed 
	IL_0013: ldloc.0
	IL_0014: call int32& ConsoleApp2.Program::GetNums(int32[])
	IL_0019: stloc.1
	IL_001a: ldloc.1
	IL_001b: ldc.i4.s 50
	IL_003e: pop
	IL_003f: ret
} // end of method Program::Main

As you can see, there are & value operators everywhere. If it’s more intuitive, take a look with WinDbg.

0:000> !clrstack -a
OS Thread Id: 0x7040 (0)
000000D4E777E760 00007FFAF1C5108F ConsoleApp2.Program.Main(System.String[]) [E:\net5\ConsoleApp1\ConsoleApp2\Program.cs @ 28]
        args (0x000000D4E777E7F0) = 0x00000218c9ae9e60
        0x000000D4E777E7C8 = 0x00000218c9aeadd8
        0x000000D4E777E7C0 = 0x00000218c9aeadf0

0:000> dp 0x00000218c9aeadf0
00000218`c9aeadf0  00000000`00000032 00000000`00000000

At the code above0x00000218c9aeadf0It is the reference address of num. continue to use DP to see that the value on this address is hexadecimal 32, that is, decimal 50 ha.

3: Summary

In general, NETCORE was born in the era of cloud computing and virtualization that prevailed at the beginning. Its genes and mission urge it to optimize and re optimize. The smallest ant is meat, and finally c# Dafa

More high quality dry goods: see my GitHub:dotnetfly