C# 引用类型作为函数参数时
在探讨本文的主题之前,先来介绍下C#中的值类型和引用类型
众所周知C#中有值类型和引用类型,值类型有基础数据类型(诸如int,double,bool等)、结构体、枚举,引用类型有接口、类、委托。
值类型全部在操作系统的栈空间中申请,而引用类型则在操作系统的堆空间中建立对象,然后在栈空间中申请一个指针指向这个对象的地址。
因此C#的引用类型其实就如同C++的指针类型。
下面我再来看看函数传参的问题。
早在C时代就有函数参数传值和传地址的概念,请记住在C#中函数参数默认都是传值。
- 对于值类型,函数是将实参变量的值在栈空间复制一份然后传给形参变量。所以在函数中对形参变量的更改不会对实参变量造成任何影响,因为函数的形参只是实参的副本。
- 而对于引用类型,由于实参变量和形参变量都是引用类型,它们都指向内存堆中的某一对象的地址,函数是将实参变量指向的地址值复制了一份给形参变量,由于形参变量和实参变量指向堆中同一地址,所以在函数中使用形参变量对所指向对象所做的更改也会在实参变量中反映出来。
所以不管是值类型还是引用类型在作为参数传进函数时,其实都是传的值,只不过引用类型传的是对象在堆中的的地址罢了。
而且从上面的定义可以看出C#中引用类型的变量用C++来说就相当于是该引用类型的指针,比如有类(引用类型)RefClass:
RefClass rc就相当于是C++上的RefClass *rc
在C#中使用rc.IntValue++;时,相当于C++的rc->IntValue++;
因为引用类型在函数传参时是传地址的,所以我脑袋里就形成了一种惯性思维,认为只要传进函数的是引用类型,那么在函数中做的任何更改都会反映到实参上。但是我发现并不完全是这样,下面给出个例子(注释内容为对应等效的C++代码):
using System.Collections.Generic;
using System.Linq;
using System.Text;
namespace RefWarn
{
class RefClass
{
public int IntValue
{
get;
set;
}
}
class Program
{
static void AddValue(RefClass prc)//RefClass *prc,prc和传进来的rc指向同一个RefClass对象的地址
{
prc.IntValue++; //prc->IntValue++;
}
static void AddValue(ref RefClass prc)//RefClass **prc,prc指向传进来的rc的地址
{
prc.IntValue++;//(*prc)->IntValue++;
}
static void ChangeRef(RefClass prc)//RefClass *prc,prc和传进来的rc指向同一个RefClass对象的地址
{
prc = new RefClass() { IntValue = 1000 };
//prc=new RefClass() { IntValue = 1000 };请注意new关键字在C++中创建的是指向对象的指针不是对象
}
static void ChangeRef(ref RefClass prc)//RefClass **prc,prc指向传进来的rc的地址
{
prc = new RefClass() { IntValue = 1000 };
//*prc=new RefClass() { IntValue = 1000 };请注意new关键字在C++中创建的是指向对象的指针不是对象
}
static void Main(string[] args)
{
RefClass rc = new RefClass() { IntValue = 1 };//RefClass *rc=new RefClass() { IntValue = 1 };请注意new关键字在C++中创建的是指向对象的指针不是对象
AddValue(rc);//rc,传递指向RefClass对象的指针
Console.WriteLine("调用AddValue(rc)后IntValue为:" + rc.IntValue);
rc.IntValue = 1;
AddValue(ref rc);//&rc,传递指向RefClass对象指针的指针
Console.WriteLine("调用AddValue(ref rc)后IntValue为:" + rc.IntValue);
rc.IntValue = 1;
ChangeRef(rc);//rc,传递指向RefClass对象的指针
Console.WriteLine("调用ChangeRef(rc)后IntValue为:" + rc.IntValue);
rc.IntValue = 1;
ChangeRef(ref rc);//&rc,传递指向RefClass对象指针的指针
Console.WriteLine("调用ChangeRef(ref rc)后IntValue为:" + rc.IntValue);
}
}
}
你会发现在Main函数中调用ChangeRef(rc)后,rc并没有发生改变,其属性IntValue的值还是1。
这是为什么?我们先来看看static void ChangeRef(RefClass prc)函数的结构,看看里面都做了什么
static void ChangeRef(RefClass prc)//RefClass *prc,prc和传进来的rc指向同一个RefClass对象的地址
{
prc = new RefClass() { IntValue = 1000 };
//prc=new RefClass() { IntValue = 1000 };
}
可以看到函数里就是对RefClass 类型的形参引用变量prc重新赋了值。但是最后我们看到这个赋值并没有反应到实参引用变量rc上。原因其实很简单就像本文开始所说的一样,由于实参变量pc和形参变量rpc都是引用类型的变量,那么它们实际上是在操作系统栈空间上的两个指针,只不过指向的是操作系统堆空间上的同一个RefClass 对象。在函数ChangeRef中对引用变量rpc重新赋值,相当于是将栈中的rpc指针重新指向了堆中的另一个RefClass 对象。形参变量rpc指向的地址改变后,并不会对实参变量pc的指向发生改变,所以pc还是指向函数ChangeRef(RefClass prc)调用前的那个RefClass 对象。
但是也许你又会问为什么AddValue(rc)执行后,函数对rc做了更改呢?我们来看看AddValue(RefClass prc)函数
static void AddValue(RefClass prc)//RefClass *prc,prc和传进来的rc指向同一个RefClass对象的地址
{
prc.IntValue++; //prc->IntValue++;
}
请注意函数AddValue并不是更改了实参引用变量rc,它更改的是rc指向的RefClass 对象的属性,是因为实参变量pc和形参变量rpc都指向同一个RefClass 对象的原理,所以在AddValue里面rpc更改了它所指向RefClass 对象的属性,也就等于更改了pc指向RefClass 对象的属性。所以才在执行AddValue(rc)后给人一种好像rpc和pc是同一个变量,更改了rpc就等于更改了pc的错觉。但是请记住这是绝对错误的,rpc和pc是两个完全不同的引用变量,只不过指向的是内存中的同一个RefClass 对象。
最后我们来探讨下有没有办法使函数在传递引用类型的参数时,让形参完全等于实参呢?能否做到不管对形参是重新赋值还是做更改,都反映到实参上?
答案是肯定就是使用ref关键字
这个关键字用在值类型上的时候,就相当于C++的指针类型,比如:
ref int param
就相当于C++的
int *param
且该指针指向的就是其对应的实参变量
所以在C#中使用声明为ref的int形参变量param.ToString()时候,相当于C++上使用int指针*param.ToString()
所以在使用声明为ref的int形参param时,就相当于是C++上的*param,其操作的就是param指向的那个int变量,即实参。
而当这个关键字用在引用类型前面的时候,就相当于是指向引用类型变量的地址,而前面说过C#引用类型的变量就相当于是C++的指针,那么指向引用类型变量的地址也相当于就是指向指针的指针。
因为前面说了RefClass rc相当于C++的RefClass *rc
那么ref RefClass rc相当于C++的RefClass **rc
在C#中使用声明为ref的RefClass变量rc.ToString()时,当于C++上上使用RefClass指针的指针*rc->ToString()
所以在使用声明为ref的RefClass类型形参rc时,就相当于是C++上的*rc(注意*rc还是指针,因为rc是指向指针的指针),其操作的是形参rc指向的那个RefClass类型的引用变量(即rc指向的是实参变量的地址,而不实参变量指向堆空间中对象的地址),即实参。
而实参前面的ref相当于是C++的&符号即取该变量的地址。
所以在函数形参前加上ref那么形参变量指向的就是实参变量的地址,只不过如果实参类型是值类型,那么形参变量指向的就是该实参变量在操作系统栈中的地址。如果实参是引用类型,那么形参变量指向的也是实参变量在操作系统栈中的地址,只不过该实参变量又指向对象在操作系统堆中的地址。所以无论是引用类型还是值类型,只要在其作为形参时在前面加上ref,那么形参变量都是指向实参变量的指针,则操作形参变量就等于是在操作实参变量。
最后一定要清楚在引用类型做函数形参时,加上ref和不加ref的不同。
还是拿RefClass rc来举例:
- 不加ref时形参变量rc是指向操作系统堆空间中RefClass对象的指针,其和实参变量共同指向这个RefClass对象的地址,形参变量和实参变量之间无直接关系,通过形参变量rc对RefClass对象所做的更改,同样也可以通过实参变量看到,但是对形参变量rc本身做更改(比如改变其指向的地址),并不会对实参变量产生任何影响。
- 加上ref时形参变量rc指向的是实参变量的地址,rc直接指向实参变量,由于实参本身就是指针,所以rc就是指向指针的指针,*rc就完全等于实参变量。对更改形参便变量*rc就是更改实参变量。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 10年+ .NET Coder 心语,封装的思维:从隐藏、稳定开始理解其本质意义
· .NET Core 中如何实现缓存的预热?
· 从 HTTP 原因短语缺失研究 HTTP/2 和 HTTP/3 的设计差异
· AI与.NET技术实操系列:向量存储与相似性搜索在 .NET 中的实现
· 基于Microsoft.Extensions.AI核心库实现RAG应用
· 10年+ .NET Coder 心语 ── 封装的思维:从隐藏、稳定开始理解其本质意义
· 地球OL攻略 —— 某应届生求职总结
· 提示词工程——AI应用必不可少的技术
· Open-Sora 2.0 重磅开源!
· 周边上新:园子的第一款马克杯温暖上架