C#語法糖系列 —— 第二篇:聊聊 ref,in 修飾符底層玩法

一線碼農發表於2022-04-25

自從 C# 7.3 放開 ref 之後,這玩法就太花哨了,也讓 C# 這門語言變得越來越多正規化,越來越重,這篇我們就來聊聊 ref,本質上來說 ref 的放開就是把 C/C++ 指標的那一套又拿回來了,而且還封裝成一套自己的玩法,下面一一解讀下。

一:方法引數上的 ref

我想設計者的初心把 ref 的功能限制的死死的,可能也考慮到 C# 是一門面向業務開發的語言,講究的是做專案快狠準,效能反而不是第一要素,這個時候的 ref 很簡單,看一下程式碼:


    class Program
    {
        static void Main(string[] args)
        {
            long price = 0;

            GetPrice(ref price);

            Console.WriteLine($"output: price={price}");
        }

        public static void GetPrice(ref long price)
        {
            price = 10;
        }
    }

output: price=10

我相信很有朋友都知道,方法引數中的 ref long price 拿的是棧地址,對棧地址上的值進行修改,自然就修改了指向這些地址上的變數,和引用型別原理一致,接下來我們從彙編角度去驗證,在 Price 方法上下一個斷點。


D:\net5\ConsoleApp4\ConsoleApp3\Program.cs @ 16:
026b048e 8d4dec          lea     ecx,[ebp-14h]
026b0491 ff15a0ebc800    call    dword ptr ds:[0C8EBA0h] (ConsoleApp3.Program.GetPrice(Int64 ByRef), mdToken: 06000002)
026b0497 90              nop
0:000> bp 026b0491
0:000> g
Breakpoint 1 hit
ChangeEngineState
eax=00000000 ebx=0057f354 ecx=0057f2d4 edx=783aaa50 esi=02979e7c edi=0057f2dc
eip=026b0491 esp=0057f2c4 ebp=0057f2e8 iopl=0         nv up ei pl zr na pe nc
cs=0023  ss=002b  ds=002b  es=002b  fs=0053  gs=002b             efl=00000246
026b0491 ff15a0ebc800    call    dword ptr ds:[0C8EBA0h] ds:002b:00c8eba0=00c2be10

從彙編的 lea ecx,[ebp-14h] 就能看到,將 ebp-14 這個單元的記憶體地址給了 ecx,這個 ecx 也就是作為引數傳遞給了 Price 方法,後續的賦值將會影響這個棧位置 上的內容。

2. 方法返回值上的 ref

這就有意思了,進入的時候傳地址,回來的時候也想傳地址,很顯然方法執行緒棧上的 值型別 是傳不出去的,畢竟方法返回後,esp,ebp 所控制的方法棧幀空間是要銷燬的,所以只能是堆上物件才能實現。

為了方便理解,看如下程式碼:


    class Program
    {
        static void Main(string[] args)
        {
            ref long price = ref GetCurrentPrice();

            price = 12;

            Console.WriteLine($"output: price={price}");
        }

        public static ref long GetCurrentPrice()
        {
            long[] nums = { 10, 20, 30 };

            return ref nums[1];
        }
    }

output: price=12

可以看到當前的 price=12,同時 nums 這個陣列也被修改了,可以用 windbg 驗證一下。


0:000> !dumpheap  -type System.Int64[] 
 Address       MT     Size
027ca7b0 04c39d00       36     

Statistics:
      MT    Count    TotalSize Class Name
04c39d00        1           36 System.Int64[]
Total 1 objects
0:000> dq 027ca7b0 L4
027ca7b0  00000003`04c39d00 00000000`0000000a
027ca7c0  00000000`0000000c 00000000`0000001e

可以看到上面的 000000000000000c 被修改成 price=12 ,這時候有人就不爽了,我不希望外面的程式碼能修改 price 內容,那怎麼辦呢? 還得在 ref 後面加上 readonly ,改造後如下:

到此時寫法就有點瘋狂了,對 C# 開發者來說很難理解,對熟悉 C/C++ 指標的朋友來說又很不習慣,太糾結了,下面是一段翻譯過來的 C/C++指標程式碼


const long long* getcurrentprice();

int main()
{
	int i = 0;

	const long long* price = getcurrentprice();

	price = 12;

	printf("num=%d, price=%d \n", i, *price);

}

const long long* getcurrentprice() {

	long long* num = new long long[3]{ 10,20,30 };
	return num + 1;
}

說實話,這程式碼看起來就清爽多了。

2. 對 ref 變數的 in 操作

這又是一套 C/C++ 的玩法,有時候不希望某一個方法對 ref 變數進行修改,注意:是不希望某一個方法進行修改,其他方法是可以的,那這個怎麼實現呢?這就需要在入參上加 in 字首,把程式碼修改一下。


    class Program
    {
        static void Main(string[] args)
        {
            ref long price = ref GetCurrentPrice();

            ModifyPrice(in price);

            Console.WriteLine($"output: price={price}");
        }

        public static ref long GetCurrentPrice()
        {
            long[] nums = { 10, 20, 30 };

            return ref nums[1];
        }

        public static void ModifyPrice(in long price)
        {
            price = 12;
            Console.WriteLine(price);
        }
    }

可以看到,這時候報錯了,如果換成 C++ 就很簡單了,只需要在引數上把 in 改成 const 即可。


void modifyprice(const long long* price) {
	*price = 12;
	printf("%d", *price);
}

總的來說,ref 這一套玩法太另類了 ???

圖片名稱

相關文章