C#中的ref struct

一、ref struct 初相识

在 C# 的编程世界里,ref struct作为一种特殊的结构体类型,自 C# 7.2 版本引入后,为开发者们带来了全新的编程体验。它与普通的struct有着本质区别,普通struct虽属于值类型,但在某些场景下,其值传递的特性可能导致性能损耗。而ref struct只能在栈上分配内存,这一特性使其在传递时并非像普通struct那样传递值的副本,而是传递引用,极大地提升了性能。例如,在处理大量数据的结构体时,普通struct每次传递都要复制整个对象,而ref struct则避免了这种开销。这就好比在快递包裹时,普通struct是把包裹里的东西全部复制一份再寄出,而ref struct则是直接给收件人一个包裹的地址,让他们直接去取,大大提高了效率。

二、ref struct 的特性剖析

2.1 栈上分配的优势

ref struct最大的特性之一就是其内存分配在栈上。栈的分配和释放速度极快,相比堆内存分配,它无需经过复杂的垃圾回收机制。这使得ref struct在性能关键型代码中表现卓越。例如,在处理高频次的数值计算场景时,使用ref struct来存储中间计算结果,能极大地减少垃圾回收带来的开销,提升程序的执行效率。

public ref struct Point
{public int X;public int Y;
}public void Calculate()
{Point p = new Point { X = 10, Y = 20 };// 这里对p进行大量计算操作,由于p在栈上分配,性能高效
}

在上述代码中,Point结构体作为ref struct在栈上分配内存,在Calculate方法中进行大量计算操作时,无需担心垃圾回收的影响,从而保证了计算的高效性。

2.2 不可装箱的限制

ref struct不能被装箱转换为object类型,这是其与普通struct的重要区别。装箱操作会将值类型转换为引用类型并在堆上分配内存,这与ref struct栈上分配的特性相悖。例如,当尝试将ref struct实例传递给需要object类型参数的方法时,会导致编译错误。

public ref struct DataInfo
{public int Value;
}public void ProcessObject(object obj)
{// 处理object对象
}public void Test()
{DataInfo info = new DataInfo { Value = 10 };// 以下代码会导致编译错误,因为ref struct不能装箱为object// ProcessObject(info);
}

在这个例子中,试图将DataInfo类型的info变量传递给ProcessObject方法,由于ref struct不可装箱,会引发编译错误,这也体现了ref struct在类型转换上的严格限制。

2.3 类型使用限制

ref struct在使用场景上存在诸多限制。在异步方法中,ref struct不能作为局部变量在await表达式所在的代码块中使用,因为异步操作可能导致栈帧的切换,而ref struct的生命周期依赖于栈,这可能引发内存安全问题 。在迭代器中,ref struct不能在包含yield return的代码段中使用,因为yield return会使方法的执行暂停并在后续恢复,这期间ref struct的生命周期管理会变得复杂。同样,在 Lambda 表达式中,ref struct也不能被捕获,因为 Lambda 表达式可能会创建闭包,而闭包的实现机制与ref struct的栈分配特性不兼容。

// 异步方法中使用ref struct的错误示例
public async Task AsyncMethod()
{ref struct TempStruct{public int Data;}TempStruct temp = new TempStruct { Data = 10 };// 以下代码会报错,因为在await表达式所在代码块中使用了ref struct// await Task.Delay(1000);// Console.WriteLine(temp.Data);
}// 迭代器中使用ref struct的错误示例
public IEnumerable<int> IteratorMethod()
{ref struct Item{public int Value;}Item item = new Item { Value = 5 };// 以下代码会报错,因为在yield return所在代码段中使用了ref struct// yield return item.Value;
}// Lambda表达式中使用ref struct的错误示例
public void LambdaTest()
{ref struct InnerStruct{public int Num;}InnerStruct inner = new InnerStruct { Num = 20 };// 以下代码会报错,因为Lambda表达式捕获了ref struct// Action action = () => Console.WriteLine(inner.Num);
}

以上代码分别展示了ref struct在异步方法、迭代器和 Lambda 表达式中使用时会出现的错误情况,这清晰地表明了ref struct在这些场景下的使用限制。

三、与普通 struct 的差异对比

3.1 内存分配对比

普通struct在内存分配上较为灵活,既可以在栈上分配,也可能在堆上分配。当struct作为局部变量时,通常在栈上分配内存,例如在方法内部定义的struct变量 。而当struct作为类的成员变量时,会随着类的实例在堆上分配内存。

class Container
{public struct InnerStruct{public int Value;}public InnerStruct inner;
}public void Test()
{Container container = new Container();// container实例在堆上分配,其成员变量inner也在堆上Container.InnerStruct localStruct = new Container.InnerStruct();// localStruct作为局部变量在栈上分配
}

在上述代码中,Container类的成员变量inner随着container实例在堆上分配内存,而localStruct作为局部变量在栈上分配。

ref struct则只能在栈上分配内存,这是其显著特性。栈上分配使得ref struct的生命周期与栈帧紧密相关,一旦栈帧出栈,ref struct的实例也会随之销毁。这一特性决定了ref struct在性能关键型场景中的优势,避免了堆内存分配带来的开销和垃圾回收的影响。

从内存布局图来看,普通struct在堆上分配时,其内存布局包含对象头信息以及结构体成员数据,对象头用于存储一些与对象相关的元数据,如对象的哈希码、对象的分代年龄等。而ref struct在栈上分配时,其内存布局更为紧凑,直接存储结构体成员数据,没有额外的对象头信息,这使得ref struct在内存使用上更加高效。

3.2 功能特性对比

功能特性普通 structref struct
能否实现接口可以实现接口,通过实现接口来定义行为契约,增强代码的可扩展性和可维护性在 C# 13 之前不能实现接口,因为实现接口可能涉及装箱操作,与ref struct不能装箱的特性冲突。从 C# 13 开始可以实现接口,但需遵循ref安全性规则
是否可装箱可以装箱,即值类型转换为引用类型,在需要将struct作为object类型传递或存储时,会发生装箱操作不能被装箱,因为装箱会将值类型转换为引用类型并在堆上分配内存,这与ref struct只能在栈上分配的特性相悖
能否用于泛型约束可以作为泛型约束,用于限制泛型类型参数的类型,确保传入的类型符合特定的结构要求在 C# 13 之前不能用于泛型约束,从 C# 13 开始引入了allows ref struct新泛型约束功能,允许对泛型类型参数应用ref struct约束
能否作为类或数组的成员可以作为类的成员变量,也可以作为数组的元素类型不能作为类、普通结构或数组的成员,也不能作为其他ref struct的字段,除非该字段被标记为ref
能否在异步方法、迭代器、Lambda 表达式中使用在异步方法、迭代器、Lambda 表达式中可正常使用在异步方法中,从 C# 13 开始可以使用,但不能在与await表达式同一个代码块中交互;在迭代器中,从 C# 13 开始允许使用,但不能在包含yield return的代码段中使用;在 Lambda 表达式中不能被捕获

通过上述对比,可以清晰地看到ref struct与普通struct在功能特性上的显著差异,这些差异决定了它们在不同编程场景中的适用性。

四、使用场景与示例

4.1 性能关键型代码

在处理大规模数据计算的场景中,ref struct的优势尤为明显。以排序算法为例,假设我们有一个包含大量整数的数组需要进行排序。传统方式下,使用普通struct存储中间数据可能会因为频繁的内存复制而导致性能瓶颈。而引入ref struct,则可以有效减少这种内存开销。

public ref struct SortingData
{public int[] Data { get; set; }public int Length { get; set; }public void QuickSort(int left, int right){if (left < right){int pivotIndex = Partition(left, right);QuickSort(left, pivotIndex - 1);QuickSort(pivotIndex + 1, right);}}private int Partition(int left, int right){int pivot = Data[right];int i = left - 1;for (int j = left; j < right; j++){if (Data[j] < pivot){i++;Swap(i, j);}}Swap(i + 1, right);return i + 1;}private void Swap(int i, int j){int temp = Data[i];Data[i] = Data[j];Data[j] = temp;}
}public class SortingExample
{public static void Main(){int[] largeArray = new int[1000000];for (int i = 0; i < largeArray.Length; i++){largeArray[i] = new Random().Next(1, 1000000);}SortingData sortingData = new SortingData { Data = largeArray, Length = largeArray.Length };sortingData.QuickSort(0, sortingData.Length - 1);// 验证排序结果for (int i = 1; i < sortingData.Length; i++){if (sortingData.Data[i - 1] > sortingData.Data[i]){Console.WriteLine("排序失败");return;}}Console.WriteLine("排序成功");}
}

在上述代码中,SortingData作为ref struct用于存储待排序的数组及相关信息。在QuickSort方法中,通过传递ref struct的引用,避免了每次递归调用时对整个数据结构的复制,从而显著提升了排序的性能。特别是在处理像largeArray这样的大规模数据时,这种优势更加突出。

4.2 内存管理场景

在直接操作非托管内存的场景中,ref struct可以很好地封装这些操作,确保内存的安全访问和管理。例如,在编写与硬件设备驱动交互的代码时,可能需要直接读写特定的内存地址。

using System;
using System.Runtime.InteropServices;public unsafe ref struct MemoryAccessor
{private void* _pointer;private int _size;public MemoryAccessor(void* pointer, int size){_pointer = pointer;_size = size;}public ref byte this[int index]{get{if (index >= _size || index < 0){throw new IndexOutOfRangeException();}return ref ((byte*)_pointer)[index];}}public void WriteInt(int offset, int value){*(int*)((byte*)_pointer + offset) = value;}public int ReadInt(int offset){return *(int*)((byte*)_pointer + offset);}
}public class MemoryManagementExample
{public static void Main(){int size = 1024;byte* buffer = stackalloc byte[size];MemoryAccessor accessor = new MemoryAccessor(buffer, size);accessor.WriteInt(0, 12345);int value = accessor.ReadInt(0);Console.WriteLine($"读取到的值: {value}");// 释放非托管内存(这里stackalloc分配的内存会在栈帧结束时自动释放)}
}

在这个示例中,MemoryAccessor作为ref struct封装了对非托管内存块的读写操作。通过ref byte索引器和WriteInt、ReadInt方法,安全地实现了对非托管内存的访问,避免了潜在的内存越界等错误。同时,由于ref struct在栈上分配,其生命周期与栈帧紧密相关,在一定程度上简化了内存管理。

4.3 结合 Span 和 Memory

在处理内存切片和缓冲区时,ref struct与Span和Memory的结合使用可以提供高效且安全的内存操作方式。例如,在网络数据传输中,需要对接收的字节缓冲区进行处理。

using System;
using System.Buffers;
using System.Text;public ref struct NetworkDataProcessor
{public Memory<byte> Buffer { get; set; }public string ReadString(int startIndex, int length){Span<byte> span = Buffer.Span.Slice(startIndex, length);return Encoding.UTF8.GetString(span);}public void WriteString(int startIndex, string value){Span<byte> span = Buffer.Span.Slice(startIndex, value.Length);Encoding.UTF8.GetBytes(value, span);}
}public class NetworkExample
{public static void Main(){byte[] bufferArray = new byte[1024];Memory<byte> bufferMemory = new Memory<byte>(bufferArray);NetworkDataProcessor processor = new NetworkDataProcessor { Buffer = bufferMemory };string message = "Hello, World!";processor.WriteString(0, message);string readMessage = processor.ReadString(0, message.Length);Console.WriteLine($"读取到的消息: {readMessage}");}
}

在上述代码中,NetworkDataProcessor作为ref struct持有一个Memory类型的缓冲区。通过Span对缓冲区进行切片操作,实现了高效的字符串读写。Span的特性使得它可以在不进行内存复制的情况下对缓冲区进行操作,结合ref struct在栈上分配的优势,进一步提升了性能。这种组合在处理网络数据、文件 I/O 等需要频繁操作内存缓冲区的场景中非常实用。

五、正确使用 ref struct 的方法与注意事项

5.1 定义与声明

定义ref struct时,需在struct关键字前添加ref。它的字段必须为值类型,且不能有实例构造函数(静态构造函数除外),因为ref struct的实例在栈上分配,其初始化是自动进行的。例如:

public ref struct DataHolder
{public int Value;public double AnotherValue;// 错误示例:不能有实例构造函数// public DataHolder(int value, double anotherValue)// {//     Value = value;//     AnotherValue = anotherValue;// }// 正确示例:可以有静态构造函数static DataHolder(){// 初始化静态数据}
}

在上述代码中,DataHolder是一个ref struct,它包含两个值类型字段Value和AnotherValue。尝试定义实例构造函数会导致编译错误,而静态构造函数则是允许的。

5.2 方法传参与返回

在方法传参中使用ref struct时,通过传递引用而非值副本,这大大提高了效率,尤其适用于大型结构体。在方法定义和调用时,都需要使用ref关键字。例如:

public ref struct LargeData
{public int[] DataArray { get; set; }
}public void ProcessLargeData(ref LargeData data)
{// 对data进行处理for (int i = 0; i < data.DataArray.Length; i++){data.DataArray[i] *= 2;}
}public void Main()
{LargeData largeData = new LargeData { DataArray = new int[10000] };// 初始化数组数据for (int i = 0; i < largeData.DataArray.Length; i++){largeData.DataArray[i] = i;}ProcessLargeData(ref largeData);// 验证处理结果for (int i = 0; i < largeData.DataArray.Length; i++){Console.WriteLine(largeData.DataArray[i]);}
}

在这个例子中,ProcessLargeData方法接收一个ref LargeData类型的参数,在方法内部对DataArray进行操作。由于传递的是引用,所以在方法中对data的修改会直接反映在调用者的largeData实例上。

在方法返回值中使用ref struct时,同样需要在返回类型前加上ref关键字。这允许返回一个对ref struct实例的引用,而不是创建一个新的副本。例如:

public ref struct ResultData
{public int ResultValue { get; set; }
}public ref ResultData CalculateResult()
{ResultData result = new ResultData { ResultValue = 42 };return ref result;
}

在上述代码中,CalculateResult方法返回一个ref ResultData类型的引用,调用者可以直接操作返回的实例,而无需进行值的复制。

5.3 注意生命周期与逃逸问题

ref struct的生命周期与它所在的栈帧紧密相关。当包含ref struct的方法返回时,该ref struct的实例将被销毁。因此,要避免将ref struct的引用存储在可能超出其生命周期的地方,即所谓的 “逃逸问题”。例如,下面的代码是错误的:

public ref struct TempData
{public int Info { get; set; }
}public TempData* IncorrectStorage()
{TempData temp = new TempData { Info = 10 };// 错误:返回局部ref struct的指针,会导致变量逃逸return &temp;
}

在这个例子中,IncorrectStorage方法返回了一个指向局部ref struct变量temp的指针,这会导致temp的引用逃逸出方法,在方法返回后,temp所在的栈帧被销毁,该指针将指向无效内存,从而引发未定义行为。

为了避免逃逸问题,可以使用scoped关键字来限制ref变量的生命周期。例如:

public ref struct SafeData
{public int Value { get; set; }
}public void SafeOperation()
{scoped ref SafeData safeData = ref GetSafeData();// 在这里使用safeData,其生命周期被限制在当前方法内Console.WriteLine(safeData.Value);
}public ref SafeData GetSafeData()
{SafeData data = new SafeData { Value = 20 };return ref data;
}

在上述代码中,SafeOperation方法中使用scoped ref来声明safeData,确保它的生命周期与当前方法一致,避免了逃逸问题。GetSafeData方法返回的ref SafeData引用在SafeOperation方法内被安全使用,当SafeOperation方法结束时,safeData的生命周期也随之结束,不会出现引用指向无效内存的情况。

六、总结与展望

ref struct作为 C# 语言中具有独特内存管理特性的类型,为开发者提供了在性能关键型场景下优化代码的有力工具。它通过栈上分配内存、避免装箱操作等特性,有效提升了程序的执行效率,减少了内存开销。在性能关键型代码、内存管理场景以及与Span和Memory的结合使用中,ref struct展现出了明显的优势。

展望未来,随着 C# 语言的不断发展,ref struct有望在更多方面得到优化和拓展。或许在未来的版本中,其使用限制会进一步放宽,在异步编程、泛型约束等场景下的支持会更加完善,从而使其能够更广泛地应用于各种复杂的编程场景中,为开发者带来更加高效、便捷的编程体验。

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

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

相关文章

AI智能体实战|使用扣子Coze搭建AI智能体,看这一篇就够了(新手必读)

有朋友看到我使用Coze搭建的AI智能体蛮实用的&#xff0c;也想自己尝试一下。那今天我就分享一下如何使用Coze&#xff08;扣子&#xff09;搭建AI智能体&#xff0c;手把手教学&#xff0c;流程超级详细&#xff0c;学会了的话&#xff0c;欢迎分享转发&#xff01; 一、搭建A…

.NET8.0多线程编码结合异步编码示例

1、创建一个.NET8.0控制台项目来演示多线程的应用 2、快速创建一个线程 3、多次运行程序&#xff0c;可以得到输出结果 这就是多线程的特点 - 当多个线程并行执行时&#xff0c;它们的具体执行顺序是不确定的&#xff0c;除非我们使用同步机制&#xff08;如 lock、信号量等&am…

nginx 实现 正向代理、反向代理 、SSL(证书配置)、负载均衡 、虚拟域名 ,使用其他中间件监控

我们可以详细地配置 Nginx 来实现正向代理、反向代理、SSL、负载均衡和虚拟域名。同时&#xff0c;我会介绍如何使用一些中间件来监控 Nginx 的状态和性能。 1. 安装 Nginx 如果你还没有安装 Nginx&#xff0c;可以通过以下命令进行安装&#xff08;以 Ubuntu 为例&#xff0…

《数据思维》之数据可视化_读书笔记

文章目录 系列文章目录前言一、pandas是什么&#xff1f;二、使用步骤 1.引入库2.读入数据总结 前言 数据之道&#xff0c;路漫漫其修远兮&#xff0c;吾将上下而求索。 一、数据可视化 最基础的数据可视化方法就是统计图。一个好的统计图应该满足四个标准&#xff1a;准确、有…

Linux之进程

Linux之进程 一.进程进程之形ps命令进程状态特殊进程孤儿进程守护进程 进程创建之创建子进程进程特性优先级进程切换&#xff08;分时操作系统&#xff09; 二.环境变量三.进程地址空间四.进程终止&进程等待五.进程替换六.自定义shell 本篇博客希望简略的介绍进程&#xff…

漫话架构师|什么是系统架构设计师(开篇)

~犬&#x1f4f0;余~ “我欲贱而贵&#xff0c;愚而智&#xff0c;贫而富&#xff0c;可乎&#xff1f; 曰&#xff1a;其唯学乎” 关注犬余&#xff0c;共同进步 技术从此不孤单

在AI智能中有几种重要的神经网络类型?6种重要的神经网络类型分享!

神经网络今天已经变得非常流行&#xff0c;但仍然缺乏对它们的了解。一方面&#xff0c;我们已经看到很多人无法识别各种类型的神经网络及其解决的问题&#xff0c;更不用说区分它们中的每一个了。其次&#xff0c;在某种程度上更糟糕的是&#xff0c;当人们在谈论任何神经网络…

业务幂等性技术架构体系之消息幂等深入剖析

在系统中当使用消息队列时&#xff0c;无论做哪种技术选型&#xff0c;有很多问题是无论如何也不能忽视的&#xff0c;如&#xff1a;消息必达、消息幂等等。本文以典型的RabbitMQ为例&#xff0c;讲解如何保证消息幂等的可实施解决方案&#xff0c;其他MQ选型均可参考。 一、…

【C语言】线程----同步、互斥、条件变量

目录 3. 同步 3.1 概念 3.2 同步机制 3.3 函数接口 1. 同步 1.1 概念 同步(synchronization)指的是多个任务(线程)按照约定的顺序相互配合完成一件事情 1.2 同步机制 通过信号量实现线程间的同步 信号量&#xff1a;通过信号量实现同步操作&#xff1b;由信号量来决定…

Linux内核的启动

一、需求 Linux系统中内核处于硬件和应用层之间。整个系统启动和初始化的过程&#xff0c;Linux内核是在主处理器启动之后才会执行。不同的处理器启动流程并不相同&#xff0c;这就要求内核能支持各种处理器的初始化操作。Liux内核各个模块&#xff0c;大部分设计时做到了体系…

[手机Linux] ubuntu 错误解决

Ubuntu: 1,ttyname failed: Inappropriate ioctl for device 将 /root/.profile 文件中的 mesg n || true 改为如下内容。 vim /root/.profile tty -s && mesg n || true 2,Errors were encountered while processing: XXX XXXX sudo apt-get --purge remove xxx…

Docker的入门

一、安装Docker 本教程参考官网文档&#xff0c;链接如下: CentOS | Docker Docs 这个教程是基于你的虚拟机已经弄好了&#xff08;虚拟机用的CentOS&#xff09;&#xff0c;并且有SecureCRT或者MobaXterm等等任意一个工具 1.1 卸载旧版 如果系统中存在旧版本的Docker&a…

Onedrive精神分裂怎么办(有变更却不同步)

Onedrive有时候会分裂&#xff0c;你在本地删除文件&#xff0c;并没有同步到云端&#xff0c;但是本地却显示同步成功。 比如删掉了一个目录&#xff0c;在本地看已经删掉&#xff0c;onedrive显示已同步&#xff0c;但是别的电脑并不会同步到这个删除操作&#xff0c;在网页版…

机器学习06-正则化

机器学习06-正则化 文章目录 机器学习06-正则化0-核心逻辑脉络1-参考网址3-大模型训练中的正则化1.正则化的定义与作用2.常见的正则化方法及其应用场景2.1 L1正则化&#xff08;Lasso&#xff09;2.2 L2正则化&#xff08;Ridge&#xff09;2.3 弹性网络正则化&#xff08;Elas…

windows 极速安装 Linux (Ubuntu)-- 无需虚拟机

1. 安装 WSL 和 Ubuntu 打开命令行&#xff0c;执行 WSL --install -d ubuntu若报错&#xff0c;则先执行 WSL --update2. 重启电脑 因安装了子系统&#xff0c;需重启电脑才生效 3. 配置 Ubuntu 的账号密码 打开 Ubuntu 的命令行 按提示&#xff0c;输入账号&#xff0c;密…

微信小程序实现个人中心页面

文章目录 1. 官方文档教程2. 编写静态页面3. 关于作者其它项目视频教程介绍 1. 官方文档教程 https://developers.weixin.qq.com/miniprogram/dev/framework/ 2. 编写静态页面 mine.wxml布局文件 <!--index.wxml--> <navigation-bar title"个人中心" ba…

Qt/C++进程间通信:QSharedMemory 使用详解(附演示Demo)

在开发跨进程应用程序时&#xff0c;进程间通信&#xff08;IPC&#xff09;是一个关键问题。Qt 框架提供了多种 IPC 技术&#xff0c;其中 QSharedMemory 是一种高效的共享内存方式&#xff0c;可以实现多个进程之间快速交换数据。本文将详细讲解 QSharedMemory 的概念、用法及…

[UE4图文系列] 5.字符串转中文乱码问题说明

原文连接&#xff1a;[UE4图文系列] 5.字符串转中文乱码问题说明 - 哔哩哔哩 本例以原生C和UE4 C字符串传输中出现的中文乱码问题进行说明 一.乱码示例: 1.直接用中文字符串初始化FString,在蓝图中进行打印 FString GetStrWithChinese() {FString fstr"这是一句中文"…

人工智能任务19-基于BERT、ELMO模型对诈骗信息文本进行识别与应用

大家好&#xff0c;我是微学AI&#xff0c;今天给大家介绍一下人工智能任务19-基于BERT、ELMO模型对诈骗信息文本进行识别与应用。近日&#xff0c;演员王星因接到一份看似来自知名公司的拍戏邀约&#xff0c;被骗至泰国并最终被带到缅甸。这一事件迅速引发了社会的广泛关注。该…

题解 CodeForces 430B Balls Game 栈 C/C++

题目传送门&#xff1a; Problem - B - Codeforceshttps://mirror.codeforces.com/contest/430/problem/B翻译&#xff1a; Iahub正在为国际信息学奥林匹克竞赛&#xff08;IOI&#xff09;做准备。有什么比玩一个类似祖玛的游戏更好的训练方法呢&#xff1f; 一排中有n个球…