为啥在 Rust 中将字符串的第一个字母大写如此复杂?
Posted
技术标签:
【中文标题】为啥在 Rust 中将字符串的第一个字母大写如此复杂?【英文标题】:Why is capitalizing the first letter of a string so convoluted in Rust?为什么在 Rust 中将字符串的第一个字母大写如此复杂? 【发布时间】:2016-11-19 07:04:12 【问题描述】:我想将&str
的第一个字母大写。这是一个简单的问题,我希望有一个简单的解决方案。直觉告诉我做这样的事情:
let mut s = "foobar";
s[0] = s[0].to_uppercase();
但是&str
s 不能像这样被索引。我能够做到的唯一方法似乎过于复杂。我将&str
转换为迭代器,将迭代器转换为向量,将向量中的第一项大写,这将创建一个迭代器,我将其编入索引,创建一个Option
,我将其展开以提供上限值- 首字母大写。然后我将向量转换为一个迭代器,我将其转换为String
,我将其转换为&str
。
let s1 = "foobar";
let mut v: Vec<char> = s1.chars().collect();
v[0] = v[0].to_uppercase().nth(0).unwrap();
let s2: String = v.into_iter().collect();
let s3 = &s2;
还有比这更简单的方法吗?如果有,是什么?如果不是,为什么 Rust 会这样设计?
Similar question
【问题讨论】:
这是一个简单的问题——不,不是。解释为德语时请大写ß
。提示:它不是单个字符。甚至问题陈述也可能很复杂。例如,将姓氏von Hagen
的第一个字符大写是不合适的。这是生活在一个全球性世界的一个方面,这个世界有着数千年的不同文化和不同的实践,我们正试图将所有这些压缩成 8 位和 2 行代码。
您提出的问题似乎是字符编码问题,而不是数据类型问题。我认为 char::to_uppercase 已经正确处理了 Unicode。我的问题是,为什么需要所有数据类型转换?似乎索引可以返回一个多字节的 Unicode 字符(不是单字节字符,它只假定为 ascii),并且 to_uppercase 可以返回它所使用的任何语言的大写字符,如果该语言可用的话。跨度>
@marshallm char::to_uppercase
确实解决了这个问题,但是您只使用第一个代码点 (nth(0)
) 而不是构成大写的所有代码点,从而放弃了它的努力
字符编码不是Joel on Software: Unicode指出的简单过程。
@Shepmaster,一般你是对的。这是一个简单的英语问题(编程语言和数据格式的事实上的标准基础)。是的,有些脚本甚至连“大写”都不是一个概念,而有些脚本则非常复杂。
【参考方案1】:
为什么这么复杂?
让我们逐行分解
let s1 = "foobar";
我们创建了一个以UTF-8 编码的文字字符串。 UTF-8 允许我们以非常紧凑的方式对 Unicode 的 1,114,112 code points 进行编码,如果您来自世界上主要输入 ASCII 中的字符的地区,该标准创建于 1963 年。UTF- 8 是一个可变长度编码,这意味着单个代码点可能take from 1 to 4 bytes。较短的编码保留给 ASCII,但 many Kanji take 3 bytes in UTF-8。
let mut v: Vec<char> = s1.chars().collect();
这将创建一个char
acters 向量。字符是直接映射到代码点的 32 位数字。如果我们从纯 ASCII 文本开始,我们的内存需求就会增加四倍。如果我们有一堆来自the astral plane 的字符,那么也许我们还没有使用那么多。
v[0] = v[0].to_uppercase().nth(0).unwrap();
这会获取第一个代码点并请求将其转换为大写变体。不幸的是,对于我们这些从小说英语长大的人来说,有not always a simple one-to-one mapping of a "small letter" to a "big letter"。旁注:我们称它们为大写和小写because one box of letters was above the other box of letters back in the day。
当代码点没有对应的大写变体时,此代码将出现混乱。实际上,我不确定这些是否存在。当代码点具有包含多个字符的大写变体时,它也可能在语义上失败,例如德语 ß
。请注意, ß 在 The Real World 中可能永远不会真正大写,这是我始终记得和搜索的唯一示例。截至2017-06-29,其实官方的德语拼写规则已经更新为both "ẞ" and "SS" are valid capitalizations!
let s2: String = v.into_iter().collect();
这里我们将字符转换回 UTF-8 并需要一个新的分配来存储它们,因为原始变量存储在常量内存中,以便在运行时不占用内存。
let s3 = &s2;
现在我们引用String
。
这是一个简单的问题
不幸的是,这不是真的。也许我们应该努力将世界转换为Esperanto?
我认为
char::to_uppercase
已经正确处理了 Unicode。
是的,我当然希望如此。不幸的是,Unicode 在所有情况下都不够用。
感谢huon for pointing out Turkish I,其中大写 (İ) 和小写 (i) 版本都有一个点。也就是说,i
的字母没有一个正确的大写;这也取决于源文本的locale。
为什么需要所有数据类型转换?
因为当您担心正确性和性能时,您使用的数据类型很重要。 char
是 32 位的,字符串是 UTF-8 编码的。它们是不同的东西。
索引可以返回一个多字节的 Unicode 字符
这里可能有一些不匹配的术语。 char
是一个多字节 Unicode 字符。
切片如果你逐字节地分割字符串是可能的,但如果你不在字符边界上,标准库会恐慌。
从未实现对字符串进行索引以获取字符的原因之一是因为很多人将字符串误用作 ASCII 字符数组。将字符串索引到 set 一个字符永远不会有效 - 您必须能够将 1-4 个字节替换为也是 1-4 个字节的值,从而导致字符串的其余部分弹跳很多。
to_uppercase
可以返回大写字符
如上所述,ß
是单个字符,大写后变为两个字符。
解决方案
另请参阅trentcl's answer,它只使用大写的 ASCII 字符。
原创
如果我必须编写代码,它看起来像:
fn some_kind_of_uppercase_first_letter(s: &str) -> String
let mut c = s.chars();
match c.next()
None => String::new(),
Some(f) => f.to_uppercase().chain(c).collect(),
fn main()
println!("", some_kind_of_uppercase_first_letter("joe"));
println!("", some_kind_of_uppercase_first_letter("jill"));
println!("", some_kind_of_uppercase_first_letter("von Hagen"));
println!("", some_kind_of_uppercase_first_letter("ß"));
但我可能会在 crates.io 上搜索 uppercase 或 unicode,然后让比我聪明的人来处理。
改进
谈到“比我聪明的人”,Veedrac points out 在访问第一个大写代码点之后将迭代器转换回切片可能更有效。这允许其余字节的memcpy
。
fn some_kind_of_uppercase_first_letter(s: &str) -> String
let mut c = s.chars();
match c.next()
None => String::new(),
Some(f) => f.to_uppercase().collect::<String>() + c.as_str(),
【讨论】:
想了很多,对这些设计选择有了更深的理解。标准库应该选择尽可能多才多艺、高性能和安全的折衷方案。否则,它会迫使开发人员做出可能不适合他们的应用程序、架构或语言环境的权衡。否则可能会导致歧义和误解。如果我更喜欢其他取舍,我可以选择一个 3rd-party 库或自己编写。 @marshallm 真是太好了!我担心许多 Rust 新手会误解 Rust 设计者所做的决定,并简单地将它们视为过于复杂而无益的决定。通过在这里提问和回答问题,我对进入此类设计所需的关怀深表赞赏,并希望成为一名更好的程序员。保持开放的心态并愿意学习更多知识是程序员的一大特质。 "Turkish i" 是区域设置依赖的一个例子,它与这个特定问题比排序更直接相关。 我很惊讶他们有 to_uppercase 和 to_lowercase 但没有 to_titlecase。 IIRC,一些 unicode 字符实际上有一个特殊的标题变体。 顺便说一下,即使是单个代码点也可能不是要转换的正确单位。如果第一个字符是在大写时应该接受特殊处理的字素簇怎么办? (如果您只是将基本字符大写,那么分解的变音符号也会起作用,但我不知道这是否普遍正确。)【参考方案2】:还有比这更简单的方法吗?如果有,是什么?如果不是,为什么 Rust 会这样设计?
嗯,是的,也不是。正如另一个答案所指出的那样,您的代码不正确,如果您给它类似བོད་སྐད་ལ་之类的东西,您的代码会感到恐慌。所以用 Rust 的标准库来做这件事比你最初想象的还要难。
但是,Rust 旨在鼓励代码重用并使引入库变得容易。因此,将字符串大写的惯用方式实际上非常可口:
extern crate inflector;
use inflector::Inflector;
let capitalized = "some string".to_title_case();
【讨论】:
用户的问题听起来更像是他想要.to_sentence_case()
。
遗憾的是,它对命名事物没有帮助...这是一个很棒的库,我以前从未见过它,但它的名字(对我来说)很难记住,而且它的功能几乎没有使用实际的变形,其中之一就是你的例子。【参考方案3】:
如果您能够将输入限制为纯 ASCII 字符串,这并不是特别复杂。
从 Rust 1.23 开始,str
有一个 make_ascii_uppercase
方法(在旧的 Rust 版本中,它可以通过 AsciiExt
trait 获得)。这意味着您可以相对轻松地大写纯 ASCII 字符串切片:
fn make_ascii_titlecase(s: &mut str)
if let Some(r) = s.get_mut(0..1)
r.make_ascii_uppercase();
这会将"taylor"
变成"Taylor"
,但不会将"édouard"
变成"Édouard"
。 (playground)
谨慎使用。
【讨论】:
帮助一个 Rust 新手,为什么r
是可变的?我看到s
是一个可变的str
。 Ohhhh 好的:我有自己问题的答案:get_mut
(此处称为带范围)显式返回 Option<&mut>
。【参考方案4】:
我是这样做的:
fn str_cap(s: &str) -> String
format!("", (&s[..1].to_string()).to_uppercase(), &s[1..])
如果不是ASCII字符串:
fn str_cap(s: &str) -> String
format!("", s.chars().next().unwrap().to_uppercase(),
s.chars().skip(1).collect::<String>())
【讨论】:
【参考方案5】:这里有一个版本,比@Shepmaster 的改进版慢一点,但也更惯用:
fn capitalize_first(s: &str) -> String
let mut chars = s.chars();
chars
.next()
.map(|first_letter| first_letter.to_uppercase())
.into_iter()
.flatten()
.chain(chars)
.collect()
【讨论】:
【参考方案6】:这就是我解决这个问题的方法,注意在转换为大写之前我必须检查 self 是否不是 ascii。
trait TitleCase
fn title(&self) -> String;
impl TitleCase for &str
fn title(&self) -> String
if !self.is_ascii() || self.is_empty()
return String::from(*self);
let (head, tail) = self.split_at(1);
head.to_uppercase() + tail
pub fn main()
println!("", "bruno".title());
println!("", "b".title());
println!("", "?".title());
println!("", "ß".title());
println!("", "".title());
println!("", "བོད་སྐད་ལ".title());
输出
Bruno
B
?
ß
བོད་སྐད་ལ
【讨论】:
如果第一个字符有两个字母则不起作用。【参考方案7】:OP 的方法更进一步: 用大写表示替换第一个字符
let mut s = "foobar".to_string();
for i in 1..4
if s.is_char_boundary(i)
let u = &s[0..i].to_uppercase();
s.replace_range(..i, u);
break;
println!("", s);
不需要检查字符串s
是否为空,因为如果索引i
大于s.len()
,is_char_boundary
不会恐慌。
【讨论】:
【参考方案8】:受get_mut examples 的启发,我编写了如下代码:
fn make_capital(in_str : &str) -> String
let mut v = String::from(in_str);
v.get_mut(0..1).map(|s| s.make_ascii_uppercase(); &*s );
v
【讨论】:
【参考方案9】:由于方法to_uppercase()
返回一个新字符串,你应该可以像这样添加字符串的其余部分。
这已在 rust 1.57+ 版本中进行了测试,但可能适用于任何支持 slice 的版本。
fn uppercase_first_letter(s: &str) -> String
s[0..1].to_uppercase() + &s[1..]
【讨论】:
以上是关于为啥在 Rust 中将字符串的第一个字母大写如此复杂?的主要内容,如果未能解决你的问题,请参考以下文章
javascript 在JavaScript中将字符串的第一个字母设为大写