当前位置: 首页 > news >正文

对比C++,Rust在内存安全上做的努力

简介

近年来,越来越多的组织表示,如果新项目在技术选型时需要使用系统级开发语言,那么不要选择使用C/C++这种内存不安全的系统语言,推荐使用内存安全的Rust作为替代。

谷歌也声称,Android 的安全漏洞,从 2019 年的 223 个降低到 2022 年的 85 个,经过分析,谷歌认为内存漏洞减少的情况,主要与 Rust 代码的比例增加有关。在 Android 13 中,就已经有约 21%的新原生代码以 Rust 开发

微软也宣布,Rust 将正式入驻 Windows 系统内核;AWS在其基础设施中越来越多地使用 Rust ;2022 年 12 月,Linux 内核 6.1 发布,包括最初的 Rust 支持. . .

作为后来者,Rust是怎么做到内存安全,且受到越来越多人的青睐呢?要知道,换做使用C/C++开发,可能只有高级C/C++开发人员写出的代码才能如此稳定,Rust是怎么保证任何一个使用它的人都能写出内存安全的代码的呢?

下面,针对在C/C++中几种常见的内存安全问题为例,简单分析下。


悬空指针

悬空指针主要是指,在C/C++中,某个对象已经被释放了,但是在某个角落还有一个指针指向这个对象,这个指针就是一个悬空指针。当代码运行到这个地方,解引用这个悬空指针时,就会出现未定义的行为

Rust解决这个问题的办法就是Rust的精髓所在——生命周期

int main()
{std::string *ptr = nullptr;{std::string = str;ptr = &str;}printf("%s", ptr->c_str());return 0;
}

上面是典型的C++中出现野指针的场景,这段代码编译器不会发出任何抱怨。

程序入口定义了一个std::string类型的指针ptr,并初始化为nullptr。进入代码块后,在代码块中创建一个局部变量str,并且让ptr指向这个局部变量。当执行流结束这个代码块后,栈上的变量str将会被释放,但是此时指针ptr 还是指向这个局部变量str,代码块后续任何解引用指针ptr的地方都将是一个不可预期的行为。

我们用Rust实现一下这段代码。

fn main()
{let str_ref;{let str_obj: String = String::new();str_ref = &str_obj;}println!("{str_ref }");
}

相同的逻辑,只是Rust中将指针改为了引用(引用就是一个指针)。当执行流结束代码块之后str_obj将会被释放,但是此时str_ref 还指向这个局部变量。尝试编译一下。

在这里插入图片描述

不出意外的,Rust的生命周期检查器发现了这个问题,报错信息是borrowed value does not live long enough,他说str_obj的生命周期不够长,引用str_ref在str_obj的生命结束后还在使用。

Rust在编译时会尝试为每个引用和被引用的对象分配一个生命周期,生命周期完全是Rust在编译期虚构的产物,在运行期,引用就是一个地址,所以生命周期不会有任何运行期开销。有了生命周期,在编译期,生命周期检查器就会对比被引用对象和引用之间的生命周期关系,如下:

在这里插入图片描述

黄色框表示str_obj的生命周期;引用str_ref 的生命周期是,str_ref 从初始化开始到str_ref 最后被使用的地方之间的代码块就是str_ref 的生命周期,所以这里白色方块表示引用str_ref 的生命周期。生命周期检查器准则之一是,引用的最大生命周期不能超过被引用对象的生命周期,很明显,这里违反了这条规则,所以无法通过编译。

Rust解决野指针最重要的方法就是生命周期,这里只是介绍了最简单的一个场景,在学习Rust时,一定要理解生命周期的含义。


缓冲区溢出

在C++中,以vector为例,想要以索引的方式访问某个对象时,我们通常会使用vector的at方法进行访问,at方法会进行数组越界检测,这很安全。

但是vector可以通过data方法返回一个C/C++的原生数组,当我们对原生数组进行索引操作时,完全是一种走钢丝的行为。

因为没有任何越界检测,此时如果发生缓冲区溢出,将会是一个未定义的行为。如果影响了其他变量,那么这将会是一个非常难排查的问题;如果改动了不可写的地址,那么会导致程序崩溃;如果运气好溢出的部分没有影响到任何对象,那看起来将会是一切安好,但是我们并不总是有那么好的运气。

这种未定义的行为绝对不是我们想要的。来看看Rust是怎么做的。

fn main() {let vec: Vec<i32> = vec![1,2,3];let vec_ref: &[i32] = &vec[0 ..];for i in 0 .. 4 {println!("{}", vec_ref[i]);}

上面是在rust中创建了一个vector——vec,其长度为3(内容为1、2、3),然后一个引用vec_ref(指针)指向这个vec。

紧接着使用引用vec_ref故意进行了一次缓冲区溢出的轮询操作, 此时我们能够正常通过编译。这当然能够编译通过,千万不要妄想Rust能够在编译期解决缓冲区溢出这种主要在运行期出现的问题

但是cargo run运行时

在这里插入图片描述

可以清楚的看到导致了panic,提示长度是3,但是index也是3,出现了缓冲区溢出的访问。也就是说Rust对于缓冲区溢出的访问会有一个已定义的行为——导致线程panic。但是新问题又来了,为什么一个引用(指针)vec_ref也有长度信息呢?

如果只是一个普通的引用当然不会有长度信息,但是这里的引用vec_ref是对一个连续数据vec的引用。在Rust中,vec_ref准确的说是一个切片。对一个连续数据的引用(切片),引用本身是一个胖指针,即该引用占两个机器字(普通引用只是一个普通指针,内存上只占用一个机器字)的内存,第一个机器字是被引用的连续数据的首地址;第二个机器字是连续数据的长度。

下面是打印两种引用占用内存大小的代码。

fn main() {let vec: Vec<i32> = vec![1,2,3];let vec_ref: &[i32] = &vec[0 ..];let num: i32 = 3;let num_ref: &i32 = &num;println!("vec_ref size_of:{} num_ref size_of:{}",std::mem::size_of_val(&vec_ref), std::mem::size_of_val(&num_ref))
}

输出为vec_ref size_of:16 num_ref size_of:8。说明,引用(切片)vec_ref占用16Bytes,引用num_ref占用8Bytes,我的电脑是64位的电脑,刚好是两个机器字和一个机器字。

除了切片之外,Rust中的原生数组也是带有长度信息的,所以在使用原生数组出现缓冲区溢出时,也会导致已定义的行为。

综上,因为缓冲区溢出主要是一个运行期的行为,所以Rust也没办法做到在编译期解决这个问题,但是通过胖指针的方式,Rust做到了在运行期如果出现缓冲区溢出,那一定会有一个已定义的行为——线程panic。这肯定好过C/C++中缓冲区溢出后,各种未定义的奇葩问题。


对空指针进行解引用

C/C++中对空指针解引用导致的崩溃问题更多的是开发人员个人编程习惯导致的。

在C/C++中,一个更好的编程习惯是在解引用指针之前,先对指针进行判空操作,但是这样简单的一个判断逻辑常常因为开发同学的“自信”,导致在很多地方偷懒忽略,然后直接对指针解引用后开始操作。往往越是自信不会为空的地方越是会给我们带来最承重的打击。

针对空指针解引用,首先Safe Rust中只有引用没有指针,这里的引用和C++中的引用类似,本质也是一个指针。Safe Rust中,在使用一个引用之前,必须对引用赋值,否则无法通过rustc的检测

fn main() {let s: String = String::new();let s_ref: &String = &s;
}

s是一个String类型的变量,s_ref是对s的一个引用。只有对s_ref赋值后才能对s_ref进行使用。rustc通过强制检测你的编码实现,杜绝了空指针的使用。

当然,一定存在一个场景。某个引用,其需要引用的对象可能在程序运行之初并没有被创建,随着程序的运行才创建,创建后还需要让这个引用指向这个刚创建的对象,也就是说需要Rust支持引用一开始为空,随着程序的运行才被赋值的情况。

上面这种场景下需要采用OptionRust中,一切可能为空的东西都需要使用Option进行包裹,不仅仅是引用。

fn main() {let mut s_ref_option: Option<&String> = None;let s: String = String::new();s_ref_option = Some(&s);
}

这一次s_ref_option因为可能为空,所以被声明为Option<&String>类型的None,语义为,有一个T&String类型的Option,这个Option目前包裹的值是None,但是后面可能会赋值,所以后续要想获取s_ref_option中包裹的&String时,你需要进行检查,因为不确定后面会不会赋值。

紧接着,s才被创建,然后使用Some包裹后赋值给s_ref_option。

通过Option获取其包裹的值通常有两种做法,一种是安全的,一种是不安全的。安全的操作是在使用之前对Option进行判空,显而易见这很安全。

// 使用 s_ref_option 时判空
if let Some(v) = s_ref_option {//... v 是&String
}

但这在Rust中也不是强制的,开发人员也可以以一种不安全的方式使用Option——直接获取Option中包裹的值。

// 不判空直接获取Option中包裹的值
let s_ref: &String = s_ref_option.unwrap();

这和C/C++中直接进行空指针解引用并没有什么区别。

但是好在可以通过rustc中内置的静态代码检测工具clippy,对代码进行扫描,如果检测到代码中有使用unwrap,那么直接报error,clippy帮助检查代码中是否有这种危险的使用。这可以理解为是Rust编程的一种规范,让不写unwrap作为Rust编程规范的一部分。

clippy中可以通过设置clippy::restriction集中的unwrap_used这条规范达到我们的目的,具体可以看我的另一篇博客 Rust代码静态分析工具Clippy浅析

综上,Rust通过编译器,强制检测引用(指针)在使用之前必须赋值解决了这个问题。对于可能为空的对象,配合clippy使用,对于是否可以直接解引用可能为空的对象的选择权留给开发者,也不为是一种比较好的方案。


非法释放内存

C/C++中存在非法释放内存的情况,比如double free、非法释放栈上的内存等等,这些操作都会导致程序的崩溃。

作为非GC系的语言,Rust也面临释放内存资源的问题。但是当你真正开始使用Safe Rust时会发现,你基本不需要关心内存的释放,因为Rust将C++中的精华RAII发挥到了极致。

对于需要进行内存管理的对象类型,其都会实现Drop 特型,定义如下:

pub trait Drop {// Required methodfn drop(&mut self);
}

实现该特型的类型,其实例在被释放前都会调用这个方法,类型的实现者可以在drop中释放自己管理的资源,这和C++中的析构函数一样。RAII在Rust中被大量采用,所以作为一个Rust的开发者,在Safe Rust中,你基本不需要再去进行内存管理。

总结

Rust作为一颗冉冉升起的新星,已经得到了越来越多人的认可,将其压入你的技术栈,一定会是一个不错的选择。


相关文章:

对比C++,Rust在内存安全上做的努力

简介 近年来&#xff0c;越来越多的组织表示&#xff0c;如果新项目在技术选型时需要使用系统级开发语言&#xff0c;那么不要选择使用C/C这种内存不安全的系统语言&#xff0c;推荐使用内存安全的Rust作为替代。 谷歌也声称&#xff0c;Android 的安全漏洞&#xff0c;从 20…...

如何利用 Qt 的模块化架构组织大型项目

目录 1. 大型项目的架构设计 1.1 分层架构 1.2 事件驱动与异步架构 2. 模块划分与职责分离 2.1 功能模块划分 2.2 模块之间的依赖管理 3. 跨平台开发与模块复用 在大型软件项目中&#xff0c;随着代码量的增加和功能的扩展&#xff0c;项目的复杂度会显著提升。没有良好…...

探索Python词云库WordCloud的奥秘

文章目录 探索Python词云库WordCloud的奥秘1. 背景介绍&#xff1a;为何选择WordCloud&#xff1f;2. WordCloud库简介3. 安装WordCloud库4. 简单函数使用方法5. 应用场景示例6. 常见Bug及解决方案7. 总结 探索Python词云库WordCloud的奥秘 1. 背景介绍&#xff1a;为何选择Wo…...

MySQL根据idb文件恢复数据

首先得有对应表的idb文件以及建表语句 1.首先在新数据库建表 CREATE TABLE sys_menu (id bigint(20) NOT NULL,parent_id bigint(20) NULL DEFAULT NULL,name varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL,type int(11) NULL DEFAULT …...

hadoop-mapreduce词频统计

一、Map Reduce主要阶段 二、词频统计示例 0.MapReduce 词频统计(Word Count)示例图 1. Input 阶段&#xff08;输入阶段&#xff09; 输入数据是一段文本&#xff0c;如下&#xff1a; Hadoop is a big data framework. Hadoop can store vast data. Hadoop processes big …...

精心修炼Java并发编程(JUC)-volatile与synchronized关键字

volatile volatile 是 JVM 提供的 最轻量级的同步机制&#xff0c;中文意思是不稳定的&#xff0c;易变的&#xff0c;用 volatile 修饰变量是为了保证变量在多线程中的可见性&#xff0c;它表达的含义是&#xff1a;告诉编译器&#xff0c;对这个变量的读写&#xff0c;需要基…...

【ROS2】ROS2 与 ROS1 编码方式对比(Python实现)

目录 一、初始化和关闭节点二、发布者三、订阅者四、服务端五、客户端六、参数管理七、日志记录八、生命周期管理 ROS2 在 Python 编程中引入了一些新的概念和 API&#xff0c;这些变化使得代码更加模块化和易于维护。特别是 rclpy 库提供了更丰富的功能和更好的错误处理机制&a…...

ElasticSearch的下载和基本使用(通过apifox)

1.概述 一个开源的高扩展的分布式全文检索引擎&#xff0c;近乎实时的存储&#xff0c;检索数据 2.安装路径 Elasticsearch 7.8.0 | Elastic 安装后启动elasticsearch-7.8.0\bin里的elasticsearch.bat文件&#xff0c; 启动后就可以访问本地的es库http://localhost:9200/ …...

城市轨道交通运营控制指挥中心设计方案

为某城市轨道交通运营控制指挥中心(OCC)的设计提供方案时,我们需要考虑到多个方面的需求,包括系统架构、设备选择、功能实现、数据流与监控、通信管理等。以下是一个综合性的设计方案,涉及系统硬件和软件的选择、布局规划、安全性等方面,以确保指挥中心的高效运作、实时监…...

多目标优化算法:多目标河马优化算法(MOHOA)求解ZDT1、ZDT2、ZDT3、ZDT4、ZDT6,提供完整MATLAB代码

一、河马优化算法 河马优化算法&#xff08;Hippopotamus optimization algorithm&#xff0c;HO&#xff09;由Amiri等人于2024年提出的一种模拟自然界中河马觅食行为的新型群体智能优化算法。该算法由Mohammad Hussein Amiri等人于2024年2月发表在Nature旗下子刊《Scientifi…...

线程与进程的个人理解

进程&#xff08;Process&#xff09;&#xff1a; 一个程序在执行时&#xff0c;操作系统为其分配的资源&#xff08;如内存、CPU 时间等&#xff09;构成了一个进程。每个进程都有自己的独立的地址空间、堆栈和局部变量&#xff0c;它们之间不共享内存&#xff08;除非通过特…...

vscode的项目给gitlab上传

目录 一.创建gitlab帐号 二.在gitlab创建项目仓库 三.Windows电脑安装Git 四.vscode项目git上传 一.创建gitlab帐号 二.在gitlab创建项目仓库 图来自:Git-Gitlab中如何创建项目、创建Repository、以及如何删除项目_gitlab新建项目-CSDN博客&#xff09; 三.Windows电脑安…...

企业微信定位打卡

废话少说&#xff1a;定位修改软件链接奉上 一、定位打卡原理 GPS定位&#xff1a;企业微信可以利用手机的GPS功能进行定位&#xff0c;这是一种基于卫星的定位技术&#xff0c;能够提供相对精确的位置信息&#xff0c;通常精确度在20米以内。这种方式耗电较大&#xff0c;且在…...

libaom 源码分析:码率控制介绍

码率控制 命令行码率控制选项:可以看到码率控制包括丢帧、resize、超分、码控模式、目标码率、目标上限下限(类似 x264、x265 中的 VBV)、码控偏置、GOP 码率等。Rate Control Options:--drop-frame=<arg> Temporal resampling threshold (buf %)--resize-mo…...

RK3568平台开发系列讲解(DMA篇)DMA engine使用

🚀返回专栏总目录 文章目录 一、申请DMA channel二、配置DMA channel的参数三、获取传输描述(tx descriptor)四、启动传输沉淀、分享、成长,让自己和他人都能有所收获!😄 📢DMA子系统下有一个帮助测试的测试驱动(drivers/dma/dmatest.c), 从这个测试驱动入手我们了解…...

C++中的函数对象

C 中函数对象的定义和特点 定义&#xff1a;函数对象&#xff08;Function Object&#xff09;也叫仿函数&#xff08;Functor&#xff09;&#xff0c;是一个类&#xff0c;这个类重载了函数调用运算符()。当创建这个类的对象后&#xff0c;可以像使用函数一样使用这个对象&am…...

Linux指标之平均负载(The Average load of Linux Metrics)

&#x1f49d;&#x1f49d;&#x1f49d;欢迎来到我的博客&#xff0c;很高兴能够在这里和您见面&#xff01;希望您在这里可以感受到一份轻松愉快的氛围&#xff0c;不仅可以获得有趣的内容和知识&#xff0c;也可以畅所欲言、分享您的想法和见解。 推荐:Linux运维老纪的首页…...

盛最多水的容器

本节将数组与坐标轴共同组成一个容器,通过改变容器的两个端点使容器装的水最多,容器两个端点不断移动可以通过左右指针算法解决. 问题描述: 给定两个非负整数k1,k2...km每个数代表坐标中的一个点(i,ki).在坐标内绘制m条垂线,垂直线i的两个端点分别为(i,k1)和(i,0)找出其中的两…...

光伏功率预测!Transformer-LSTM、Transformer、CNN-LSTM、LSTM、CNN五模型时序预测

目录 预测效果基本介绍程序设计参考资料 预测效果 基本介绍 Transformer-LSTM、Transformer、CNN-LSTM、LSTM、CNN五模型多变量时序光伏功率预测 (Matlab2023b 多输入单输出) 1.程序已经调试好&#xff0c;替换数据集后&#xff0c;仅运行一个main即可运行&#xff0c;数据格式…...

java全栈day10--后端Web基础(基础知识)

引言&#xff1a;只要能通过浏览器访问的网站全是B/S架构&#xff0c;其中最常用的服务器就是Tomcat 在浏览器与服务器交互的时候采用的协议是HTTP协议 一、Tomcat服务器 1.1介绍 官网地址&#xff1a;Apache Tomcat - Welcome! 1.2基本使用(网上有安装教程&#xff0c;建议…...

使用爬虫时,如何确保数据的准确性?

在数字化时代&#xff0c;数据的准确性对于决策和分析至关重要。本文将探讨如何在使用Python爬虫时确保数据的准确性&#xff0c;并提供代码示例。 1. 数据清洗 数据清洗是确保数据准确性的首要步骤。在爬取数据后&#xff0c;需要对数据进行清洗&#xff0c;去除重复、无效和…...

Burp入门(4)-扫描功能介绍

声明&#xff1a;学习视频来自b站up主 泷羽sec&#xff0c;如涉及侵权马上删除文章 感谢泷羽sec 团队的教学 视频地址&#xff1a;burp功能介绍&#xff08;1&#xff09;_哔哩哔哩_bilibili 本文介绍burp的主动扫描和被动扫描功能。 一、主动扫描 工作原理&#xff1a; 主动…...

Tourtally:颠覆传统的AI智能旅行规划革命

# Tourtally&#xff1a;颠覆传统的AI智能旅行规划革命 在快速变化的旅行科技世界里&#xff0c;一个划时代的平台正在重新定义我们探索世界的方式。让我们一起认识 Tourtally&#xff0c;这个由人工智能驱动的旅行规划助手&#xff0c;正在彻底改变旅行体验。 ## 旅行规划的…...

chrome允许http网站打开摄像头和麦克风

第一步 chrome://flags/#unsafely-treat-insecure-origin-as-secure 第二步 填入网址&#xff0c;点击启用 第三步 重启 Chrome&#xff1a;设置完成后&#xff0c;点击页面底部的 “Relaunch” 按钮&#xff0c;重新启动 Chrome 浏览器&#xff0c;使更改生效。...

视觉经典神经网络与复现:深入解析与实践指南

目录 引言 经典视觉神经网络模型详解 1. LeNet-5&#xff1a;卷积神经网络的先驱 LeNet-5的关键特点&#xff1a; 2. AlexNet&#xff1a;深度学习的突破 AlexNet的关键特点&#xff1a; 3. VGGNet&#xff1a;深度与简洁的平衡 VGGNet的关键特点&#xff1a; 4. ResNe…...

ByConity ELT 测试体验

在实际业务中&#xff0c;用户会基于不同的产品分别构建实时数仓和离线数仓。其中&#xff0c;实时数仓强调数据能够快速入库&#xff0c;且在入库的第一时间就可以进行分析&#xff0c;低时延的返回分析结果。而离线数仓强调复杂任务能够稳定的执行完&#xff0c;需要更好的内…...

对象键值对内容映射

对象映射&#xff1a; 数据字段的英文名映射为更易理解的中文标签进行展示。即数据字段英文名 -> 中文描述。 作用&#xff1a; 提高代码的可读性。支持数据字段与展示内容的解耦&#xff0c;方便修改展示语言或样式&#xff0c;而无需改动数据源。 映射特点&#xff1a…...

《生成式 AI》课程 第7講:大型語言模型修練史 — 第二階段: 名師指點,發揮潛力 (兼談對 ChatGPT 做逆向工程與 LLaMA 時代的開始)

资料来自李宏毅老师《生成式 AI》课程&#xff0c;如有侵权请通知下线 Introduction to Generative AI 2024 Springhttps://speech.ee.ntu.edu.tw/~hylee/genai/2024-spring.php 摘要 这一系列的作业是为 2024 年春季的《生成式 AI》课程设计的&#xff0c;共包含十个作业。…...

学习C#中的反射

在C#编程中&#xff0c;反射&#xff08;Reflection&#xff09;是一项强大且灵活的技术&#xff0c;它允许程序在运行时动态地获取类型信息、创建对象实例、调用方法、访问字段和属性等。这种机制极大地增强了程序的动态性和可扩展性&#xff0c;使得开发者能够在编译时未知的…...

学习使用jquery实现在指定div前面增加内容

学习使用jquery实现在指定div前面增加内容 设计思路代码示例 设计思路 选择要添加内容的指定元素‌&#xff1a; 使用jQuery选择器来选择你希望在其前添加内容的元素。例如&#xff0c;如果你有一个 元素&#xff0c;其ID为qipa250&#xff0c;你可以使用$(‘#qipa250’)来选择…...

郑州外贸网站建设公司价格/东莞网络推广排名

Android中&#xff0c;注册BroadcastReceiver&#xff08;广播&#xff09;有两种方式&#xff1a; 1.在代码中注册&#xff1a; context.registerReceiver(receriver,filter); 2.在AndroidManifest中注册&#xff0c;使用这种方式注册时&#xff0c;即使程序已经退出&#xff…...

昆山做网站的jofuns/网站关键词排名优化系统

函数 函数的作用&#xff1a; 函数&#xff1a;function,是被设计为执行特定任务的代码块 函数可以把具有相同的或者相似逻辑的代码“包裹”起来&#xff0c;通过函数调用执行&#xff0c;为了代码的多次使用&#xff08;重用&#xff09;&#xff08;复用&#xff09; 一次编…...

如何做服装的微商城网站/南昌网站优化公司

书法要讲结构——新版本的瑕疵6说明此瑕疵只是疑似——或许“不”。书法部编版小学语文教材2019学年六年级上册34页“书写提示”&#xff1a;浅析课本中两个“密”字的结构似不妥。先把课本的“密”字放大&#xff1a;再拿它与规范的正楷&#xff08;手写楷书与印刷体楷体&…...

网站建设云南/市场营销策略

我们自己要意识到&#xff0c;不要把谈话当成义务&#xff0c;而内心抗拒&#xff0c;需要以成年人的姿态对待父母。要有技巧地和父母沟通&#xff0c;主动找话题是一个不错的方法。比如教父母用微信、支付宝。分享别人的事情。分享别人的事情时能让人产生强烈的好奇。多表达父…...

企业网站的建立要做的准备/网站建设开发

转载自&#xff1a;http://www.cnblogs.com/SissyNong/archive/2009/09/22/1571752.html 一、获取当前文件的路径 1. System.Diagnostics.Process.GetCurrentProcess().MainModule.FileName 获取模块的完整路径&#xff0c;包括文件名。 2. System.Environment.CurrentDirect…...

wordpress 大不开/关键词长尾词优化

DevExpress拥有.NET开发需要的所有平台控件&#xff0c;包含600多个UI控件、报表平台、DevExpress Dashboard eXpressApp 框架、适用于 Visual Studio的CodeRush等一系列辅助工具。 屡获大奖的软件开发平台DevExpress 2022年第一个重要版本——v22.1已正式发布&#xff0c;该版…...