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

Rust 为什么不适合开发 GUI

前言

在当今科技蓬勃发展的时代,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"><EditTextandroid:id="@+id/username_edit_text"android:layout_width="match_parent"android:layout_height="wrap_content"android:hint="用户名"/><EditTextandroid:id="@+id/password_edit_text"android:layout_width="match_parent"android:layout_height="wrap_content"android:hint="密码"android:inputType="textPassword"/><Buttonandroid: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 开发方面有望迎来更广阔的发展前景 。

相关文章:

Rust 为什么不适合开发 GUI

前言 在当今科技蓬勃发展的时代&#xff0c;Rust 编程语言正崭露头角&#xff0c;逐步为世界上诸多重要基础设施提供动力支持。从存储海量信息到应用于 Linux 内核&#xff0c;Rust 展现出强大的实力。然而&#xff0c;当涉及构建 GUI&#xff08;图形用户界面&#xff09;时&…...

消息队列篇--通信协议篇--理解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 = "…...

Java的设计模式详解

摘要&#xff1a;设计模式是软件工程中解决常见问题的经典方案。本文结合Java语言特性&#xff0c;深入解析常用设计模式的核心思想、实现方式及实际应用场景&#xff0c;帮助开发者提升代码质量和可维护性。 一、设计模式概述 1.1 什么是设计模式&#xff1f; 设计模式&…...

实战篇Redis

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

化学方程式配平 第33次CCF-CSP计算机软件能力认证

很经典的大模拟题目 但是还不算难 大模拟题最需要注意的就是细节 写代码一定要考虑全面 并且要细心多debug 多打断点STL库的熟练使用 istringstream真的处理字符串非常好用 注意解耦合思想 这样改代码debug更加清晰 https://www.acwing.com/problem/content/5724/ #includ…...

Java基础-25-继承-方法重写-子类构造器的特点-构造器this的调用

在面向对象编程中&#xff0c;继承是实现代码复用和扩展的重要机制。通过继承&#xff0c;子类可以继承父类的属性和方法&#xff0c;并且可以通过方法重写来改变或扩展父类的行为。此外&#xff0c;构造器在对象初始化过程中扮演了重要角色&#xff0c;尤其是在子类构造器中如…...

nvidia 各 GPU 架构匹配的 CUDA arch 和 CUDA gencode

使用 NVCC 进行编译 cuda c(.cu)时&#xff0c;arch 标志 (-arch) 指定了 CUDA 文件将为其编译的 NVIDIA GPU 架构的名称。 Gencodes (-gencode) 允许更多的 PTX 代&#xff0c;并且可以针对不同的架构重复多次。 NVIDIA 架构名称的列表&#xff0c;以及它们具有的计算能力&am…...

沉浸式体验测评|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;智能合约的审计和分析至关重要。…...

【Easylive】TokenUserInfoDto中@JsonIgnoreProperties和 Serializable 接口作用

【Easylive】项目常见问题解答&#xff08;自用&持续更新中…&#xff09; 汇总版 这段代码定义了一个名为 TokenUserInfoDto 的 DTO&#xff08;数据传输对象&#xff09;&#xff0c;用于封装用户令牌信息。以下是对 JsonIgnoreProperties 和 Serializable 接口作用的详…...

k8s EmptyDir(空目录)详解

1. 定义与特性 emptyDir 是 Kubernetes 中一种临时存储卷类型&#xff0c;其生命周期与 Pod 完全绑定。当 Pod 被创建时&#xff0c;emptyDir 会在节点上生成一个空目录&#xff1b;当 Pod 被删除时&#xff0c;该目录及其数据会被永久清除。它主要用于同一 Pod 内多个容器间的…...

毕业设计:实现一个基于Python、Flask和OpenCV的人脸打卡Web系统(六)

毕业设计:实现一个基于Python、Flask和OpenCV的人脸打卡Web系统(六) Flask Flask是一个使用 Python 编写的轻量级 Web 应用框架。其 WSGI 工具箱采用 Werkzeug ,模板引擎则使用 Jinja2 。Flask使用 BSD 授权。 Flask也被称为 “microframework” ,因为它使用简单的核心,…...

洛谷题单2-P5717 【深基3.习8】三角形分类-python-流程图重构

题目描述 给出三条线段 a , b , c a,b,c a,b,c 的长度&#xff0c;均是不大于 10000 10000 10000 的正整数。打算把这三条线段拼成一个三角形&#xff0c;它可以是什么三角形呢&#xff1f; 如果三条线段不能组成一个三角形&#xff0c;输出Not triangle&#xff1b;如果是…...

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

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

MySQL数据库中,tinyint(1) 和 tinyint 有什么区别

TINYINT(1) 和 TINYINT 的区别 在 MySQL 中&#xff0c;TINYINT(1) 和 TINYINT 本质上是相同的数据类型&#xff0c;但 TINYINT(1) 中的 (1) 实际上不会影响存储大小或取值范围。 1. TINYINT 及其取值范围 TINYINT 是 MySQL 中最小的整数类型&#xff0c;占用 1 个字节 (8 bi…...

android databinding使用教程

Android DataBinding 是一种可以将 UI 组件与数据源绑定的框架&#xff0c;能够减少 findViewById 的使用&#xff0c;并提高代码的可维护性。下面是 DataBinding 的完整使用教程&#xff1a; 1. 启用 DataBinding 在 build.gradle&#xff08;Module 级别&#xff09;中启用 …...

【FreeRtos】任务调度器可以被挂起吗?

1. 省流回答 FreeRTOS的任务调度器可以被挂起&#xff08;Suspend&#xff09;。 通过调用API函数 vTaskSuspendAll()&#xff0c;可以临时禁止任务调度器的运行&#xff0c;此时系统将不再进行任务切换&#xff08;包括抢占式调度和时间片轮转&#xff09;&#xff0c;但中断…...

ES5内容之String接口

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

k8s运维面试总结(持续更新)

一、你使用的promethues监控pod的哪些指标&#xff1f; CPU使用率 内存使用率 网络吞吐量 磁盘I/O 资源限制和配额&#xff1a;Prometheus可以监控Pod的资源请求和限制&#xff0c;确保它们符合预设的配额&#xff0c;防止资源过度使用。具体指标如container_spec_cpu_quota用于…...

中级:MyBatis面试题深度剖析

一、引言 在Java持久层技术中&#xff0c;MyBatis凭借其强大的映射功能和灵活的SQL编写方式&#xff0c;成为许多企业的首选。面试官通过MyBatis相关问题&#xff0c;考察候选人对框架核心组件的理解、配置管理能力以及在实际项目中解决问题的能力。本文将深入剖析MyBatis的配…...

Kubernetes高级应用(NFS存储)

一、介绍 在 **Kubernetes&#xff08;K8s&#xff09;** 中&#xff0c;**NFS&#xff08;Network File System&#xff09;存储** 是一种常见的 **持久化存储&#xff08;Persistent Storage&#xff09;** 解决方案&#xff0c;适用于需要共享存储、数据持久化或跨 Pod 访问…...

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;这些建筑在一般情况下的…...