C# 11 对 ref 和 struct 的改进

前言

C# 11 中即将到来一个可以让重视性能的开发者狂喜的重量级特性,这个特性主要是围绕着一个重要底层性能设施 ref 和 struct 的一系列改进。

但是这部分的改进涉及的内容较多,不一定能在 .NET 7(C# 11)做完,因此部分内容推迟到 C# 12 也是有可能的。当然,还是很有希望能在 C# 11 的时间点就看到完全体的。

本文仅仅就这一个特性进行介绍,因为 C# 11 除了本特性之外,还有很多其他的改进,一篇文章根本说不完,其他那些我们就等到 .NET 7 快正式发布的时候再说吧。

背景

C# 自 7.0 版本引入了新的 ref struct 用来表示不可被装箱的栈上对象,但是当时局限性很大,甚至无法被用于泛型约束,也无法作为 struct 的字段。在 C# 11 中,由于特性 ref 字段的推动,需要允许类型持有其它值类型的引用,这方面的东西终于有了大幅度进展。

这些设施旨在允许开发者使用安全的代码编写高性能代码,而无需面对不安全的指针。接下来我就来对 C# 11 甚至 12 在此方面的即将到来的改进进行介绍。

ref 字段

C# 以前是不能在类型中持有对其它值类型的引用的,但是在 C# 11 中,这将变得可能。从 C# 11 开始,将允许 ref struct 定义 ref 字段。

readonly ref struct Span<T>
{private readonly ref T _field;private readonly int _length;public Span(ref T value){_field = ref value;_length = 1;}
}

直观来看,这样的特性将允许我们写出上面的代码,这段代码中构造了一个 Span<T>,它持有了对其他 T 对象的引用。

当然,ref struct 也是可以被 default 来初始化的:

Span<int> span = default;

但这样 _field 就会是个空引用,不过我们可以通过 Unsafe.IsNullRef 方法来进行检查:

if (Unsafe.IsNullRef(ref _field))
{throw new NullReferenceException(...);
}

另外,ref字段的可修改性也是一个非常重要的事情,因此引入了:

  • readonly ref:一个对对象的只读引用,这个引用本身不能在构造方法或 init 方法之外被修改

  • ref readonly:一个对只读对象的引用,这个引用指向的对象不能在构造方法或 init 方法之外被修改

  • readonly ref readonly:一个对只读对象的只读引用,是上述两种的组合

例如:

ref struct Foo
{ref readonly int f1;readonly ref int f2;readonly ref readonly int f3;void Bar(int[] array){f1 = ref array[0];  // 没问题f1 = array[0];      // 错误,因为 f1 引用的值不能被修改f2 = ref array[0];  // 错误,因为 f2 本身不能被修改f2 = array[0];      // 没问题f3 = ref array[0];  // 错误:因为 f3 本身不能被修改f3 = array[0];      // 错误:因为 f3 引用的值不能被修改}
}

生命周期

这一切看上去都很美好,但是真的没有任何问题吗?

假设我们有下面的代码来使用上面的东西:

Span<int> Foo()
{int v = 42;return new Span<int>(ref v);
}

v 是一个局部变量,在函数返回之后其生命周期就会结束,那么上面这段代码就会导致 Span<int> 持有的 v 的引用变成无效的。顺带一提,上面这段代码是完全合法的,因为 C# 之前不支持 ref 字段,因此上面的代码是不可能出现逃逸问题的。但是 C# 11 加入了 ref 字段,栈上的对象就有可能通过 ref 字段而发生引用逃逸,于是代码变得不安全。

如果我们有一个 CreateSpan 方法用来创建一个引用的 Span :

Span<int> CreateSpan(ref int v)
{// ...
}

这就衍生出了一系列在以前的 C# 中没问题(因为 ref 的生命周期为当前方法),但是在 C# 11 中由于可能存在 ref 字段而导致用安全的方式写出的非安全代码:

Span<int> Foo(int v)
{// 1return CreateSpan(ref v);// 2int local = 42;return CreateSpan(ref local);// 3Span<int> span = stackalloc int[42];return CreateSpan(ref span[0]);
}

因此,在 C# 11 中则不得不引入破坏性更改,不允许上述代码通过编译。但这并没有完全解决问题。

为了解决逃逸问题, C# 11 制定了引用逃逸安全规则。对于一个在 e 中的字段 f

  • 如果 f 是个 ref 字段,并且 ethis,则 f 在它被包围的方法中是引用逃逸安全的

  • 否则如果 f 是个 ref 字段,则 f 的引用逃逸安全范围和 e 的逃逸安全范围相同

  • 否则如果 e 是一个引用类型,则 f 的引用逃逸安全范围是调用它的方法

  • 否则 f 的引用逃逸安全范围和 e 相同

由于 C# 中的方法是可以返回引用的,因此根据上面的规则,一个 ref struct 中的方法将不能返回一个对非 ref 字段的引用:

ref struct Foo
{private ref int _f1;private int f2;public ref int P1 => ref _f1; // 没问题public ref int P2 => ref _f2; // 错误,因为违反了第四条规则
}

除了引用逃逸安全规则之外,同样还有对 ref 赋值的规则:

  • 对于 x.e1 = ref e2, 其中 x 是在调用方法中逃逸安全的,那么 e2 必须在调用方法中是引用逃逸安全的

  • 对于 e1 = ref e2,其中 e1 是个局部变量,那么 e2 的引用逃逸安全范围必须至少和 e1 的引用逃逸安全范围一样大

于是, 根据上述规则,下面的代码是没问题的:

readonly ref struct Span<T>
{readonly ref T _field;readonly int _length;public Span(ref T value){// 没问题,因为 x 是 this,this 的逃逸安全范围和 value 的引用逃逸安全范围都是调用方法,满足规则 1_field = ref value;_length = 1;}
}

于是很自然的,就需要在字段和参数上对生命周期进行标注,帮助编译器确定对象的逃逸范围。

而我们在写代码的时候,并不需要记住以上这么多的规则,因为有了生命周期标注之后一切都变得显式和直观了。

scoped

在 C# 11 中,引入了 scoped 关键字用来限制逃逸安全范围:

局部变量 s引用逃逸安全范围逃逸安全范围
Span<int> s当前方法调用方法
scoped Span<int> s当前方法当前方法
ref Span<int> s调用方法调用方法
scoped ref Span<int> s当前方法调用方法
ref scoped Span<int> s当前方法当前方法
scoped ref scoped Span<int> s当前方法当前方法

其中,scoped ref scoped 是多余的,因为它可以被 ref scoped 隐含。而我们只需要知道 scoped 是用来把逃逸范围限制到当前方法的即可,是不是非常简单?

如此一来,我们就可以对参数进行逃逸范围(生命周期)的标注:

Span<int> CreateSpan(scoped ref int v)
{// ...
}

然后,之前的代码就变得没问题了,因为都是 scoped ref

Span<int> Foo(int v)
{// 1return CreateSpan(ref v);// 2int local = 42;return CreateSpan(ref local);// 3Span<int> span = stackalloc int[42];return CreateSpan(ref span[0]);
}

scoped 同样可以被用在局部变量上:

Span<int> Foo()
{// 错误,因为 span 不能逃逸当前方法scoped Span<int> span1 = default;return span1;// 没问题,因为初始化器的逃逸安全范围是调用方法,因为 span2 可以逃逸到调用方法Span<int> span2 = default;return span2;// span3 和 span4 是一样的,因为初始化器的逃逸安全范围是当前方法,加不加 scoped 都没区别Span<int> span3 = stackalloc int[42];scoped Span<int> span4 = stackalloc int[42];
}

另外,struct 的 this 也加上了 scoped ref 的逃逸范围,即引用逃逸安全范围为当前方法,而逃逸安全范围为调用方法。

剩下的就是和 outin 参数的配合,在 C# 11 中,out 参数将会默认为 scoped ref,而 in 参数仍然保持默认为 ref

ref int Foo(out int r)
{r = 42;return ref r; // 错误,因为 r 的引用逃逸安全范围是当前方法
}

这非常有用,例如比如下面这个常见的情况:

Span<byte> Read(Span<byte> buffer, out int read)
{// .. 
}Span<int> Use()
{var buffer = new byte[256];// 如果不修改 out 的引用逃逸安全范围,则这会报错,因为编译器需要考虑 read 是可以被作为 ref 字段返回的情况// 如果修改 out 的引用逃逸安全范围,则就没有问题了,因为编译器不需要考虑 read 是可以被作为 ref 字段返回的情况int read;return Read(buffer, out read);
}

下面给出一些更多的例子:

Span<int> CreateWithoutCapture(scoped ref int value)
{// 错误,因为 value 的引用逃逸安全范围是当前方法return new Span<int>(ref value);
}Span<int> CreateAndCapture(ref int value)
{// 没问题,因为 value 的逃逸安全范围被限制为 value 的引用逃逸安全范围,这个范围是调用方法return new Span<int>(ref value)
}Span<int> ComplexScopedRefExample(scoped ref Span<int> span)
{// 没问题,因为 span 的逃逸安全范围是调用方法return span;// 没问题,因为 refLocal 的引用逃逸安全范围是当前方法、逃逸安全范围是调用方法// 在 ComplexScopedRefExample 的调用中它被传递给了一个 scoped ref 参数,// 意味着编译器在计算生命周期时不需要考虑引用逃逸安全范围,只需要考虑逃逸安全范围// 因此它返回的值的安全逃逸范围为调用方法Span<int> local = default;ref Span<int> refLocal = ref local;return ComplexScopedRefExample(ref refLocal);// 错误,因为 stackLocal 的引用逃逸安全范围、逃逸安全范围都是当前方法// 在 ComplexScopedRefExample 的调用中它被传递给了一个 scoped ref 参数,// 意味着编译器在计算生命周期时不需要考虑引用逃逸安全范围,只需要考虑逃逸安全范围// 因此它返回的值的安全逃逸范围为当前方法Span<int> stackLocal = stackalloc int[42];return ComplexScopedRefExample(ref stackLocal);
}

unscoped

上述的设计中,仍然有个问题没有被解决:

struct S
{int _field;// 错误,因为 this 的引用逃逸安全范围是当前方法public ref int Prop => ref _field;
}

因此引入一个 unscoped,允许扩展逃逸范围到调用方法上,于是,上面的方法可以改写为:

struct S
{private int _field;// 没问题,引用逃逸安全范围被扩展到了调用方法public unscoped ref int Prop => ref _field;
}

这个 unscoped 也可以直接放到 struct 上:

unscoped struct S
{private int _field;public unscoped ref int Prop => ref _field;
}

同理,嵌套的 struct 也没有问题:

unscoped struct Child
{int _value;public ref int Value => ref _value;
}unscoped struct Container
{Child _child;public ref int Value => ref _child.Value;
}

此外,如果需要恢复以前的 out 逃逸范围的话,也可以在 out 参数上指定 unscoped

ref int Foo(unscoped out int r)
{r = 42;return ref r;
}

不过有关 unscoped 的设计还属于初步阶段,不会在 C# 11 中就提供。

ref struct 约束

从 C# 11 开始,ref struct 可以作为泛型约束了,因此可以编写如下方法了:

void Foo<T>(T v) where T : ref struct
{// ...
}

因此,Span<T> 的功能也被扩展,可以声明 Span<Span<T>> 了,比如用在 byte 或者 char 上,就可以用来做高性能的字符串处理了。

反射

有了上面那么多东西,反射自然也是要支持的。因此,反射 API 也加入了 ref struct 相关的支持。

实际用例

有了以上基础设施之后,我们就可以使用安全代码来造一些高性能轮子了。

栈上定长列表

struct FrugalList<T>
{private T _item0;private T _item1;private T _item2;public readonly int Count = 3;public unscoped ref T this[int index] => index switch{0 => ref _item1,1 => ref _item2,2 => ref _item3,_ => throw new OutOfRangeException("Out of range.")};
}

栈上链表

ref struct StackLinkedListNode<T>
{private T _value;private ref StackLinkedListNode<T> _next;public T Value => _value;public bool HasNext => !Unsafe.IsNullRef(ref _next);public ref StackLinkedListNode<T> Next => HasNext ? ref _next : throw new InvalidOperationException("No next node.");public StackLinkedListNode(T value){this = default;_value = value;}public StackLinkedListNode(T value, ref StackLinkedListNode<T> next){_value = value;_next = ref next;}
}

除了这两个例子之外,其他的比如解析器和序列化器等等,例如 Utf8JsonReaderUtf8JsonWriter 都可以用到这些东西。

未来计划

高级生命周期

上面的生命周期设计虽然能满足绝大多数使用,但是还是不够灵活,因此未来有可能在此基础上扩展,引入高级生命周期标注。例如:

void M(scoped<'a> ref MyStruct s, scoped<'b> Span<int> span) where 'b >= 'a
{s.Span = span;
}

上面的方法给参数 s 和 span 分别声明了两个生命周期 'a 和 'b,并约束 'b 的生命周期不小于 'a,因此在这个方法里,span 可以安全地被赋值给 s.Span

这个虽然不会被包含在 C# 11 中,但是如果以后开发者对相关的需求增长,是有可能被后续加入到 C# 中的。

总结

以上就是 C# 11(或之后)对 ref 和 struct 的改进了。有了这些基础设施,开发者们将能轻松使用安全的方式来编写没有任何堆内存开销的高性能代码。尽管这些改进只能直接让小部分非常关注性能的开发者收益,但是这些改进带来的将是后续基础库代码质量和性能的整体提升。

如果你担心这会让语言的复杂度上升,那也大可不必,因为这些东西大多数人并不会用到,只会影响到小部分的开发者。因此对于大多数人而言,只需要写着原样的代码,享受其他基础库作者利用上述设施编写好的东西即可。

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

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

相关文章

lia人是什么意思_狗狗喜欢舔人到底什么意思?毛孩的心思主人你要懂

很多人都喜欢养狗&#xff0c;因为它们忠诚、淘气、可爱。同时&#xff0c;狗狗也有很多奇怪的习惯&#xff0c;例如&#xff1a;喜欢舔人&#xff0c;喜欢追逐活动的东西等等。不过大多数狗主人通常都会有一个最想知道的问题&#xff1a;为什么狗狗总喜欢舔人&#xff0c;它们…

UINavigationController

-(void)resetTabbarController:(UITabBarController *)controller { NSArray *arr controller.tabBar.items; UITabBarItem *item0 [arr objectAtIndex:0]; //使用指定图片 [item0 setSelectedImage:[[UIImage imageNamed:"icon_everyday_press"] imageWithRenderi…

“爱思助手”曝为iOS木马:可绕过苹果DRM机制

一款新的iOS木马已在国内曝光&#xff0c;它可以通过PC感染未越狱的iOS设备&#xff0c;而无需利用企业证书。Palo Alto Networks指出&#xff0c;其名叫“爱思助手”(AceDeceiver)&#xff0c;目前正在影响我国的iOS用户。“爱思助手”利用了苹果数字版权管理(DRM)上的FairPla…

php自动生成mysql的触发代码。

php自动生成mysql的触发代码。 如果公司里有上百个表要做触发器&#xff0c;如果手动写代码的话。很累&#xff0c;所以今天写了一个小程序&#xff0c; <?php $dbname test;//数据库 $tab1 user; //执行的表 $tab2 user_bak; //被触发的表 $conn mysql_connect("…

C#趣味程序---求两个数的最大公约数和最小公倍数

using System;namespace ConsoleApplication1 {class Program{static void Main(string[] args){Console.WriteLine("请输入一个数&#xff1a;");int num1 int.Parse(Console.ReadLine());Console.WriteLine("请输入另一个数&#xff1a;");int num2 in…

php运行条件,PHP配置环境要求 php运行的先决条件

类型&#xff1a;编程相关大小&#xff1a;320KB语言&#xff1a;中文 评分&#xff1a;6.6标签&#xff1a;立即下载在本教程中&#xff0c;假设用户的服务器已经安装并运行了 PHP&#xff0c;所有以 .php 结尾的文件都将由 PHP 来处理。在大部分的服务器上&#xff0c;这是 P…

剑指offer之二维数组中查找

1 问题 二维数组中查找&#xff1a; 在一个二维数组钟&#xff0c;每一行都按照从左到右递增得顺序排列&#xff0c;每一列 都按照从上往下得递增排列&#xff0c;请完成一个函数&#xff0c;输入这样得一个二维数组和一个 整数&#xff0c;判断数组是否含有该整数 列如&…

【C语言简单说】二十:指针基础

。。据说指针很难 其实稍微理解概念不难。 先看百科的定义&#xff1a;在计算机科学中&#xff0c;指针&#xff08;Pointer&#xff09;是编程语言中的一个对象&#xff0c;利用地址&#xff0c;它的值直接指向&#xff08;points to&#xff09;存在电脑存储器中另一个地方的…

移动web开发(三)——字体使用

参考&#xff1a; 移动web页面使用字体的思考.http://www.cnblogs.com/PeunZhang/p/3592096.html

c#可变参数params的介绍

c#可变参数params的介绍作为一个netUp主&#xff0c;今天在b站刷到了java的一个视频&#xff0c;可变参数的介绍&#xff0c;所以今天给大家介绍一下c#中可变参数params的使用介绍&#xff0c;我们首先看一下官方解释: 使用 params 关键字可以指定采用数目可变的参数的params。…

Javascript中的循环变量声明,到底应该放在哪儿?

不放走任何一个细节。相信很多Javascript开发者都在声明循环变量时犹 豫过var i到底应该放在哪里&#xff1a;放在不同的位置会对程序的运行产生怎样的影响&#xff1f;哪一种方式符合Javascript的语言规范&#xff1f;哪一种方式和ecma标准未来的发展 方向匹配&#xff1f;本文…

Delphi全局热键的注册

1.在窗启动时创建ATOM;(aatom:ATOM;定义在private中&#xff09; 1 if FindAtom(ZWXhotKey)0 then 2 begin 3 aatom:GlobalAddAtom(ZWXhotKey); 4 end; 5 if RegisterHotKey(Handle,aatom,MOD_ALT,$41) then 6 begin 7 MessageBox(Handle,按alta,提示,MB_OK); 8 end; 2.定义处…

python爬取网易云音乐问题陈述_python 网易云音乐 评论爬取问题

除了使用phantomjs,selenium之外&#xff0c;怎么爬取多页评论&#xff0c;这两个都太慢了。例如http://music.163.com/#/song?i... 的 评论。webapi都是http://music.163.com/weapi/v1...&#xff0c;每页20个评论&#xff0c;怎么获取下一页的评论&#xff0c;param是加密的…

C#趣味程序----分数之和

问题:求这样的四个自然数p,q,r,s(p<=q<=r<=s),使得等式1/p + 1/q +1/r +1/s=1成立。 分析:将原式同分,化简整理后得到:2<=p<5,p<=q<7,q<r<13。 using System;namespace ConsoleApplication1 {class Program{static void Main(string[] ar…

php gearmanclient addoptions,gearman PHP7扩展安装

注&#xff1a;官方提供的只支持PHP 6&#xff0c;需要第3方支持&#xff0c;https://github.com/wcgallego/pecl-gearmanyum install libgearman-devel -y 如没装&#xff0c;则config时报错 error: Please install libgearmanwget https://github.com/wcgallego/pecl-gearm…

剑指offer之把字符串里面空格替换成百分之20

1 问题 把字符串里面空格替换成百分之20 2 代码实现 第一种时间复杂的o(n * n)实现 #include <stdio.h> #include <stdlib.h>char* insert(char *a, int len, char *replace, int replaceLen) {//先得到多少个空格char *p a;int count 0;while (*p ! \0){if (…

【C语言简单说】二十一:双重指针基础 (完结)

其实后面这两节我是用我几年前写的教程复制过来的。。。 ’ – ’ ) ( &#xff13; )╱~~ 如有错误&#xff0c;请留言提醒哈~~~尴尬的笑。 多重指针呢其实就是指向指针的指针。 首先&#xff0c;变量大家都知道是啥意思了吧&#xff1f;一个变量是有地址的。那么指针变量也是…

jquery设置滚动条距离页面顶部的高度

//获取滚动条到顶部的垂直高度 $(document).scrollTop(); //获取滚动条到左边的垂直宽度 $(document).scrollLeft(); function clickFn(){var topL0 $("#l0").offset().top;var topL1 $("#l1").offset().top;alert("l0距离顶部:"topL0)aler…

精彩回顾|「源」来如此 第六期 - 开源经济与产业投资

| 作者&#xff1a;活动组、袁睿斌| 编辑&#xff1a;金心悦| 设计&#xff1a;朱亿钦4月17日 14:00&#xff0c;由开源社联合云启资本、易观分析、云赛空间&#xff0c;以及微软 Reactor 共同主办&#xff0c;由示说网提供转播支持的「源」来如此 第六期直播活动如约而至。在本…

python中@staticmethod_Python中的@staticmethod和@classmethod的区别

一直搞不明白&#xff0c;类方法和静态方法的区别&#xff0c;特意研究了一下&#xff0c;跟大家分享一下。为了方便大家了解两者的差别&#xff0c;以下的示例代码将有助于发现其中的差别&#xff1a;class A(object):def foo(self, x):print "executing foo(%s, %s)&quo…