C# 8: 可变结构体中的只读实例成员

在之前的文章中我们介绍了 C# 中的 只读结构体(readonly struct)[1] 和与其紧密相关的 in 参数[2]
今天我们来讨论一下从 C# 8 开始引入的一个特性:可变结构体中的只读实例成员(当结构体可变时,将不会改变结构体状态的实例成员声明为 readonly)。

引入只读实例成员的原因

简单来说,还是为了提升性能
我们已经知道了只读结构体(readonly struct)和 in 参数可以通过减少创建副本,来提高代码运行的性能。当我们创建只读结构体类型时,编译器强制所有成员都是只读的(即没有实例成员修改其状态)。但是,在某些场景,比如您有一个现有的 API,具有公开可访问字段或者兼有可变成员和不可变成员。在这种情形下,不能将类型标记为 readonly (因为这关系到所有实例成员)。

通常,这没有太大的影响,但是在使用 in 参数的情况下就例外了。对于非只读结构体的 in 参数,编译器将为每个实例成员的调用创建参数的防御性副本,因为它无法保证此调用不会修改其内部状态。这可能会导致创建大量副本,并且比直接按值传递结构体时的总体性能更差(因为按值传递只会在传参时创建一次副本)。

看一个例子您就明白了,我们定义这样一个一般结构体,然后将其作为 in 参数传递:

public struct Rect
{public float w;public float h;public float Area{get{return w * h;}}
}
public class SampleClass
{public float M(in Rect value){return value.Area + value.Area;}
}

编译后,类 SampleClass 中的方法 M 代码运行逻辑实际上是这样的:

public float M([In] [IsReadOnly] ref Rect value)
{Rect rect = value;  //防御性副本float area = rect.Area;rect = value;       //防御性副本return area + rect.Area;
}

可变结构体中的只读实例成员

我们把上面的可变结构体 Rect 修改一下,添加一个 readonly 方法 GetAreaReadOnly,如下:

public struct Rect
{public float w;public float h;public float Area{get{return w * h;}}public readonly float GetAreaReadOnly(){return Area; //警告	CS8656	从 "readonly" 成员调用非 readonly 成员 "Rect.Area.get" 将产生 "this" 的隐式副本。}
}

此时,代码是可以通过编译的,但是会提示一条这样的的警告:从 "readonly" 成员调用非 readonly 成员 "Rect.Area.get" 将产生 "this" 的隐式副本。
翻译成大白话就是说,我们在只读方法 GetAreaReadOnly 中调用了非只读 Area 属性将会产生 "this" 的防御性副本。用代码演示一下编译后方法 GetAreaReadOnly 的方法体运行逻辑实际上是这样的:

[IsReadOnly]
public float GetAreaReadOnly()
{Rect rect = this; //防御性副本return rect.Area;
}

所以为了避免创建多余的防御性副本而影响性能,我们应该给只读方法体中调用的属性或方法都加上 readonly 修饰符(在本例中,即给属性 Area 加上 readonly 修饰符)。

调用结构体中的只读实例成员

我们将上面的示例再修改一下:

public struct Rect
{public float w;public float h;public readonly float Area{get{return w * h;}}public readonly float GetAreaReadOnly(){return Area;}public float GetArea(){return Area;}
}public class SampleClass
{public static float CallGetArea(Rect vector){return vector.GetArea();}public static float CallGetAreaIn(in Rect vector){return vector.GetArea();}public static float CallGetAreaReadOnly(in Rect vector){//调用可变结构体中的只读实例成员return vector.GetAreaReadOnly();}
}

类 SampleClass 中定义三个方法:

  • 第一个方法是以前我们常见的调用方式;

  • 第二个以 in 参数传入可变结构体,调用非只读方法(可能修改结构体状态的方法);

  • 第三个以 in 参数传入可变结构体,调用只读方法。

我们来重点看一下第二个和第三个方法有什么区别,还是把它们的 IL 代码逻辑翻译成易懂的执行逻辑,如下所示:

public static float CallGetAreaIn([In] [IsReadOnly] ref Rect vector)
{Rect rect = vector; //防御性副本return rect.GetArea();
}public static float CallGetAreaReadOnly([In] [IsReadOnly] ref Rect vector)
{return vector.GetAreaReadOnly();
}

可以看出,CallGetAreaReadOnly 在调用结构体的(只读)成员方法时,相对于 CallGetAreaIn (调用结构体的非只读成员方法)少创建了一次本地的防御性副本,所以在执行性能上应该是有优势的。

只读实例成员的性能分析

性能的提升在结构体较大的时候比较明显,所以在测试的时候为了能够突出三个方法性能的差异,我在 Rect 结构体中添加了 30 个 decimal 类型的属性,然后在类 SampleClass 中添加了三个测试方法,代码如下所示:

public struct Rect
{public float w;public float h;public readonly float Area{get{return w * h;}}public readonly float GetAreaReadOnly(){return Area;}public float GetArea(){return Area;}public decimal Number1 { get; set; }public decimal Number2 { get; set; }//...public decimal Number30 { get; set; }
}public class SampleClass
{const int loops = 50000000;Rect rectInstance;public SampleClass(){rectInstance = new Rect();}[Benchmark(Baseline = true)]public float DoNormalLoop(){float result = 0F;for (int i = 0; i < loops; i++){result = CallGetArea(rectInstance);}return result;}[Benchmark]public float DoNormalLoopByIn(){float result = 0F;for (int i = 0; i < loops; i++){result = CallGetAreaIn(in rectInstance);}return result;}[Benchmark]public float DoReadOnlyLoopByIn(){float result = 0F;for (int i = 0; i < loops; i++){result = CallGetAreaReadOnly(in rectInstance);}return result;}public static float CallGetArea(Rect vector){return vector.GetArea();}public static float CallGetAreaIn(in Rect vector){return vector.GetArea();}public static float CallGetAreaReadOnly(in Rect vector){return vector.GetAreaReadOnly();}
}

在没有使用 in 参数的方法中,意味着每次调用传入的是变量的一个新副本; 而在使用 in 修饰符的方法中,每次不是传递变量的新副本,而是传递同一副本的只读引用。

  • DoNormalLoop 方法,参数不加修饰符,传入一般结构体,调用可变结构体的非只读方法,这是以前比较常见的做法。

  • DoNormalLoopByIn 方法,参数加 in 修饰符,传入一般结构体,调用可变结构体的非只读方法。

  • DoReadOnlyLoopByIn 方法,参数加 in 修饰符,传入一般结构体,调用可变结构体的只读方法。

使用 BenchmarkDotNet 工具测试三个方法的运行时间,结果如下:

|             Method |    Mean |    Error |   StdDev | Ratio |
|-------------------:|--------:|---------:|---------:|------:|
|       DoNormalLoop | 1.978 s | 0.0140 s | 0.0125 s |  1.00 |
|   DoNormalLoopByIn | 3.363 s | 0.0280 s | 0.0262 s |  1.70 |
| DoReadOnlyLoopByIn | 1.032 s | 0.0200 s | 0.0187 s |  0.52 |

从结果可以看出,当结构体可变时,使用 in 参数调用结构体的只读方法,性能高于其他两种; 使用 in 参数调用可变结构体的非只读方法,运行时间最长,严重影响了性能,应该避免这样调用。

总结

  • 当结构体为可变类型时,应将不会引起变化(即不会改变结构体状态)的成员声明为 readonly

  • 当仅调用结构体中的只读实例成员时,使用 in 参数,可以有效提升性能。

  • readonly 修饰符在只读属性上是必需的。编译器不会假定 getter 访问者不修改状态。因此,必须在属性上显式声明。

  • 自动属性可以省略 readonly 修饰符,因为不管 readonly 修饰符是否存在,编译器都将所有自动实现的 getter 视为只读。

  • 不要使用 in 参数调用结构体中的非只读实例成员,因为会对性能造成负面影响。


相关链接:

  1. https://mp.weixin.qq.com/s/wwVZbdY7m7da1nmIKb2jCA C# 中的只读结构体 ↩︎

  2. https://mp.weixin.qq.com/s/L73y4zdJmeT7zGTwGEJDZg C# 中的 in 参数和性能分析 ↩︎

作者 :技术译民  
出品 :技术译站(https://ITTranslator.cn/)

END

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

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

相关文章

部署Dotnet Core应用到Kubernetes(一)

最近闲了点&#xff0c;写个大活&#xff1a;部署Dotnet应用到K8s。写在前边的话一直想完成这个主题。但这个主题实在太大了&#xff0c;各种拖延症的小宇宙不时爆发一下&#xff0c;结果就拖到了现在。这个主题&#xff0c;会是一个系列。在这个系列中&#xff0c;我会讨论将应…

JAVA实验报告九异常处理_Java课后练习9(异常处理)

动手动脑1:import javax.swing.*;class AboutException {public static void main(String[] a){int i1, j0, k;ki/j;try{k i/j; // Causes division-by-zero exception//throw new Exception("Hello.Exception!");}catch ( ArithmeticException e){System.out.print…

java 实现 指派_TAP任务指派问题的汇编实现

近六周的课程设计&#xff0c;编了一个四百行的汇编程序&#xff0c;编的过程很不顺利&#xff0c;遇到种种意想不到的困难&#xff0c;但最终能够实现&#xff0c;可谓欣喜若狂&#xff0c;这期间学到了好多好多&#xff0c;遇到问题怎么精下心来解决&#xff0c;同时对汇编的…

.NET 5.0正式发布,有什么功能特性(翻译)

我们很高兴今天.NET5.0正式发布。这是一个重要的版本—其中也包括了C# 9和F# 5大量新特性和优秀的改进。微软和其他公司的团队已经在生产和性能测试环境中开始使用了。这些团队向我们反馈的结果比较令人满意&#xff0c;它证明了对性能提升及降低Web应用托管成本的机会有积极的…

简单聊聊C#中lock关键字

为了避免多个线程同时操作同一资源&#xff0c;引起数据错误&#xff0c;通常我们会将这个资源加上锁&#xff0c;这样在同一时间只能有一个线程操作资源。在C#中我们使用lock关键字来锁定资源&#xff0c;那lock关键字是如何实现锁定的呢&#xff1f;我们先看一段代码&#xf…

idea如何导入java工程_Eclipse java web项目 ,导入IntelliJ IDEA 完整操作!

或许你用惯了Eclipse&#xff0c;有点排斥其他工具了&#xff0c;你写框架的时候&#xff0c;编译速度是不是特别慢啊&#xff1f;有时候还超过45秒&#xff0c;自动取消运行&#xff01;有时候代码是正常的&#xff0c;却无端端报错&#xff1f;下午吃个饭回来又好了&#xff…

行业思考 | 互联网对传统行业的降维打击

【行业思考】| 作者 / Edison Zhou这是EdisonTalk的第301篇原创内容在周一发布的推文《我在传统行业做数字化转型之预告篇》中&#xff0c;我提到互联网的发展和和竞争对传统行业起到了降维打击的作用&#xff0c;于是就有童鞋私下问我&#xff0c;为何这么说。今天就跟你聊聊这…

BCVP开发者说第一期:Destiny.Core.Flow

沉静岁月&#xff0c;淡忘流年1项目简介Destiny.Core.FlowDestiny.Core.Flow是基于.NetCore平台&#xff0c;轻量级的模块化开发框架&#xff0c;Admin管理应用框架&#xff0c;旨在提升团队的快速开发输出能力&#xff0c;由常用公共操作类&#xff08;工具类、帮助类&#xf…

.NET Core 取消令牌:CancellationToken

在 .NET 开发中&#xff0c;CancellationToken&#xff08;取消令牌&#xff09;是一项比较重要的功能&#xff0c;掌握并合理的使用 CancellationToken 可以提升服务的性能。特别在异步编程中&#xff0c;我常常会以创建 Task 的方式利用多线程执行一些耗时或非核心业务逻辑&a…

java char short区别_java 彻底理解 byte char short int float long double

遇到过很多关于 数值类型范围的问题了&#xff0c;在这做一个总结&#xff0c;我们可以从多方面理解不同数值类型的所能表示的数值范围在这里我们只谈论 java中的数值类型首先说byte&#xff1a;这段是摘自jdk中 Byte.java中的源代码从这里可以看出 byte的取值范围&#xff1a;…

程序员过关斩将--从未停止过的系统架构设计步伐

“首先&#xff0c;这篇文章肯定会得罪一些人“其次&#xff0c;此文只代表我个人的意见&#xff0c;仅供参考从分层说起谈到系统架构的分层和系统领域边界的划分&#xff0c;每个架构师&#xff0c;每个技术经理&#xff0c;甚至每个程序员都有自己的一套想法。无论是怎么样的…

BCVP第2期:项目已完成升级.NET5.0

(是时候拿出来这种图了)1开心的锣鼓想必这两天最热闹的几个词语&#xff0c;就是c#9.0、.net5.0还有conf大会了吧&#xff0c;当然还有大一统。其实&#xff0c;早在2019年年中&#xff0c;就已经引入了.NET5.0了&#xff0c;然后从2020-03-16开始&#xff0c;就一直在说.NET5.…

如何在ASP.NetCore增加文件上传大小

关注架构师高级俱乐部开启架构之路不定期福利发放哦~架构师高级俱乐部读完需要7分钟速读仅需 3 分钟/ 如何在核心中增加文件 ASP.NET 大小 /从ASP.NET 2.0开始最大请求正文大小限制为30MB &#xff08;28.6 MiB&#xff09;。在正常情况下&#xff0c;无需增加 HTTP 请求 body …

java完全二叉树最小堆_Java实现最小堆一

Java实现最小堆一堆是一种经过排序的完全二叉树&#xff0c;其中任一非终端节点的数据值均不大于(或不小于)其左孩子和右孩子节点的值。最大堆和最小堆是二叉堆的两种形式。最大堆&#xff1a;根结点的键值是所有堆结点键值中最大者。最小堆&#xff1a;根结点的键值是所有堆结…

一个 Task 不够,又来一个 ValueTask ,真的学懵了!

一&#xff1a;背景 1. 讲故事前几天在项目中用 MemoryStream 的时候意外发现 ReadAsync 方法多了一个返回 ValueTask 的重载&#xff0c;真是日了狗了&#xff0c;一个 Task 已经够学了&#xff0c;又来一个 ValueTask&#xff0c;晕&#xff0c;方法签名如下&#xff1a;publ…

Magicodes.IE 3.0重磅设计畅谈

Magicodes.IE 3.0重磅设计畅谈总体设计图Magicodes.IE导入导出通用库&#xff0c;支持Dto导入导出、模板导出、花式导出以及动态导出&#xff0c;支持Excel、Csv、Word、Pdf和Html。IE在去年年底重构一次之后&#xff0c;经过这么长时间的迭代&#xff0c;又迎来了瓶颈。根据本…

php引用类,thinkphp引用类的使用

比如发送邮件类phpmailer1.将核心文件放入ORG目录下2.在使用的地方&#xff0c;引入这个类文件如何引入呢&#xff1f;import(.ORG.phpmailer);这个表示引入当前项目中的ORG中的phpmailer.class.php文件3.引入之后就可以使用文件中的类了public function sendEmail() {import(.…

Net5 已经来临,让我来送你一个成功

没错&#xff0c;那就是“下载成功”。现在&#xff0c;已经可以急速下载.Net5 docker 镜像 .Net 5 进行今天已经正式发布&#xff0c;想必各位已经通过各种渠道了解到了此次发布的所有内容。并且也都体会到了这次凑成三连的金 scott 是什么效果&#xff08;啊哈&#xff0c;三…

推荐几款强大流行的BI系统

高级架构师俱乐部 读完需要2分钟速读仅需 1 分钟企业在日常运营过程中&#xff0c;需要根据公司实时经营数据来做未来决测或者发现经营中的问题&#xff0c;在此过程中离不开对数据的分析&#xff0c;而平常利用 excel 等方式极大的提高了领导层快速做出决测的成本&#xff0c…

php 4位数字不足补零,php实现数字不足补0的方法

php实现数字不足补0的方法发布时间&#xff1a;2020-08-28 09:51:06来源&#xff1a;亿速云阅读&#xff1a;100作者&#xff1a;小新这篇文章将为大家详细讲解有关php实现数字不足补0的方法&#xff0c;小编觉得挺实用的&#xff0c;因此分享给大家做个参考&#xff0c;希望大…