不知道你在开发过程中有没有遇到过这样的困惑:这个变量怎么值被改?这个值怎么没变?

今天就来和大家分享可能导致这个问题的根本原因值传递 vs 引用传递。

在此之前我们先回顾两组基本概念:

值类型 vs 引用类型

值类型: 直接存储数据,数据存储在栈上;

引用类型: 存储数据对象的引用,数据实际存储在堆上。

形参 vs 实参

形参: 即形式参数,表示调用方法时,方法需要你传递的值。方法声明定义了其形参。也就是说在定义方法时,紧跟在方法名后面括号中的参数列表就是形参。

实参: 即实际参数,表示调用方法时,你传递给方法形参的值。调用代码在调用过程时提供实参。也就是说在调用方法时,紧跟在方法名后面括号中的参数列表就是实参。

再来回顾一下值类型和引用类型在内存中是怎么存储的呢?

对于值类型变量的值直接存储在栈中,如下图的int a=10,10就直接存在栈空间中,而其栈空间对应的内存地址为0x66666668;对于引用类型变量本身存储的是实例对象的引用,即实例对象在堆中的实际内存地址,因此引用类型变量是存储其实例对象的引用于栈上,如下图中变量Test a在栈中实际存储的是实例对象Test a在堆中的内存地址0x88888880,而栈空间对应的内存地址为0x66666668。

栈也是有内存地址的,这一点很重要,无论栈空间上存储的是值还是引用地址,这个栈空间本身也有自己对应的内存地址。

什么是值传递?什么是引用传递?

值传递:如果变量按值传递给方法,则会把变量的副本传递给方法。对于值类型则把变量的副本传递给方法,对于引用类型则把变量的引用的副本传递给方法。因此被调用方法参数会创建一个新的内存地址用于接收存储变量,因此在方法内部对变量修改并不会影响原来的值。

引用传递:如果变量按引用传递给方法,则会把变量的引用传递给方法,对于值类型则把变量的栈空间地址传递给方法,对于引用类型则把变量的引用的栈空间地址传递给方法。因此被调用方法参数不会创建一个新的内存地址用于接收存储变量,意味着形参与实参共同指向相同的内存地址,因此在方法内部修对变量修改会影响原来的值。

上面的描述可能有点拗口,下面我们在基于值类型、引用类型、值传递、引用传递各种组合进行一个详细说明。

01、值类型按值传递

当值类型按值传递时,调用者会把值类型变量的副本传递给方法,因此被调用方法参数会创建一个新的内存地址用于接收存储变量,因此当在方法内部对参数进行修改时并不会影响调用者调用处的值类型变量。

传递值类型变量的副本就是相当于在栈上,又复制了一个同样的值,而且内存地址还不一样,所以互不影响。如下图把a赋值给b,则b直接新开辟了一个栈空间,虽然a和b都是10,但是它们在不同的地址空间中,因此如果他们各自被修改了,也互不影响。

下面我们写个例子演示一下,这个例子就是定义个变量a并赋值,然后调用一个方法此方法内对传进来的参数a进行加1,具体代码如下:

public static void ValueByValueRun()
{
var a = 10;
Console.WriteLine($"调用者-调用方法前 a 值:{a}");
ChangeValueByValue(a);
Console.WriteLine($"调用者-调用方法后 a 值:{a}");
}
public static void ChangeValueByValue(int a)
{
Console.WriteLine($" 被调用方法-接收到 a 值:{a}");
a = a + 1;
Console.WriteLine($" 被调用方法-修改后 a 值:{a}");
}

运行结果如下:

通过代码执行结果可以发现,方法内对变量的修改已经生效,但是不没有影响到调用者调用处的变量值。

02、引用类型按值传递

当引用类型按值传递时,调用者会把引用类型变量的引用副本传递给方法,因此被调用方法参数会创建一个新的内存地址用于接收存储变量,而对于一个引用类型变量来说其本身存储的就是引用类型实例对象的引用副本,而方法接收到的也是此变量引用的副本,所以调用者参数和被调用方法参数是引用了同一个实例对象的两个引用副本。如下图Test a可以理解为调用者传的实参,Test b可以理解为被调用方法定义的形参,这两个参数都只是指向堆中Test a的引用副本。

因此可以得出两个结论:

1、变量a和b都是指向实例对象Test a的引用,所以无论变量a或b,只要有一个更新了实例成员则另一个变量也会同步发生变化。

2、虽然变量a和b都是指向实例对象Test a的引用,但是他们存储在栈上的内存地址却不同,因此如果他们各种重新分配实例也就是new一个新对象,则另一个变量无法感知到还是保持原因状态不变。

我们先用代码说明第一个结论:

public static void ChangeReferenceByValueRun()
{
var a = new Test
{
Age = 10
};
Console.WriteLine($"调用者-调用方法前 a.Age 值:{a.Age}");
ChangeReferenceByValue(a);
Console.WriteLine($"调用者-调用方法后 a.Age 值:{a.Age}");
}
public static void ChangeReferenceByValue(Test a)
{
Console.WriteLine($" 被调用方法-接收到 a.Age 值:{a.Age}");
a.Age = a.Age + 1;
Console.WriteLine($" 被调用方法-修改后 a.Age 值:{a.Age}");
}

运行结果如下:

可以看到被调用方法中a实例对象的Age属性发生变化后,调用者中变量也同步发生了变化。

对于第二个结论我们这样论证,在方法中直接对参数new一个新对象,看看原变量是否发生变化,代码如下:

public static void NewReferenceByValueRun()
{
var a = new Test
{
Age = 10
};
Console.WriteLine($"调用者-调用方法前 a.Age 值:{a.Age}");
NewReferenceByValue(a);
Console.WriteLine($"调用者-调用方法后 a.Age 值:{a.Age}");
}
public static void NewReferenceByValue(Test a)
{
Console.WriteLine($" 被调用方法-接收到 a.Age 值:{a.Age}");
a = new Test
{
Age = 100
};
Console.WriteLine($" 被调用方法-new后 a.Age 值:{a.Age}");
}

执行结果如下:

可以发现当在方法中对变量执行new操作后,调用者处的变量并没有发生变化。

为什么会这样呢?因为对于引用类型来说,形参和实参是对引用类型的实例对象引用的两个副本,而这两个副本存储在栈上又分别在不同的内存地址空间上,而new主要就是重新分配内存,这就导致形参变量a=new后,栈上形参变量a指向了Test新的实例对象的引用,而实参变量a还是保持原有实例对象引用不变。

如下图所示。

03、值类型按引用传递

当值类型按引用传递时,调用者会把值类型变量对应的栈空间地址传递给方法,因此被调用方法参数不会创建一个新的内存地址用于接收存储变量,因此当在方法内部对参数进行修改时并同样会影响调用者调用处的值类型变量。

传递值类型变量对应的栈空间地址就意味着形参与实参共同指向相同的内存地址,所以才导致对形参修改时,实参也会同步发生变化。

我们用一个小例子演示一下:

public static void ValueByReferenceRun()
{
Console.WriteLine($"值类型按引用传递");
var a = 10;
Console.WriteLine($"调用者-调用方法前 a 值:{a}");
ChangeValueByReference(ref a);
Console.WriteLine($"调用者-调用方法后 a 值:{a}");
}
public static void ChangeValueByReference(ref int a)
{
Console.WriteLine($" 被调用方法-接收到 a 值:{a}");
a = a + 1;
Console.WriteLine($" 被调用方法-修改后 a 值:{a}");
}

执行结果如下:

可以发现调用者处的值类型变量已经发生改变。

04、引用类型按引用传递

当引用类型按引用传递时,调用者会把引用类型变量对应的栈空间地址传递给方法,因此被调用方法参数不会创建一个新的内存地址用于接收存储变量,因此当在方法内部对参数进行修改时并同样会影响调用者调用处的引用类型变量。

传递引用类型变量对应的栈空间地址就意味着形参与实参共同指向相同的内存地址,因此对形参修改时,实参也会同步发生变化,而且这个里的修改不单单指修改实例成员,还包括new一个新实例对象。

下面我们看一个修改实例成员的例子:

public static void ChangeReferenceByReferenceRun()
{
Console.WriteLine($"引用类型按引用传递 - 修改实例成员");
var a = new Test
{
Age = 10
};
Console.WriteLine($"调用者-调用方法前 a.Age 值:{a.Age}");
ChangeReferenceByReference(ref a);
Console.WriteLine($"调用者-调用方法后 a.Age 值:{a.Age}");
}
public static void ChangeReferenceByReference(ref Test a)
{
Console.WriteLine($" 被调用方法-接收到 a.Age 值:{a.Age}");
a.Age = a.Age + 1;
Console.WriteLine($" 被调用方法-修改后 a.Age 值:{a.Age}");
}

执行结果如下:

再看看new一个新对象的例子:

public static void NewReferenceByReferenceRun()
{
Console.WriteLine($"引用类型按引用传递 - new 新实例");
var a = new Test
{
Age = 10
};
Console.WriteLine($"调用者-调用方法前 a.Age 值:{a.Age}");
NewReferenceByReference(ref a);
Console.WriteLine($"调用者-调用方法后 a.Age 值:{a.Age}");
}
public static void NewReferenceByReference(ref Test a)
{
Console.WriteLine($" 被调用方法-接收到 a.Age 值:{a.Age}");
a = new Test
{
Age = 100
};
Console.WriteLine($" 被调用方法-new后 a.Age 值:{a.Age}");
}

执行结果如下:

另外string是一个特殊的引用类型,string类型变量的按值传递和按引用传递和值类型是一致的,也就是要把string类型当值类型一样看待就行。string类型的特殊性我们后面会单独具体介绍。

在C#中以下修饰符可应用与参数声明,并且会使得参数按引用传递:ref、out、readonly ref、in。对于每个修饰符具体怎么使用就不再这里细说了。

相信到这里你应该就可以回答我之前在《LeetCode题集-2 - 两数相加》最后提的问题了。

:测试方法代码以及示例源码都已经上传至代码库,有兴趣的可以看看。https://gitee.com/hugogoos/Planner

C#|.net core 基础 - 值传递 vs 引用传递的更多相关文章

  1. C#基础原理拾遗——引用类型的值传递和引用传递

    C#基础原理拾遗——引用类型的值传递和引用传递 以前写博客不深动,只搭个架子,像做笔记,没有自己的思考,也没什么人来看.这个毛病得改,就从这一篇开始… 最近准备面试,深感基础之重要,奈何我不是计算机科 ...

  2. C#学习笔记(基础知识回顾)之值传递和引用传递

    一:要了解值传递和引用传递,先要知道这两种类型含义,可以参考上一篇 C#学习笔记(基础知识回顾)之值类型和引用类型 二:给方法传递参数分为值传递和引用传递. 2.1在变量通过引用传递给方法时,被调用的 ...

  3. C#基础原理拾遗——引用类型的值传递和引用传递

    以前写博客不深动,只搭个架子,像做笔记,没有自己的思考,也没什么人来看.这个毛病得改,就从这一篇开始- 最近准备面试,深感基础之重要,奈何我不是计算机科班出身,基础方面有些捉襟见肘.短期怎么补?做面实 ...

  4. java基础 - 形参和实参,值传递和引用传递

    形参和实参 形参:就是形式参数,用于定义方法的时候使用的参数,是用来接收调用者传递的参数的. 形参只有在方法被调用的时候,虚拟机才会分配内存单元,在方法调用结束之后便会释放所分配的内存单元. 因此,形 ...

  5. GO语言基础---值传递与引用传递

    package main import ( "fmt" ) /* 值传递 函数的[形式参数]是对[实际参数]的值拷贝 所有对地址中内容的修改都与外界的实际参数无关 所有基本数据类型 ...

  6. Java面向对象-方法的值传递和引用传递

    Java面向对象-方法的值传递和引用传递 0 发布时间:『 2016-08-21 14:21』  博客类别:Java核心基础  阅读(197) 评论(0) Java面向对象-方法的值传递和引用传递 方 ...

  7. Java是值传递还是引用传递?

    Java的值传递和引用传递在面试中一般都会都被涉及到,今天我们就来聊聊这个问题.这个问题一般是相对函数而言的,也就是Java中所说的方法参数,那么我们先来回顾一下在程序设计语言中有关参数传递给方法的两 ...

  8. 堆栈详解 + 彻底理解Java的值传递和引用传递

    本文旨在用最通俗的语言讲述最枯燥的基本知识 学过Java基础的人都知道:值传递和引用传递是初次接触Java时的一个难点,有时候记得了语法却记不得怎么实际运用,有时候会的了运用却解释不出原理,而且坊间讨 ...

  9. 【Java】 参数的传递:值传递与引用传递讨论

    内容稍多,可直接看第4点的讨论结果 前言 在涉及到传递参数给方法时,容易出现一些参数传递错误的问题,这就涉及到了参数的传递问题,必须搞清楚:参数是如何传递到方法中的?一般来说,参数的传递可以分为两种: ...

  10. JavaScript传递变量:值传递?引用传递?

    今天在看 seajs-2.2.1/src/util-events.js源码,里面有段代码不是很理解: var events = data.events = {} // Bind event seajs ...

随机推荐

  1. oeasy教您玩转vim - 10 - # 插入新行

    插入新行 回忆上节课内容 i.I 在前面插入文本 a.A 在后面插入文本 o.O 换行插入文本 o 其实是 A 回车 O 其实是 k o O 也是 k A 回车 模式切换小技巧 比如你在一句话的中间, ...

  2. 从30个角度对比 PostgreSQL 和 MySQL

    比较版本:PostgreSQL 11    VS      MySQL5.7(innodb引擎) Oracle官方社区版 版权情况:PostgreSQL 11(免费开源).MySQL5.7 Oracl ...

  3. vue项目读取文件问题

    问题:在src\assets资源目录存放非图片文件无法获取. 解决:将非图片文件存放到public上,读取的时候路径不带public. 例如:资源的相对路径为:public/roboto/1Kg.wo ...

  4. 使用nvm管理node

    安装包在阿里云盘 安装时,记录安装位置 安装后,在安装目录的setting添加镜像地址 node_mirror:npm.taobao.org/mirrors/node/ npm_mirror:npm. ...

  5. canvas实现截图功能

    开篇 最近在做一个图片截图的功能. 因为工作时间很紧张, 当时是使用的是一个截图插件. 周末两天无所事事,来写一个简单版本的截图功能. 因为写的比较简单,如果写的不好,求大佬轻一点喷 读取图片并获取图 ...

  6. pycham配置GitHub环境【一文了解window上GitHub的基本操作】

    基础用户设置[包含用户登录.密钥生成] 网络配置 外观->系统设置->https代理->检查连接 我这里测试网址是GitHub,连接成功即可后续操作[不成功别找我,我也不知道] gi ...

  7. 【Dos-BatchPrograming】04

    --1.PING 主机联通性检测 Microsoft Windows [版本 10.0.19041.746] (c) 2020 Microsoft Corporation. 保留所有权利. C:\Us ...

  8. pytorch报错:Variable._execution_engine.run_backward( # Calls into the C++ engine to run the backward pass RuntimeError: FIND was unable to find an engine to execute this computatio

    GPU模式下运行pytorch代码报错,pytorch为2.2.1,NVIDIA驱动版本535.161.07 File "/home/devil/anaconda3/envs/sample- ...

  9. AI开源是否应该完全开源?AI的完全开源是否可以实现?

    看了一个视频: 袁进辉:零代码改动,加速AIGC 里面提到了一个完全开源的概念,感觉有些意思,虽然觉得可实现性不高,嘿嘿嘿!!! AI的完全开源: 训练数据开源.数据清洗过程开源.模型权重开源.项目代 ...

  10. 【转载】 nvidia-smi - Persistence-M (Persistence Mode)

    原文链接: https://blog.csdn.net/chengyq116/article/details/103224622 版权声明:本文为CSDN博主「Yongqiang Cheng」的原创文 ...