dotnet C# 警惕可空结构体的方法内部赋值无效

news/2024/11/15 16:00:40/文章来源:https://www.cnblogs.com/lindexi/p/18078081

本文将记录一个 C# dotnet 里的一个稍微隐藏的行为,那就是如果有一个结构体存在某个的方法,此方法的作用是修改结构里面的字段或属性的值,那此时将会在可空的结构体调用此方法时,发现没有真正修改到可空结构体局部变量本身

其实这个问题非常好理解,只不过可能在编写代码的时候,由于语法原因,可能不小心才会踩到这样的坑。先来讲讲我踩到这个坑的故事,这是我在编写一个 WPF 应用程序时,我有一段逻辑代码,我需要将一个 WPF 的 Rect 类型进行 Union 一个点,从而求出加入包含某个点的矩形范围

简单的编写代码如下

        Rect? rect1 = new Rect(10, 10, 10, 10);rect1.Value.Union(new Point(100, 100));

以上代码的 rect1.Value.Union 则是将传入的点参数加入到 Rect 包含范围里面,将会在 Union 方法里面修改 Rect 的宽度高度和 X 和 Y 坐标

预期以上代码的能够将 Rect 的范围,也就是右下角坐标放大到 100x100 的坐标,然而通过以下代码输出到控制台时,却发现结果不符合预期

        Console.WriteLine($"{rect1.Value.X} {rect1.Value.Y} {rect1.Value.Width} {rect1.Value.Height}");

以上控制台输出的内容如下

10 10 10 10

可以看到 rect1 局部变量依然保持初始的值

此时我以为是代码哪里没有写对,我就写了一个非可空的 rect2 变量

        Rect rect2 = new Rect(10, 10, 10, 10);

依然和 rect1 一样调用 Union 方法

        rect2.Union(new Point(100, 100));

此时的输出就符合预期了

        Console.WriteLine($"{rect2.X} {rect2.Y} {rect2.Width} {rect2.Height}");

以上代码输出的是

10 10 90 90

意味着右下角坐标放大到 100x100 的坐标

这里需要提一下的是 WPF 的坐标系是左上角是坐标 0 点,从左往右 X 越来越大,从上到下 Y 越来越大

那这究竟是为什么呢?为什么可空会有此影响呢?为了了解这个问题,防止是 WPF 的 Rect 投毒,咱自己编写一个名为 Foo 的结构体,在这个结构体里面添加一个方法,用于修改结构体里面的属性

struct Foo
{public int Number {  set; get; }public void SetNumber(int value) => Number = value;
}

尝试调用 SetNumber 方法给可空结构体赋值,如以下代码

        Foo? foo = new Foo();foo.Value.SetNumber(100);Console.WriteLine(foo.Value.Number);

运行以上代码,可以看到控制台输出的是 0 的值,也就是说 SetNumber 方法没有能够给 foo 局部变量的 Number 属性赋值

其实如果大家尝试不通过 SetNumber 赋值,而是直接对 Number 属性赋值,就能看到其实在 dotnet 里面已经尝试给出拦截了,如以下代码将会提示构建失败

        foo.Value.Number = 100;

以上代码会构建失败,提示如下

error CS1612: 无法修改“Foo?.Value”的返回值,因为它不是变量

这是因为 foo.Value.Number = 100; 这句话里面隐式包含了从 foo 可空类型里面取出 Value 的代码。根据 C# 基础知识可以知道,局部变量获取结构体就是获取结构体的一份在栈上的拷贝

换句话说就是如果想要获取一个结构体的拷贝可以如何做?获取一个结构体或准确来说一个值类型的拷贝可以直接通过局部变量赋值,赋值就是拷贝的过程,如 int a = b; 一样,就让 a 获取了 b 的拷贝值

于是 foo.Value 其实就是隐藏了一个获取 foo 可空类型的 Value 内容的隐藏的变量,如果此时写 foo.Value.SetNumber(100) 则是对隐藏的变量调用 SetNumber 方法,自然修改的是这个隐藏的变量,而不是 foo 可空类型本身的结构体的值

从 IL 层面可以更好看出来 foo.Value.SetNumber(100) 这句话的实际逻辑

    IL_0011: ldloca.s     fooIL_0013: call         instance !0/*valuetype DurkalbaliNerkalcemya.Foo*/ valuetype [System.Runtime]System.Nullable`1<valuetype DurkalbaliNerkalcemya.Foo>::get_Value()IL_0018: stloc.1      // V_1IL_0019: ldloca.s     V_1IL_001b: ldc.i4.s     100 // 0x64IL_001d: call         instance void DurkalbaliNerkalcemya.Foo::SetNumber(int32)IL_0022: nop

可以看到这里其实有一个隐藏的名为 V_1 的局部变量,大概实际的运行的代码如下

        var temp = foo.Value;temp.SetNumber(100);

从以上的代码相信大家也就知道为什么可空结构体的方法对内部的属性赋值无效的原因了,从 var temp = foo.Value; 这一句其实就获取了结构体的拷贝了,之后 SetNumber 的对内部属性的赋值自然就无法影响到可空类型里面的结构体了

这是一个很简单的基础的 C# 结构体值类型的知识,只是可能有时写成一句话了,就没看出来

以上的 foo.Value.SetNumber(100) 的符合预期的行为的改法如下

        Foo temp = foo.Value;temp.SetNumber(100);foo = temp;

相对来说需要多写几句话

现在有了 record 和 readonly struct 的出现,很多时候结构体从设计上都不会让方法去修改自身,大部分推荐的都是返回新的结构体回来让原本的结构体保持不变

按照以上方式的优化如下

readonly record struct Foo(int Number)
{public Foo SetNumber(int value) => this with { Number = value };
}

优化之后的代码依然十分简单,此时的赋值代码就可以修改为如下逻辑

        Foo? foo = new Foo();foo = foo.Value.SetNumber(100);

由于是直接修改 foo 可空类型局部变量本身,自然就可以完成进行赋值

本文代码放在 github 和 gitee 上,可以使用如下命令行拉取代码

先创建一个空文件夹,接着使用命令行 cd 命令进入此空文件夹,在命令行里面输入以下代码,即可获取到本文的代码

git init
git remote add origin https://gitee.com/lindexi/lindexi_gd.git
git pull origin 066cae4e4f6aa4f31d3e43eca9c278aa7b546b60

以上使用的是 gitee 的源,如果 gitee 不能访问,请替换为 github 的源。请在命令行继续输入以下代码,将 gitee 源换成 github 源进行拉取代码

git remote remove origin
git remote add origin https://github.com/lindexi/lindexi_gd.git
git pull origin 066cae4e4f6aa4f31d3e43eca9c278aa7b546b60

获取代码之后,进入 DurkalbaliNerkalcemya 文件夹,即可获取到源代码

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.hqwc.cn/news/795844.html

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

南沙C++信奥老师解一本通题: 1206:放苹果

​【题目描述】把M个同样的苹果放在N个同样的盘子里,允许有的盘子空着不放,问共有多少种不同的分法?(用K表示)5,1,1和1,5,1 是同一种分法。【输入】第一行是测试数据的数目t(0<=t<=20)。以下每行均包含二个整数M和N,以空格分开。1<=M,N<=10。【输出】…

ServiceMesh 1:大火的云原生微服务网格,究竟好在哪里?

1 关于云原生 云原生计算基金会(Cloud Native Computing Foundation, CNCF)的官方描述是: 云原生是一类技术的统称,通过云原生技术,我们可以构建出更易于弹性扩展、极具分布式优势的应用程序。这些应用可以被运行在不同的环境当中,比如说 私有云、公有云、混合云、还有多…

数据库容灾等级

数据库容灾等级 在信息化时代,企业的数据安全和业务连续性变得至关重要,容灾备份作为确保数据不丢失和业务不中断的重要措施备受关注。 我们通常将容灾备份分为四个等级,从最基本的本地备份到复杂的异地多活系统,每个等级的特点和适用场景各不相同。下面我们就来详细了解一…

Visual Studio 查看项目的能力

在 Visual Studio 里面,可以在项目里面通过配置 DiagnoseCapabilities 查看项目的能力。什么是项目的能力?项目的能力就是对当前项目来说,可以具备 VS 支持的功能,项目功能是确定项目类型、平台和特性的推荐方法查看项目的能力的功能只适合于框架开发者使用,用于了解当前的…

vue3 setup语法糖 扩展

安装扩展 npm i vite-plugin-vue-setup-extend 修改配置文件接下啦就可以直接在标签中写name了

ISCC 2024 部分WP

练武题 WEB 还没想好名字的塔防游戏 题目中给了塔防游戏的github原项目地址。下载题目的网页源代码,和github项目对比,发现基本只加了world.js里的三个提示。Cats Craft Scarves Ivory Towers Twinkle Dragons Whisper Secrets提示不知道是什么意思。但是看首字母有点奇怪,另…

protobuf cmake Visual Studio 编译安装 (全命令行)

protobuf cmake Visual Studio 编译安装 (全命令行)protobuf cmake Visual Studio 编译安装 中间踩了挺多的坑的, 这篇文章记录一下. 重要前言: 所有在引用框中的命令都不要输入!!cmake --install . # 在引用框中的不要输入到命令行cmake --install . --config Debug # 命令…

陈彦吉的第一次作业

这个作业属于哪个课程 https://edu.cnblogs.com/campus/zjlg/rjjc这个作业的目标 向教师和助教介绍自己,阐述自己期望的课程收获和扮演的课程实践角色姓名-学号 陈彦吉 2022329301139一、自我介绍 (一)基本信息 我叫陈彦吉,来自浙江台州,是2022级电气工程及其自动化(2)班…

(16)USB通信

USB协议讲解(大范围讲解) USB,英文全称 Universal Serial Bus(通用串行总线),是一种支持热插拔的高速串行传输总线(目前已发展至3.0) USB体系包括主机、设备以及物理连接三部分,其中: 主机是一个提供USB接口以及接口管理能力的硬件、软件及固件复合体,可以使PC,也可…

Redis 入门 - C#|.NET Core客户端库六种选择

介绍了6款.NET系Redis客户端库:ServiceStack.Redis、StackExchange.Redis、CSRedisCore、FreeRedis、NewLife.Redis、BeetleX.Redis,各具特色,如商业支持、高性能、高并发、低延迟等,适合不同场景和需求。经过前面的Redis基础学习,今天正式进入编码阶段了,进入编码阶段我…

[NLP] 知识抽取技术

1 概述:知识抽取 定义知识抽取通常指从非结构化文本中挖掘结构化信息。例如,含有丰富语义信息的标签和短语。 这在业界被广泛应用于内容理解和商品理解等场景,通过从用户生成的文本信息中提取有价值的标签,将其应用于内容或商品上知识抽取通常伴随着对所抽取标签或短语的分…

如何用 Scrapy 爬取网站数据并在 Easysearch 中进行存储检索分析

做过数据分析和爬虫程序的小伙伴想必对 Scrapy 这个爬虫框架已经很熟悉了。今天给大家介绍下,如何基于 Scrapy 快速编写一个爬虫程序并利用 Easysearch 储存、检索、分析爬取的数据。我们以极限科技的官网 Blog 为数据源,做下实操演示。 安装 scrapy 使用 Scrapy 可以快速构建…