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 float CallGetArea(Rect vector)
{
return vector.GetArea();
}
public float CallGetAreaIn(in Rect vector)
{
return vector.GetArea();
}
public float CallGetAreaReadOnly(in Rect vector)
{
//调用可变结构体中的只读实例成员
return vector.GetAreaReadOnly();
}
}
类 SampleClass
中定义三个方法:
- 第一个方法是以前我们常见的调用方式;
- 第二个以
in
参数传入可变结构体,调用非只读方法(可能修改结构体状态的方法); - 第三个以
in
参数传入可变结构体,调用只读方法。
我们来重点看一下第二个和第三个方法有什么区别,还是把它们的 IL 代码逻辑翻译成易懂的执行逻辑,如下所示:
public float CallGetAreaIn([In] [IsReadOnly] ref Rect vector)
{
Rect rect = vector; //防御性副本
return rect.GetArea();
}
public 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 float CallGetArea(Rect vector)
{
return vector.GetArea();
}
public float CallGetAreaIn(in Rect vector)
{
return vector.GetArea();
}
public float CallGetAreaReadOnly(in Rect vector)
{
return vector.GetAreaReadOnly();
}
}
在没有使用 in
参数的方法中,意味着每次调用传入的是变量的一个新副本; 而在使用 in
修饰符的方法中,每次不是传递变量的新副本,而是传递同一副本的只读引用。
DoNormalLoop
方法,参数不加修饰符,传入一般结构体,调用可变结构体的非只读方法,这是以前比较常见的做法。DoNormalLoopByIn
方法,参数加in
修饰符,传入一般结构体,调用可变结构体的非只读方法。DoReadOnlyLoopByIn
方法,参数加in
修饰符,传入一般结构体,调用可变结构体的只读方法。
使用 BenchmarkDotNet 工具测试三个方法的运行时间,结果如下:
Method | Mean | Error | StdDev | Ratio | RatioSD |
---|---|---|---|---|---|
DoNormalLoop | 2.034 s | 0.0392 s | 0.0348 s | 1.00 | 0.00 |
DoNormalLoopByIn | 3.490 s | 0.0667 s | 0.0557 s | 1.71 | 0.03 |
DoReadOnlyLoopByIn | 1.041 s | 0.0189 s | 0.0202 s | 0.51 | 0.01 |
从结果可以看出,当结构体可变时,使用 in
参数调用结构体的只读方法,性能高于其他两种; 使用 in
参数调用可变结构体的非只读方法,运行时间最长,严重影响了性能,应该避免这样调用。
总结
- 当结构体为可变类型时,应将不会引起变化(即不会改变结构体状态)的成员声明为
readonly
。 - 当仅调用结构体中的只读实例成员时,使用
in
参数,可以有效提升性能。 readonly
修饰符在只读属性上是必需的。编译器不会假定 getter 访问者不修改状态。因此,必须在属性上显式声明。- 自动属性可以省略
readonly
修饰符,因为不管readonly
修饰符是否存在,编译器都将所有自动实现的 getter 视为只读。 - 不要使用
in
参数调用结构体中的非只读实例成员,因为会对性能造成负面影响。
作者 : 技术译民
出品 : 技术译站
https://www.cnblogs.com/ittranslator/p/13876180.html C# 中的只读结构体 ︎
https://www.cnblogs.com/ittranslator/p/13919691.html C# 中的 in 参数和性能分析 ︎
C# 8: 可变结构体中的只读实例成员的更多相关文章
- C结构体中数据的内存对齐问题
转自:http://www.cnblogs.com/qwcbeyond/archive/2012/05/08/2490897.html 32位机一般默认4字节对齐(32位机机器字长4字节),64位机一 ...
- 结构体中string成员的问题
在结构体中定义字符串的成员的时候要注意定义成string有时候,在某些程序中给成员赋值会崩溃,但是不确定到底什么情况会崩溃.运行报错如下: Program received signal SIGSEG ...
- 问题解决——在结构体中使用set保存结构体数据
=====================声明========================== 本文原创,转载请明确的注明出处和作者,并保持文章的完整性(包括本声明部分). 本文链接:http:/ ...
- C语言 结构体中属性的偏移量计算
//计算结构体偏移量 #include<stdio.h> #include<stdlib.h> #include<string.h> //详解:对于offscfof ...
- C语言 结构体中的成员域偏移量
//C语言中结构体中的成员域偏移量 #define _CRT_SECURE_NO_WARNINGS #include<stdio.h> #include<stdlib.h> # ...
- sturct stat 结构体中 st_mode 的含义
工作中遇到 else if( (s_buf.st_mode&S_IFMT) == S_IFDIR) return 2; else if( !(s_buf.st_mode&S_IFREG ...
- SCROLLINFO结构体中fMask和nPage的理解
还是VC++中有关显示图像的问题. 我们在显示一幅比较大的图像时,要使用带标准滚动条的对话框.涉及对滚动条的操作就不得不提SCROLLINFO这个结构体.只看单词意思就这道这个结构体要储存滚动条的一些 ...
- C语言结构体中的函数指针
这篇文章简单的叙述一下函数指针在结构体中的应用,为后面的一系列文章打下基础 本文地址:http://www.cnblogs.com/archimedes/p/function-pointer-in ...
- c语言结构体中的冒号的用法
结构体中常见的冒号的用法是表示位域. 有些信息在存储时,并不需要占用一个完整的字节, 而只需占几个或一个二进制位.例如在存放一个开关量时,只有0和1 两种状态, 用一位二进位即可.为了节省 ...
随机推荐
- 【题解】CF1375D Replace by MEX
\(\color{purple}{Link}\) \(\text{Solution:}\) 观察到题目要求操作次数不超过\(2n,\)且不必最小化操作次数,所以一定是构造题. 考虑将序列转化为\([0 ...
- npm包管理器报错-npm ERR! Response timeout while trying to fetch https://registry.npmjs.org/@XXX(over 30000ms)
由于这两天买的新电脑在短期内频频蓝屏.卡机,不得不把自己其他的本本拿出来换上,但是程序员换电脑是真的痛苦,其他不说就说一个配环境 真的折腾哈 我是一名前端菜鸟,现在自己的本本上使用的是npm包管理工具 ...
- C# 软件版本号
如果需要查看更多文章,请微信搜索公众号 csharp编程大全,需要进C#交流群群请加微信z438679770,备注进群, 我邀请你进群! ! ! --------------------------- ...
- postgreSQL与Kingbase 字符串裁剪区别
--postgreSQL postgres=# select substring('abcdefg',0,4); substring abc (1 行记录) postgres=# select sub ...
- Sqlite嵌入式数据库讲解
在计算机系统中,保存数据的方式一般有两种:1. 普通文件方式2. 数据库方式 相比于普通文件方式,使用数据库来管理大批量数据具有更高的效率与安全性. 数据库系统一般由3个部分构成1. 数据库2. 数据 ...
- mqtt网关
MQTT网关 MQTT网关是可以是将普通的串口数据.Modbus RTU数据等转化为MQTT协议的从而方便与平台的对接,通过连接服务器.订阅和发布主题来实现传统设备和MQTT云端的联系.例如,笔记本和 ...
- 分布式系统中的CAP、ACID、BASE概念
目录 CAP ACID BASE CAP 分布式系统中,这三个特性只能满足其中两个. 一致性(Consistency):分布式中一致性又分强一致性和弱一致性,强一致性主浊任何时刻任何节点看到的数据都是 ...
- 51Nod 最大M子段和系列 V1 V2 V3
前言 \(HE\)沾\(BJ\)的光成功滚回家里了...这堆最大子段和的题抠了半天,然而各位\(dalao\)们都已经去做概率了...先%为敬. 引流之主:老姚的博客 最大M子段和 V1 思路 最简单 ...
- ansible的copy模块应用(ansible 2.9.5)
一,copy模块的作用: 复制文件到受控的远程主机 说明:刘宏缔的架构森林是一个专注架构的博客,地址:https://www.cnblogs.com/architectforest 对应的源码可以访问 ...
- C++ Primer第5版 第二章课后练习
练习2.1 C++ 语言规定short 和 int 至少 16 位, long 至少32位, long long 至少64位.带符号类型可以表示整数.负数或0, 无符号类型则仅能表示大于等于0的值Th ...