首页 星云 工具 资源 星选 资讯 热门工具
:

PDF转图片 完全免费 小红书视频下载 无水印 抖音视频下载 无水印 数字星空

C#|.net core 基础 - 值传递 vs 引用传递

编程知识
2024年09月19日 00:42

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

今天就来和大家分享可能导致这个问题的根本原因值传递 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

From:https://www.cnblogs.com/hugogoos/p/18419656
本文地址: http://shuzixingkong.net/article/2130
0评论
提交 加载更多评论
其他文章 C# + WPF 音频播放器 界面优雅,体验良好
前言 本文介绍一款使用 C# 与 WPF 开发的音频播放器,其界面简洁大方,操作体验流畅。该播放器支持多种音频格式(如 MP4、WMA、OGG、FLAC 等),并具备标记、实时歌词显示等功能。 另外,还支持换肤及多语言(中英文)切换。核心音频处理采用 FFmpeg 组件,获得了广泛认可,目前 Git
C# + WPF 音频播放器 界面优雅,体验良好 C# + WPF 音频播放器 界面优雅,体验良好 C# + WPF 音频播放器 界面优雅,体验良好
vivo 全链路多版本开发测试环境落地实践
作者:来自 vivo 互联网研发效能团队- Wang Kang 测试环境全链路多版本部署,解决多测试环境资源争抢等问题。 一、背景介绍 软件系统中全链路指的是从用户请求发起,到最终返回响应的整个过程中所涉及到的所有环节和组件。在微服务软件架构风格盛行的今天,因为微服务独立部署、松耦合等特性,往往一个
vivo 全链路多版本开发测试环境落地实践 vivo 全链路多版本开发测试环境落地实践 vivo 全链路多版本开发测试环境落地实践
升讯威在线客服系统如何高性能同时支持 MySQL 和 SQL Server
详细介绍升讯威在线客服系统是如何高性能同时支持 MySQL 和 SQL Server 的,经过实践验证,可在低配服务器上无压力支持超 2000 人同时在线。
升讯威在线客服系统如何高性能同时支持 MySQL 和 SQL Server 升讯威在线客服系统如何高性能同时支持 MySQL 和 SQL Server 升讯威在线客服系统如何高性能同时支持 MySQL 和 SQL Server
十七,Spring Boot 整合 MyBatis 的详细步骤(两种方式)
十七,Spring Boot 整合 MyBatis 的详细步骤(两种方式) @目录十七,Spring Boot 整合 MyBatis 的详细步骤(两种方式)1. Spring Boot 配置 MyBatis 的详细步骤2. 最后: MyBatis 的官方文档:https://mybatis.p2hp
十七,Spring Boot 整合 MyBatis 的详细步骤(两种方式) 十七,Spring Boot 整合 MyBatis 的详细步骤(两种方式) 十七,Spring Boot 整合 MyBatis 的详细步骤(两种方式)
OAuth2.0授权-gitee授权码模式
OAuth2.0授权验证-gitee授权码模式 本文主要介绍如何笔者自己是如何使用gitee提供的OAuth2.0协议完成授权验证并登录到自己的系统,完整模式如图 1、创建应用 打开gitee个人中心->第三方应用->创建应用 创建应用后在我的应用界面,查看已创建应用的Client ID
OAuth2.0授权-gitee授权码模式 OAuth2.0授权-gitee授权码模式
从零开始掌握Kubernetes: Pod和Deployment的幕后故事
1. 引言 在如今的技术世界中,随着微服务架构的广泛应用和云原生理念的兴起,应用程序的开发、部署和管理发生了翻天覆地的变化。容器技术的出现使得开发者可以轻松地将应用及其所有依赖打包在一个轻量级、可移植的容器中,这种方式大大提升了应用的部署效率和一致性。然而,随着应用规模的扩大和微服务数量的增加,管理
k8s 中的 Ingress 简介【k8s 系列之三】
Ingress 的重要性不言而喻,它不仅统一了集群对外访问的入口,还提供了高级路由、七层负载均衡、SSL终止等关键功能,同时支持动态配置更新、灰度发布等高级特性。下文将进行详细介绍。
k8s 中的 Ingress 简介【k8s 系列之三】 k8s 中的 Ingress 简介【k8s 系列之三】 k8s 中的 Ingress 简介【k8s 系列之三】
c语言 宏的一些深层应用(##,#,宏函数)
"##" 宏拼接 #define CONCATENATE(a, b) a ## b CONCATENATE(student_, 1) // 将a和b拼接起来变成一个新的变量 -> student_1 #define CONS(a,b) int(a##e##b) CONS(2