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

今天就来和大家分享可能导致这个问题的根本原因值传递 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. 领域驱动设计(DDD)分层架构的三种模式

    模式一:四层架构 1.User Interface为用户界面层(或表示层),负责向用户显示信息和解释用户命令.这里指的用户可以是另一个计算机系统,不一定是使用用户界面的人.2.Application为 ...

  2. ScaleDet:AWS 基于标签相似性提出可扩展的多数据集目标检测器 | CVPR 2023

    论文提出了一种可扩展的多数据集目标检测器(ScaleDet),可通过增加训练数据集来扩大其跨数据集的泛化能力.与现有的主要依靠手动重新标记或复杂的优化来统一跨数据集标签的多数据集学习器不同,论文引入简 ...

  3. ABC358

    A link -- 点击查看代码 #include<bits/stdc++.h> using namespace std; string s,t; signed main(){ cin & ...

  4. 开源新纪元:Llama 3.1超大杯405B跑分惊艳,首次超越GPT-4o,下载链接曝光!

    开源巨擘Llama 3.1崭露头角,性能卓越引发热议 在科技界的瞩目下,Llama 3.1系列模型以其卓越的性能脱颖而出,尤其是其405B超大杯版本,在微软Azure-ML GitHub平台的多项评测 ...

  5. midjourney 生成相似类型图片

    生成类似形象 midjourney 核心就是一次运行3次图片,多尝试 上传此图片到discord里的mj -> 复制图片的link -> 使用describe拆词 也可以自己手动拆词,人工 ...

  6. .Net内存管理释放的两种方式

    在.Net中,资源回收主要是指内存管理和非托管资源的释放.分别提供了两种主要的方式进行处理: 垃圾回收(GC) 确认性资源释放(DRD) 官网相关文档的链接:https://learn.microso ...

  7. 【转载】 你真的理解Python中MRO算法吗?

    来自:www.xymlife.com 作者: XYM 链接:http://www.xymlife.com/2016/05/22/python_mro/ (点击阅读原文前往) ------------- ...

  8. python语言绘图:绘制一组二项分布图

    代码源自: https://github.com/PacktPublishing/Bayesian-Analysis-with-Python ============================= ...

  9. 一个好用的消息推送服务【Server 酱】

    今天给大家介绍一个好用的消息推送服务Server 酱 Server 酱简介 Server 酱是什么 「Server 酱」,英文名「ServerChan」,是一款「手机」和「服务器」.「智能设备」之间的 ...

  10. 如何将Linux的NIC 名称更改为 eth0 而不是 enps33 或 enp0s25,只要几秒钟

    概述 我们使用Linux系统,网卡名称通常都是eth0,但是有一些新的linux发行版,网卡名字 enps33 或 enp0s25. peng@ubuntu:~$ ifconfig ens33 Lin ...