Rust 为什么不适合开发 GUI

news2025/4/25 12:45:37

前言

在当今科技蓬勃发展的时代,Rust 编程语言正崭露头角,逐步为世界上诸多重要基础设施提供动力支持。从存储海量信息到应用于 Linux 内核,Rust 展现出强大的实力。然而,当涉及构建 GUI(图形用户界面)时,Rust 却面临着诸多挑战。据数据显示,超过 56% 的 Rust 开发者认为其 GUI 开发亟待大幅改进,这也是许多人起初不愿采用 Rust 进行相关开发的重要原因。

Rust 的独特之处

Rust 自诞生之初,便以独特的姿态区别于其他编程语言。在众多编程语言中,垃圾回收机制较为常见,它能自动管理内存的分配与释放,极大减轻了开发者的负担。而 Rust 采用了所有权机制,这一机制在编译时生效。也就是说,值由变量拥有,变量可对值进行引用,当拥有变量超出作用域时,其所拥有的值会被自动释放。

此外,Rust 能够有效防范多线程同时访问相同数据的情况,即数据竞争问题。它通过确保同一时刻要么只有一个可变引用,要么有多个不可变引用,保证引用始终有效,并且当存在有效引用时,相关值不能被修改。同时,Rust 并非像 Java、C++ 或 JavaScript 那样的面向对象语言,它不支持抽象类和类继承。

例如,在面向对象语言中,通常会有一个顶层类 Component,其中包含 draw 方法,像按钮(Button)或文本(Text)等组件会继承自这个类并复用其函数。但在 Rust 中,情况有所不同,它使用 traits。开发者可以在库中添加一个名为 draw 的通用 trait,只要按钮对象、文本对象和图像对象实现了这个 Draw trait,它们就会被视为 UI 组件。甚至可以将一个随机的 sandwich 对象添加到 UI 组件库中,只要它实现了 Draw trait,当然这在实际开发中不太可能通过代码评审。

Rust 构建 GUI 之难

那么,究竟是什么让用 Rust 构建 GUI 如此困难呢?前面提到的 Rust 的独特之处,恰恰也是构建 GUI 时的阻碍因素。在编程领域,UI 通常被设计为树状结构,但使用 Rust 的继承机制构建树状结构极为困难。

以构建一个简单的登录界面为例,在 Android 开发中,视图树形结构有着清晰的层级关系。Android 的视图体系基于 View 和 ViewGroup 类。ViewGroup 是一个特殊的 View,它可以包含多个子 View,就像是树枝可以长出许多树叶一样,这就形成了一个树形结构。

比如,登录界面的最外层可能是一个 LinearLayout(线性布局,属于 ViewGroup 的一种),它决定了内部组件的排列方式是水平还是垂直。在这个 LinearLayout 里,可能有两个 EditText(输入框,属于 View)用于输入用户名和密码,还有一个 Button(按钮,同样属于 View)用于触发登录操作。

在代码实现上,开发者会在 XML 布局文件中描述这个树形结构。假设布局文件名为 activity_login.xml,代码可能如下:

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical">
    <EditText
        android:id="@+id/username_edit_text"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:hint="用户名"/>
    <EditText
        android:id="@+id/password_edit_text"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:hint="密码"
        android:inputType="textPassword"/>
    <Button
        android:id="@+id/login_button"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="登录"/>
</LinearLayout>

在运行时,Android 系统会根据这个 XML 文件构建出相应的视图树形结构。当用户进行操作,比如点击按钮时,事件会从最上层的视图容器开始,沿着树形结构向下传递,找到对应的按钮 View 并触发相应的点击事件处理逻辑。

而在 Rust 中,由于缺乏像 Android 这种基于类继承的成熟视图体系,构建类似的树形结构就变得复杂许多。Rust 的 trait 不存储数据,导致每个组件需要自行管理其下的子组件,这使得遍历树状结构变得困难。

以构建登录界面为例,假设我们定义一个用于绘制 UI 组件的 trait,比如Draw:

trait Draw {
    fn draw(&self);
}

这里的 Draw trait 规定了实现它的类型必须拥有draw方法,但它并没有为实现它的类型提供存储数据的空间。当我们创建登录界面的各个组件(如输入框和按钮)并让它们实现 Draw trait 时,每个组件都需要自行处理数据存储的问题。

struct LoginButton {
    // 按钮的相关数据,如文本、位置等
    text: String,
    x: i32,
    y: i32,
}
impl Draw for LoginButton {
    fn draw(&self) {
        // 绘制按钮的逻辑,使用自身存储的数据
        println!("Drawing button with text: {}", self.text);
    }
}

在上述代码中,LoginButton 结构体实现了 Draw trait,它需要自己定义和管理数据(text、x、y)。相比之下,在 Android 中,视图类(如 EditText、Button)继承自 View 类,View 类及其父类会为子类提供一些默认的数据存储和管理机制,例如位置、大小等属性,子类可以直接使用或继承这些数据。

除了状态管理的不方便,Rust 的可变性规则也给 UI 组件状态的动态更新带来了挑战。Rust 的可变性规则主要用于确保内存安全和避免数据竞争。简单来说,在同一时间内,一个数据要么有多个不可变引用(可以理解为只读访问),要么只有一个可变引用(可以修改数据),但不能同时存在可变和不可变引用。

例如,在登录界面的场景中,如果我们要根据用户输入实时显示错误提示信息,在 Rust 中实现起来就不像在 Android 开发中那么直观。因为可能会涉及到状态的动态更新,此时遇到可变性规则的挑战。

假设我们有一个登录逻辑,需要根据用户名和密码的输入情况更新错误提示信息:

fn login(username: &str, password: &str) -> String {
    let mut error_message = String::new();
    if username.is_empty() {
        error_message.push_str("用户名不能为空");
    }
    if password.len() < 6 {
        if!error_message.is_empty() {
            error_message.push_str(", ");
        }
        error_message.push_str("密码长度至少为6位");
    }
    error_message
}

在这个例子中,error_message 是可变的,以便在不同的条件下添加错误信息。这个代码在单一线程运行,同一时刻只有一个可变引用指向 error_message,是可以通过编译的。但如果在更复杂的 UI 场景中,多个线程或不同的代码块同时尝试访问和修改 error_message,就会违反 Rust 的可变性规则,导致编译错误。因为 Rust 要保证数据在任何时刻的状态都是可预测的,避免出现数据竞争和未定义行为。

比如下面这样

use std::thread;

fn main() {
    let mut error_message = String::new();

    let handle1 = thread::spawn(move || {
        error_message.push_str("线程 1 产生的错误");
    });

    let handle2 = thread::spawn(move || {
        error_message.push_str("线程 2 产生的错误");
    });

    handle1.join().unwrap();
    handle2.join().unwrap();

    println!("{}", error_message);
}

在这个示例里,多个线程同时尝试修改 error_message,Rust 编译器会检测到这种情况并报错,因为这违反了 Rust 的可变性规则,可能会引发数据竞争问题。

当然,多线程更新字符串的情况不多,也许有人说这个例子不具代表性,再看一个更具场景的情况。在 GUI 开发里,常常需要根据用户的操作动态更新 UI 状态,并且重新渲染视图。假设我们要开发一个简单的计数器界面,用户点击按钮时,计数器的值会增加。

在 Rust 里,为了保证内存安全,可变性规则会对状态更新和视图渲染之间的交互产生影响。以下是一个简化的示例代码:

// 假设这是一个简单的 UI 组件
struct Counter {
    value: u32,
}

impl Counter {
    fn increment(&mut self) {
        self.value += 1;
    }
    fn draw(&self) {
        println!("当前计数器的值: {}", self.value);
    }
}

fn main() {
    let mut counter = Counter { value: 0 };
    // 模拟用户点击按钮
    counter.increment();
    counter.draw();
}

在这个示例中,Counter 结构体表示一个计数器组件,increment 方法用于增加计数器的值,draw 方法用于渲染计数器的当前值。编译正常。

如果稍不注意, main 函数写成下面这样,编译就出错了

fn main() {
    let mut counter = Counter { value: 0 };
    let mut_ref = &mut counter;
    mut_ref.increment();

    // 这里会产生编译错误
    counter.draw(); 
}

上面 main 函数中,我们首先对 counter 进行了可变借用,创建了可变引用 mut_ref,并调用 mut_ref.increment() 方法对 counter 的值进行修改。

接着,我们尝试直接调用 counter.draw() 方法。但由于此时 counter 仍处于被可变借用的状态(mut_ref 的生命周期还未结束),Rust 的可变性规则不允许在可变借用期间对同一个数据进行不可变借用。因此,counter.draw() 这行代码会导致编译错误。

error[E0502]: cannot borrow `counter` as immutable because it is also borrowed as mutable
  --> src/main.rs:17:5
   |
15 |     let mut_ref = &mut counter;
   |                   -------- mutable borrow occurs here
16 |     mut_ref.increment();
17 |     counter.draw(); 
   |     ^^^^^^^ immutable borrow occurs here
18 | }
   | - mutable borrow ends here

通过正确和错误两个版本代码的对比,可以看出

  • 错误版本:对 counter 进行了可变借用,创建了可变引用 mut_ref,并且在可变借用的生命周期内尝试对 counter 进行不可变借用,违反了 Rust 的可变性规则。
  • 正确版本:没有同时存在可变借用和不可变借用的冲突情况。先直接调用 counter.increment() 方法对 counter 进行可变操作,操作完成后,可变借用结束,再调用 counter.draw() 方法进行不可变操作,符合 Rust 的可变性规则。

但在实际的 GUI 应用中,UI 组件的状态可能会受到多个因素的影响,状态更新和视图渲染的逻辑也会更加复杂,很可能需要根据不同的条件更新多个 UI 组件的状态,并且在合适的时机进行视图渲染。Rust 的可变性规则会让这种状态管理变得更加困难,稍有不慎就会出现编译错误,增加了开发者的心智负担。

应对之策与实践探索

尽管困难重重,但并非毫无解决办法。有一个专门的网站 https://areweguiyet.com/ 致力于更新 Rust 在 GUI 开发方面的进展情况。在开源社区,也有许多项目取得了显著进展,比如 ICED 或 Tauri,它们使用 Rust 为原生 Web 视图提供支持。

另一种有效的解决方案是完全摒弃面向对象编程,深入采用 Rust 的方式来处理问题。例如使用 ELM 架构,它由模型(Model)、视图(View)和更新(Update)组成。模型存储视图的所有状态,视图将模型数据转换为屏幕上可见的内容,更新则负责使用程序员定义的对象 “MSGs” 来修改模型。

在这里插入图片描述

这种架构其实就是 Android 近年来推崇的 UDF(单项数据流),在 Rust 中实现这种架构有诸多优势,它是功能性且可变的,开发者无需直接修改数据,因为数据始终通过更新函数进行处理。例如,可以插入一个全新的值,由于模型只有一个单一所有者,不会触发任何警报。此外,Rust 的枚举(Enums)使得确定不同数据类型变得容易,开发者可以在代码中轻松进行模式匹配,ICED 项目就有很好的示例展示如何使用 Rust 枚举通过按钮来增加或减少数字。
但是 ELM 架构并非完美无缺,也有一些尝试对其进行替代的方案,其中一种替代方案是实体组件系统架构(Entity Component System Architecture)。在这种架构中,Entity(实体)和 Component(组件)是两个核心概念。

Entity 可以理解为一个唯一的标识符,它本身不包含任何数据或行为。在 ECS 架构里,Entity 就像是一个容器或者一个 “占位符”,用于将不同的 Component 组合在一起。例如一个游戏中,它代表其中一个角色、一个道具,或者 GUI 界面中的一个按钮、一个文本框等。在 Rust 中,Entity 通常用一个简单的整数 ID 来表示。

Component 是包含数据的最小单元,它只负责存储特定类型的数据,而不包含任何行为。例如,在一个游戏中,可能有表示位置的 PositionComponent、表示速度的 VelocityComponent;在 GUI 开发中,可能有表示文本内容的 TextComponent、表示颜色的 ColorComponent 等。每个 Component 专注于一种特定的属性或状态。

在这里插入图片描述

著名的 Warp 终端项目就采用了这种方式实现。将每个组件称为 view,并赋予其一个唯一的 ID,即 entity id。每个窗口存储实体 ID 到实际视图的映射,通过这种方式存储与视图相关的任何状态,并且可以存储每个视图到父视图的映射,以便在树状结构中向上遍历。这些数据以一系列由系统拥有的映射和列表形式存储,这是目前在 Rust 中模拟面向对象编程语言最接近的方式。

通过这种实现方式,Warp 能够创建丰富的 UI 元素,并且性能几乎可与其他任何终端媲美。如果读者对此感兴趣,可以通过视频描述中的链接免费下载 Warp 来体验其 GUI。

展望

在 Rust 构建 GUI 的领域中,尽管充满挑战,但通过不断探索和创新,开发者们已经找到多种有效的解决途径,并且在实践中取得了不错的成果,未来 Rust 在 GUI 开发方面有望迎来更广阔的发展前景 。

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

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

相关文章

消息队列篇--通信协议篇--理解HTTP、TLS和TCP如何协同工作

前面介绍了HTTP/HTTPS&#xff0c;SSL/TLS以及TCP和UDP&#xff0c;这些在网络传输上分别有着自己的作用。为了深入理解下这些概念&#xff0c;本篇重点介绍下HTTP、TLS 和 TCP是如何协同工作的&#xff1f;我们从底层到上层逐步分析每个协议的作用及其相互关系。这些协议共同协…

代码随想录算法训练营第三十四天 | 62.不同路径 63.不同路径II 343.整数拆分

62.不同路径 题目链接&#xff1a;62. 不同路径 - 力扣&#xff08;LeetCode&#xff09; 文章讲解&#xff1a;代码随想录 视频讲解&#xff1a;动态规划中如何初始化很重要&#xff01;| LeetCode&#xff1a;62.不同路径_哔哩哔哩_bilibili 思路&#xff1a;机器人位于一…

2023第十四届蓝桥杯大赛软件赛省赛C/C++ 大学 B 组(真题题解)(C++/Java题解)

记录刷题的过程、感悟、题解。 希望能帮到&#xff0c;那些与我一同前行的&#xff0c;来自远方的朋友&#x1f609; 大纲&#xff1a; 1、日期统计-&#xff08;解析&#xff09;-暴力dfs&#xff08;&#x1f609;蓝桥专属 2、01串的熵-&#xff08;解析&#xff09;-不要chu…

RK3568-适配ov5647摄像头

硬件原理图 CAM_GPIO是摄像头电源控制引脚,连接芯片GPIO4_C2 CAM_LEDON是摄像头led灯控制引脚,连接芯片GPIO4_C3编写设备树 / {ext_cam_clk: external-camera-clock {compatible = "fixed-clock";clock-frequency = <25000000>;clock-output-names = "…

实战篇Redis

黑马程序员的Redis的笔记&#xff08;后面补一下图片&#xff09; 【黑马程序员Redis入门到实战教程&#xff0c;深度透析redis底层原理redis分布式锁企业解决方案黑马点评实战项目】https://www.bilibili.com/video/BV1cr4y1671t?p72&vd_source001f1c33a895eb5ed820b9a4…

沉浸式体验测评|AI Ville:我在Web3小镇“生活”了一周

最近&#xff0c;我在朋友的推荐下&#xff0c;体验了 aivillebot 的项目。起初&#xff0c;我只是抱着试试看的心态&#xff0c;心想这不就是个 Web3 版的《星露谷物语》吗&#xff1f; 但是一周下来&#xff0c;我发现这个虚拟小镇也没那么简单——里面的居民不是目前端游或链…

TTL 值 | 在 IP 协议、ping 工具及 DNS 解析中的作用

注&#xff1a;本文为 “TTL” 相关文章合辑。 未整理去重。 如有内容异常&#xff0c;请看原文。 TTL 值的意义 2007-10-18 11:33:17 TTL 是 IP 协议包中的一个值&#xff0c;用于标识网络路由器是否应丢弃在网络中停留时间过长的数据包。数据包可能因多种原因在一定时间内…

人工智能之数学基础:初等反射阵

本文重点 在线性代数中,初等反射阵(Householder矩阵)作为一类特殊的正交矩阵,在矩阵变换、特征值计算及几何变换等领域具有广泛应用。其简洁的构造方式和丰富的数学性质,使其成为数值分析和几何处理中的重要工具。 什么是初等反射阵(豪斯霍尔德变换) I为单位矩阵,wwT…

4.1 代码随想录第三十二天打卡

准备:完全背包理论基础-二维DP数组 1.完全背包就是同一物品可以往里多次装 2.这里先遍历背包 或物品都可以 3.dp[i][j] 表示从下标为[0-i]的物品&#xff0c;每个物品可以取无限次&#xff0c;放进容量为j的背包&#xff0c;价值总和最大是多少 518.零钱兑换II (1)题目描述…

SQL Server:数据库镜像端点检查

目录标题 **1. 端点的作用****2. 检查的主要内容****&#xff08;1&#xff09;端点是否存在****&#xff08;2&#xff09;端点状态****&#xff08;3&#xff09;协议与端口****&#xff08;4&#xff09;权限配置** **3. 操作步骤&#xff08;示例&#xff09;****&#xff…

【区块链安全 | 第九篇】基于Heimdall设计的智能合约反编译项目

文章目录 背景目的安装1、安装 Rust2、克隆 heimdall-dec3、编译 heimdall-dec4、运行 heimdall-dec 使用说明1、访问 Web 界面2、输入合约信息3、查看反编译结果 实战演示1、解析普通合约2、解析代理合约 背景 在区块链安全研究中&#xff0c;智能合约的审计和分析至关重要。…

批量删除 txt/html/json/xml/csv 等文本文件空白行

我们常常会遇到需要删除 txt 文本文件中空白行的情况&#xff0c;如果文本文件较大&#xff0c;行数较多的时候&#xff0c;有些空白行不容易人工识别&#xff0c;这使得删除文本文件空白行变得非常繁琐&#xff0c;我们需要先找到空白的行&#xff0c;然后才能进行删除操作。尤…

ES5内容之String接口

注意&#xff1a;slice、substr、substring 都接受一个或两个参数&#xff0c;第一个参数指定字符串的开始位置&#xff0c;第二个参数表示子字符串到哪里结束&#xff0c;slice 和 substring 的第二个参数指定的是子字符串的最后一个字符后面的位置&#xff0c;substr 第二个参…

Mysql之事务(下)

&#x1f3dd;️专栏&#xff1a;Mysql_猫咪-9527的博客-CSDN博客 &#x1f305;主页&#xff1a;猫咪-9527-CSDN博客 “欲穷千里目&#xff0c;更上一层楼。会当凌绝顶&#xff0c;一览众山小。” 目录 5. 事务的隔离级别与并发控制 5.1事务的隔离级别 5.2查看与设置事务的…

某地老旧房屋自动化监测项目

1. 项目简介 自从上个世纪90年代以来&#xff0c;我国经济发展迅猛&#xff0c;在此期间大量建筑平地而起&#xff0c;并且多为砖混结构的住房&#xff0c;使用寿命通常约为30-50年&#xff0c;钢筋混凝土结构&#xff0c;钢结构等高层建筑&#xff0c;这些建筑在一般情况下的…

【QT】QT的多界面跳转以及界面之间传递参数

QT的多界面跳转以及界面之间传递参数 一、在QT工程中添加新的界面二、多界面跳转的两种情况1、A界面跳到B界面&#xff0c;不需要返回2、A界面跳到B界面&#xff0c;需要返回1&#xff09;使用this指针传递将当前界面地址传递给下一界面2&#xff09;使用parentWidget函数获取上…

【学习笔记】计算机网络(五)

第5章 运输层 文章目录 第5章 运输层5.1 运输层协议概述5.1.1 进程之间的通信5.1.2 运输层的两个主要协议5.1.3 运输层的端口 5.2 用户数据报协议 UDP5.2.1 UDP 概述5.2.2 UDP的首部格式 5.3 传输控制协议 TCP 概述5.3.1 TCP 最主要的特点5.3.2 TCP 的连接 5.4 可靠传输的工作原…

鸿蒙NEXT小游戏开发:记忆翻牌

1. 引言 在本案例中&#xff0c;我们将使用鸿蒙NEXT框架开发一个简单的记忆翻牌游戏。该游戏的核心逻辑是玩家通过翻转卡片来寻找匹配的对。本文将详细介绍游戏的实现过程&#xff0c;包括卡片的展示、匹配逻辑以及用户交互。 2. 开发环境准备 电脑系统&#xff1a;windows 1…

Java常用工具算法-2--加密算法1--对称加密算法(推荐AES算法)

1、定义与核心原理 定义&#xff1a;加密和解密使用相同密钥的算法。工作流程&#xff1a; 秘钥协商&#xff1a;双方需提前通过安全信道共享密钥。加密过程&#xff1a;发送方用密钥对明文加密&#xff0c;生成密文。解密过程&#xff1a;接收方用相同密钥对密文解密&#xf…

RK3588使用笔记:导出做好的文件系统

一、前言 初始镜像一般都比较空&#xff0c;当费劲八嘞的装了一堆环境之后&#xff0c;得知设备还要在做n套&#xff0c;想想每一套都要无穷的调试配置和在线更新一堆安装包&#xff0c;是不是脑壳痛&#xff0c;所以导出文件系统的功能就有需求了&#xff0c;本文介绍如何导出…