一文详解Rust中的字符串

news2024/11/19 22:50:09

有人可能会说,字符串这么简单还用介绍?但是很多人学习rust受到的第一个暴击就来自这浓眉大眼、看似毫无难度的字符串。

请看下面的例子。

fn main() {
  let my_name = "World!";
  greet(my_name);
}

fn greet(name: String) {
  println!("Hello, {}!", name);
}

这段简单Hello world的代码看起来没什么问题,但是在rust里却编译不了。

error[E0308]: mismatched types
 --> src/main.rs:3:11
  |
3 |     greet(my_name);
  |           ^^^^^^^
  |           |
  |           expected struct `std::string::String`, found `&str`
  |           help: try using a conversion method: `my_name.to_string()`

error: aborting due to previous error

报错的意思是,greet函数需要一个String类型的参数,但是提供了一个&str类型的实参。

这下不觉得字符串简单了吧?

学习Rust你必须理解&str和String的区别。别急,你还经常会在代码里看到 &'static str&[u8]&[u8; N]Vec<u8>OsStrOsStringCStrCString

这张图很好地描绘了学习Rust后再谈到字符串的情形:
在这里插入图片描述

本文就介绍一下这些字符串相关的类型。

先来说说&str

&str

str类型也叫字符串切片,是最基本的字符器类型,通常是借用的试出现,也就是&str

什么是切片?

在rust里,切片是连续序列[T]的动态大小视图 ,切片是内存块的视图,表示为指针和长度。 这样的定义会让人难以理解。其实slice就是一种引用,允许你对一个连续序列中元素进行引用。

fn main() {
    let a = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
    
    let slice = &a[3..7];

    println!("{:#?}", slice);
}

let slice = &a[3..7];这一行我们创建了一个slice。它的内容是:

[
    4,
    5,
    6,
    7,
]

slice的中文翻译切片这个词,很容易让人认为是从一个连续序列中下来一段,很难与引用联想在一起,我认为翻译成片段可能更合适。

理解了slice,&str就好理解了,&str就是字符串的slice。Rust负责保证str是有效的UTF-8。因为通常是以借用引用(&str)的方式出现,因此是不可变的。

在其它语言中常用的字符串操作,如split、find、trim,大小写转换等操作,都是str的方法,并不是由String类型提供。

在这里要注意,在对字符串使用切片语法时需要格外小心。因为字符串的内部是[u8]数组,每个数组的元素是一个u8,所以数组的长度就是字符串的长度,跟你看到的字符串的长度可能是不一样的。

let s = "我是中国人";
println!("{}",s.len());

你以为结果会是5,但是结果是15; 为什么是15,因为这个字符串的字节数是15。

let s = "我是中国人";   
println!("{:?}",s.bytes())

结果是:

Bytes(Copied { it: Iter([230, 136, 145, 230, 152, 175, 228, 184, 173, 229, 155, 189, 228, 186, 186]) })

字符串的len()返回的是字节数,不是UTF-8字符数。

let s = "我是中国人";
println!("{}",s.chars().count());

这时输出的才是5。
所以当直接对字符串对切片时,一定要注意切片的索引必须落在字符之间的边界位置。

let s = "我是中国人";
let a = &s[0..2];
println!("{}",a);

这段代码可以编译,但是在运行时会报错

Compiling playground v0.0.1 (/playground)
    Finished dev [unoptimized + debuginfo] target(s) in 0.39s
     Running `target/debug/playground`
thread 'main' panicked at src/main.rs:4:15:
byte index 2 is not a char boundary; it is inside '我' (bytes 0..3) of `我是中国人`
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace

每个汉字占用3个字节,&s[0..2]只取了前两个节点,所以报错信息告诉你,index 2是不是字符的边界。所以对字符串使用切片语法时需要格外谨慎。

注意: Rust里字符串字面量的类型是&'static str,这涉及到静态生命周期,有兴趣同学可以参考生命周期相关的文章。

String

在rust中String不是基本类型,是个复合类型,它包含了一个私有的u8的vec。

pub struct String {
	vec: Vec<u8>,
}

因为它的唯一字段vec是私有的,所以只能通过String类型提供的构建函数创建String,因此let my_name = "Rust";这样语句创建出来的不是String类型。

因为它的底层是一个vec,所以String支持改变它自身的一些操作,比如push、pop、clear,可以看出来都是针对vec的操作。

let mut s = String::from("abc");

s.push('1');
s.push('2');
s.push('3');

assert_eq!("abc123", s);

let mut s = String::from("abč");

assert_eq!(s.pop(), Some('č'));
assert_eq!(s.pop(), Some('b'));
assert_eq!(s.pop(), Some('a'));

assert_eq!(s.pop(), None);

let mut s = String::from("foo");

s.clear();

assert!(s.is_empty());
assert_eq!(0, s.len());
assert_eq!(3, s.capacity());

&[u8]

&[u8]是一个切片,指向一段连续的内存区域,其中存储着 u8 类型的值(字节)。它不拥有数据,只是借用了数据的引用。

由于不拥有数据,&[u8] 通常用于不可变的字符串操作。可以从 String 或其他字节数组中创建&[u8] 切片。

let mut my_string = String::from("Hello, world!");

// 获取 &[u8] 切片
let my_bytes: &[u8] = my_string.as_bytes();

// 将 &[u8] 转换为 String (需要确保是有效的 UTF-8 编码)
let new_string = String::from_utf8(my_bytes.to_vec()).unwrap();

&[u8;N]

&[u8; N] 表示一个指向长度为 N 的 u8 类型数组的切片。

&[u8]的区别是,&[u8] 是一个指向任意长度u8 类型数组的切片,可以指向不同长度的数组。&[u8; N]是一个指向固定长度为 N 的字节数组的切片,只能指向长度为 N 的数组。

一个特别常用的场景就是网络协议栈的解析,数据包头通常都是固定长度的,非常适合用&[u8; N]来保存。

Vec<u8>

Vec<u8> 是String类型的底层存储,可以通过String::from_utf8这个方法创建一个String。

&u8

&u8只是 &[u8]切片中的一个元素,也不展开介绍。

OsStr和OsString

这两个类型包含在std::ffi这个模块里,ffi 的意思是 Foreign Function Interface ,外部函数接口,用来调用其它语言(如C语言)编写的函数。因为目前主流的操作系统都是用C语言写的,所以ffi可以用来调用系统接口和处理与操作系统相关的操作。

为什么需要OsStrOsString呢?

因为在不同的操作系统中,字符串的编码是有差异的。
在 Unix 系统上,字符串通常是非零字节的任意序列,通常情况下,这些字符串会被解释为 UTF-8 编码的文本,但并非总是如此。

在 Windows 上,字符串通常是非零 16 位值的任意序列,通常情况下,这些字符串会被解释为 UTF-16 编码的文本,也并非总是如此。

在 Rust 中,字符串始终是有效的 UTF-8 编码,可以包含零。 这意味着 Rust 字符串只能包含有效的 UTF-8 编码的字节序列,但可以包含 0 字节。

因为操作系统原生字符串与Rust字符串的这种差异,因此需要有一种类型能同时表示这两种字符串,并可以在需要时进行相互转换,这种类型就是OsStringOsStr

注意, OsStringOsStr 内部不一定以平台原生的形式保存字符串;

use std::env;
use std::ffi::OsString;

fn main() {
  // 获取命令行参数
  let args: Vec<OsString> = env::args_os().collect();

  // 获取第一个参数(文件名)
  let filename = &args[1];

  // 打印文件名
  println!("Filename: {:?}", filename);
}

Path 和PathBuf

Path 结构表示底层文件系统中的文件路径。有两种样式: Path posix::Path ,用于类 UNIX 系统,以及 windows::Path ,用于 Windows。只所以有两种形式,是因为windows和Unix的路径差别很大,比如路径分隔符就不一样,windows用\,Unix用/
prelude.rs会根据当前平台导出相应的特定于平台 Path 的变体。

Path这个类型是一个切片,是不可变的(immutable),它的owned版本的类型是PathBufPathPathBuf的关系跟strString的关系相似。

因为Path是与操作系统相关的,因此它内部使用的是OsStr

pub struct Path {
    inner: OsStr,
}

下面是Path的代码示例。

use std::path::Path;
use std::ffi::OsStr;

// 注意: 下面代码不能运行在windows下
let path = Path::new("./foo/bar.txt");

let parent = path.parent();
assert_eq!(parent, Some(Path::new("./foo")));

let file_stem = path.file_stem();
assert_eq!(file_stem, Some(OsStr::new("bar")));

let extension = path.extension();
assert_eq!(extension, Some(OsStr::new("txt")));

PathBuf是 Path的 owned版本,是可变的。

use std::path::PathBuf;

let mut path = PathBuf::new();

path.push(r"C:\");
path.push("windows");
path.push("system32");

path.set_extension("dll");

CStr和CString

在C语言中字符串是NUL(\0)为结尾的一维字符数组。
Rust中的CStr表示对以 nul 结尾的字节数组的借用引用,也就是C语言的字符串在Rust中的对应类型。
它可以安全地从 &[u8] 切片构建,也可以不安全地(unsafely)从原始 *const c_char 构建。

因为Rust的字符串必须是UTF-8的,所以CStr要转换为String,需要通过 UTF-8 验证,以保证每个字符都是UTF-8的。

use std::ffi::CStr;
use std::os::raw::c_char;

extern "C" { fn my_string() -> *const c_char; }

unsafe {
    let slice = CStr::from_ptr(my_string());
    println!("string buffer size without nul terminator: {}", slice.to_bytes().len());
}

总结

在Rust语言中有几种字符串相关的类型,&strString是Rust字符串最常用的类型,前者是一个slice,是借用引用,后者则是它的owned版本,可变。OsStrOsString是Rust的字符串和操作系统原生字符串的桥,通过这个桥,Rust的字符串和操作系统原生字符串可以相互转换。PathPathBuf则是Rust为不同的操作系统提供的统一的路径(Path)类型,在内部使用的是OsStr。而CStr则是C语言中以NUL(\0)为结尾的一维字符数组在Rust语言的一种表示。

本文为原创,未经同意不得转载。本文亦发表于https://www.renhl.com/posts/2024/03/17/rust-string-osstring-cstring/

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

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

相关文章

HTML元素语义化补充之css函数(三)

文章目录 CSS中的函数css函数–varcss函数–calccss函数–blurcss函数–gradientlinear-gradient的使用 CSS中的函数 ◼ 在前面我们有使用过很多个CSS函数: 比如rgb/rgba/translate/rotate/scale等; CSS函数通常可以帮助我们更加灵活的来编写样式的值&#xff1b; ◼ 下面有几…

亚稳态及其解决办法

异步电路 亚稳态 亚稳态亚稳态的产生原因什么是同步异步信号怎么消除亚稳态 亚稳态 在数字电路中&#xff0c;每一位数据不是1&#xff08;高电平&#xff09;就是0&#xff08;低电平&#xff09;。当然对于具体的电路来说&#xff0c;并非1&#xff08;高电平&#xff09;就是…

学习Python的第一天(下载Python环境和工具Pycharm)

下载工具 PyCharm Community Edition 2023.3.4 下载环境 Python3.10.4 目录 1.环境Python下载 1.1 Python环境变量下载网址: 1.2 下载步骤 2.工具Pycharm下载 2.1 pycharm下载网址 2.2 pycharm下载步骤 1.环境Python下载 1.1 Python环境变量下载网址: Python Releas…

在离线的arm架构kylin v10服务器上使用Kuboard-Spray搭建K8S集群

在离线的arm架构kylin v10服务器上使用Kuboard-Spray搭建K8S集群 在内网项目中需要安装K8S集群&#xff0c;经过调研&#xff0c;选择使用Kuboard-Spray工具搭建K8S集群&#xff0c;降低学习成本&#xff0c;提高安装效率。 为了简化安装使用集群的过程&#xff0c;搭建了私有…

01.家目录及桌面的认识(Linux基本概念)

知识引入&#xff1a; 我们在打开我们的电脑后&#xff0c;直接就可以看到图形化界面&#xff0c;这个图形化的界面就是我们常说的桌面。同时我们的电脑还可以多用户使用&#xff0c;就是在电脑开机的时候&#xff0c;选择用户&#xff0c;输入密码。这就说明&#xff0c;我们的…

java数据结构与算法基础-----字符串------正则表达式的练习案例---持续补充中

java数据结构与算法刷题目录&#xff08;剑指Offer、LeetCode、ACM&#xff09;-----主目录-----持续更新(进不去说明我没写完)&#xff1a;https://blog.csdn.net/grd_java/article/details/123063846 正则表达式基础&#xff1a;https://blog.csdn.net/grd_java/article/det…

零基础-MySQL数据库的基本操作

①MYSQL数据库基本操作-DDL 1.DDL的解释 DDL&#xff08;Data Definition Language&#xff09;&#xff0c;数据定义语言&#xff0c;包括以下内容&#xff1a; 对数据库的常用操作 对表结构的常用操作 修改表结构 2.对数据库的常用操作 功能SQL查看所有的数据库show d…

阿猪写作能用吗 #媒体#微信

阿猪写作是一个非常实用的论文写作工具&#xff0c;它不仅能帮助用户快速完成论文写作&#xff0c;还可以提供查重降重的功能&#xff0c;帮助用户确保论文的原创性。在当今社会&#xff0c;论文写作是学术界和科研领域最重要的工作之一&#xff0c;而阿猪写作的出现无疑是给这…

使用阿里云服务器搭建网站教程,超简单10分钟网站上线

使用阿里云服务器快速搭建网站教程&#xff0c;先为云服务器安装宝塔面板&#xff0c;然后在宝塔面板上新建站点&#xff0c;阿里云服务器网aliyunfuwuqi.com以搭建WordPress网站博客为例&#xff0c;来详细说下从阿里云服务器CPU内存配置选择、Web环境、域名解析到网站上线全流…

JVM——运行时数据区

前言 由于JAVA程序是交由JVM执行的&#xff0c;所以我们所说的JAVA内存区域划分也是指的JVM内存区域划分&#xff0c;JAVA程序具体执行的过程如下图所示。首先Java源代码文件会被Java编译器编译为字节码文件&#xff0c;然后由JVM中的类加载器加载各个类的字节码文件&#xff0…

RabbitMQ之Plugins插件----AMQP对接MQTT

1.启用插件 rabbitmq-plugins enable rabbitmq_mqtt 2.检查是否启动成功&#xff0c;打开rabbitmq后台 3.概念&#xff1a; AMQP是由交换器和queue队列组成的消息队列机制&#xff0c;MQTT是由订阅主题组成的消息机制 1.MQTT创建连接时会向rabbitmq创建一个自己的queue&…

信号处理之快速傅里叶变换(FFT)

信号处理之快速傅里叶变换FFT 历史溯源欧拉公式傅里叶级数(FS)傅里叶变换(FT)离散傅里叶级数(DFS)离散时间傅里叶变换(DTFT)离散傅里叶变换(DFT)快速傅里叶变换(FFT)MATLAB中常用的FFT工具FFT中常见的问题 历史溯源 相信很多人知道傅里叶变换&#xff0c;但是很多人对傅里叶变…

【Monero】Onion Monero Blockchain Explorer | 洋葱门罗币区块链浏览器

github&#xff1a;onion-monero-blockchain-explorer Onion Monero Blockchain Explorer特点: 没有cookie&#xff0c;没有网络分析跟踪器&#xff0c;没有image&#xff0c; 开源&#xff0c; 完全用C编写&#xff0c; 显示加密的付款 ID&#xff0c; 显示环签名&#xff0c;…

C# WPF编程-控件

C# WPF编程-控件 概述WPF控件类别包括以下控件&#xff1a;背景画刷和前景画刷字体文本装饰和排版字体继承字体替换字体嵌入文本格式化模式鼠标光标 内容控件Label&#xff08;标签&#xff09;Button&#xff08;按钮&#xff09; 概述 在WPF领域&#xff0c;控件通常被描述为…

阿里云原生:如何熟悉一个系统

原文地址:https://mp.weixin.qq.com/s/J8eK-qRMkmHEQZ_dVts9aQ?poc_tokenHMA-_mWjfcDmGVW6hXX1xEDDvuJPE3pL9-8uSlyY 导读&#xff1a;本文总结了熟悉系统主要分三部分&#xff1a;业务学习、技术学习、实战。每部分会梳理一些在学习过程中需要解答的问题&#xff0c;这些问题…

一笔画--PTA

文章目录 题目描述思路AC代码 题目描述 输入样例1 3 2 1 2 2 3 输出样例1 Y输入样例2 4 3 1 2 1 3 1 4 输出样例2 N输入样例3 1 0 输出样例3 Y思路 dfs 、欧拉通路、欧拉回路的判定 前导知识 欧拉通路、欧拉回路、欧拉图 无向图&#xff1a; ①设G是连通无向图&#xff0c;则称…

在使用 Java 数据采集时,有哪些需要注意的问题?

近年来&#xff0c;随着网络数据的爆发式增长&#xff0c;爬虫技术在信息收集和数据分析领域发挥着重要作用。而Java作为一种强大的编程语言&#xff0c;其爬虫库和框架也日益受到开发者的青睐。然而&#xff0c;使用Java爬虫也存在一些需要注意的问题。 首先&#xff0c;是合…

【排序算法】实现快速排序值(霍尔法三指针法挖坑法优化随即选key中位数法小区间法非递归版本)

文章目录 &#x1f4dd;快速排序&#x1f320;霍尔法&#x1f309;三指针法&#x1f320;挖坑法✏️优化快速排序 &#x1f320;随机选key&#x1f309;三位数取中 &#x1f320;小区间选择走插入&#xff0c;可以减少90%左右的递归&#x1f309; 快速排序改非递归版本&#x1…

2024阿里云2核2G服务器租用价格99元和61元一年

阿里云2核2G服务器配置优惠价格61元一年和99元一年&#xff0c;61元是轻量应用服务器2核2G3M带宽、50G高效云盘&#xff1b;99元服务器是ECS云服务器经济型e实例ecs.e-c1m1.large&#xff0c;2核2G、3M固定带宽、40G ESSD entry系统盘&#xff0c;阿里云活动链接 aliyunfuwuqi.…

STM32 | Systick定时器(第四天)

STM32 第四天 一、Systick定时器 1、定时器概念 定时器:是芯片内部用于计数从而得到时长的一种外设。 定时器定时长短与什么有关???(定时器定时长短与频率及计数大小有关) 定时器频率换算单位:1GHZ=1000MHZ=1000 000KHZ = 1000 000 000HZ 定时器定时时间:计数个数…