在Rust中,为什么将字符串的第一个字母大写变得如此复杂?

133

我想要将&str的首字母大写。这是一个简单的问题,我希望能得到一个简单的解决方案。我的直觉告诉我要像这样做:

let mut s = "foobar";
s[0] = s[0].to_uppercase();

但是&str不能像这样进行索引。我一直以来所能做的唯一方法似乎过于复杂。我将&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要设计成这样?

类似的问题


88
“这是一个简单的问题” — 不,它并不简单。请在德语中将 ß 大写。提示:它不是一个单独的字符。即使问题陈述本身也可能很复杂。例如,将姓氏 von Hagen 的第一个字母大写是不正确的。这都是生活在一个拥有数千年分歧文化和不同惯例的全球世界的方面,我们试图将所有这些压缩到8个位和2行代码中。 - Shepmaster
3
你提出的似乎是字符编码问题,而不是数据类型问题。我认为 char::to_uppercase 应该已经正确处理了 Unicode。我的问题是,为什么需要进行所有数据类型转换?因为索引可能返回一个多字节的 Unicode 字符(而不是仅限于 ASCII 的单字节字符),而 to_uppercase 可能会返回该语言中可用的大写字符。 - marshallm
4
char::to_uppercase确实可以解决这个问题,但是你只获取了第一个码点(使用nth(0)),而没有获取所有组成大写字母的码点。 - user395760
2
字符编码并非像Joel on Software: Unicode所指出的那样一件简单的过程。 - Nathan
@Shepmaster,总的来说你是正确的。这是一个简单的问题,因为英语是编程语言和数据格式的事实标准基础。是的,有一些脚本根本不考虑“大写”,而其他一些则非常复杂。 - Paul Draper
11个回答

177

为什么这么复杂?

让我们逐行分解它

let s1 = "foobar";

我们创建了一个以 UTF-8 编码的文本字符串。 UTF-8 允许我们以相对紧凑的方式编码 Unicode 的 1,114,112 代码点,如果你来自一个主要使用 ASCII 字符的地区,则这种方式非常适合你。UTF-8 是一种可变长度编码,这意味着单个代码点可能需要 1 到 4 个字节。较短的编码保留给 ASCII,但是许多汉字在 UTF-8 中占用 3 个字节
let mut v: Vec<char> = s1.chars().collect();

这将创建一个字符向量。字符是一个32位数字,直接映射到代码点。如果我们从ASCII纯文本开始,我们的内存需求将增加四倍。如果我们有许多来自天界的字符,那么可能我们并没有使用太多额外的内存。
v[0] = v[0].to_uppercase().nth(0).unwrap();

这会获取第一个代码点并要求将其转换为大写变体。不幸的是,对于我们这些以英语为母语的人来说,并不总是存在“小写字母”到“大写字母”的简单一对一映射。顺便说一下:我们称它们为大写和小写因为在过去的日子里,一个字母盒在另一个字母盒上方链接1

当一个字符没有相应的大写变体时,此代码将会抛出错误。实际上我不确定这种情况是否存在。另外,当一个字符有多个大写变体时(比如德语中的ß),此代码也可能在语义上失败。请注意,在现实世界中,ß可能永远不会被大写,这只是我经常记得并搜索的例子。事实上,截至2017年6月29日,德语拼写的官方规则已更新,因此两种大写形式"ẞ"和"SS"都是有效的

let s2: String = v.into_iter().collect();

在这里,我们将字符转换回UTF-8,并需要新的分配来存储它们,因为原始变量存储在常量内存中,以便在运行时不占用内存。

let s3 = &s2;

现在我们引用那个String

这是一个简单的问题

不幸的是,这并不是真的。也许我们应该努力将世界转化为Esperanto

我假设char::to_uppercase已经正确处理Unicode了。

是的,我当然希望如此。不幸的是,在某些情况下,Unicode并不足够。感谢huon for pointing out指出了土耳其语I,其中大写(İ)和小写(i)版本都有一个点。也就是说,字母i的大小写取决于源文本的locale

为什么需要所有数据类型转换?

因为在关注正确性和性能时,您正在使用的数据类型非常重要。一个char是32位,而字符串是UTF-8编码的。它们是不同的东西。

索引可能返回多字节的Unicode字符

可能存在一些术语不匹配的情况。一个char 一个多字节的Unicode字符。
如果逐字节进行,则可以对字符串进行切片,但如果不在字符边界上,则标准库将会出现故障。
没有实现通过索引字符串以获取字符的原因之一是因为很多人误用字符串作为ASCII字符数组。索引字符串以设置字符永远不可能是高效的——你必须能够用一个也是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 上搜索 uppercaseunicode,让比我更聪明的人来处理它。

改进

说到“比我更聪明的人”,Veedrac 指出,在访问第一个大写字符代码点后,将迭代器转换回切片可能更有效率。这样可以使用 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(),
    }
}

51
经过深思熟虑,我现在更好地理解了这些设计选择。标准库应该选择最具通用性、性能和安全性的权衡方案。否则,它会迫使开发者做出对于他们的应用程序、架构或本地化可能不合适的权衡,甚至会导致歧义和误解。如果我更喜欢其他的权衡方案,我可以选择第三方库或自己编写代码。 - marshallm
20
@marshallm,听到这个消息真是太好了!我担心很多刚接触 Rust 的人会误解 Rust 设计者做出的决策,并简单地认为它们过于复杂而没有任何好处。通过在这里提问和回答问题,我开始欣赏需要投入到这些设计中的关注,并希望成为一名更好的程序员。保持开放的心态并愿意学习更多是作为程序员拥有的伟大特质。 - Shepmaster
7
"Turkish i"是一个与本问题直接相关的区域依赖性的例子,比排序更为直接。 - huon
6
他们有 to_uppercase 和 to_lowercase,但没有 to_titlecase,这让我感到惊讶。据我所知,一些 Unicode 字符实际上有特殊的 titlecase 变体。 - Tim
6
顺便说一下,甚至一个代码点可能不是正确的转换单位。如果第一个字符是一个应该在大写时接受特殊处理的音素簇,怎么办?(碰巧的是,分解的变音符号可以通过仅将基字符大写来实现,但我不知道这是否普遍适用。) Translated: 顺便说一下,一个代码点可能不是正确的转换单位。如果第一个字符是一个音素簇,在大写时应该接受特殊处理,怎么办?(分解的变音符号可以通过仅将基字符大写来实现,但我不知道这是否普遍适用。) - Sebastian Redl
显示剩余8条评论

27
有没有比这更简单的方法?如果有,是什么?如果没有,为什么 Rust 要设计成这样?
嗯,是和不是。正如其他答案所指出的那样,你的代码是不正确的,如果你给它一些像 བོད་སྐད་ལ་ 这样的东西,它会出现错误。因此,使用 Rust 的标准库来完成这个任务比你最初想象的要困难得多。
然而,Rust 的设计旨在鼓励代码重用并使引入库变得容易。因此,将字符串大写的惯用方式实际上相当可行:
extern crate inflector;
use inflector::Inflector;

let capitalized = "some string".to_title_case();

5
用户的问题似乎更像是希望使用 .to_sentence_case() - Christopher Oezbek
1
遗憾的是它不能帮助命名...这是一个很棒的库,我以前从未见过,但它的名称很难(对我来说)记住,并且它的函数与实际的屈折几乎没有任何关系,其中之一就是你的例子。 - user11877195
如果你在<current year>寻找一个可以进行大小写转换等操作的crate,那么你可能需要heck。但是它没有to_sentence_case(),只有to_title_case() - BallpointBen
请注意,heckinflector都会丢弃标点符号和其他在标识符名称中无效的字符。 - BallpointBen

19

如果您能将输入限制为仅使用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

请谨慎使用。


3
能帮助一个Rust新手吗,为什么 r 是可变的?我看到 s 是可变的 str。哦,好的:我已经找到了自己问题的答案:get_mut(在这里使用范围调用)明确地返回 Option<&mut> - Steven Lu

8

进一步采取OP的方法:
将第一个字符替换为其大写表示

let mut s = "foobar".to_string();
let r = s.remove(0).to_uppercase().to_string() + &s;

或者

let r = format!("{}{s}", s.remove(0).to_uppercase());
println!("{r}");

它同样可以使用Unicode字符,例如"foobar"


第一个字符保证是ASCII字符,在原地更改为大写字母:

let mut s = "foobar".to_string();
if !s.is_empty() {
    s[0..1].make_ascii_uppercase();  // Foobar
}

以非ASCII字符为首位时会引发恐慌!

6

我是这样做的:

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>())
}

3
这是一个比@Shepmaster改进版本慢一点但更加地道的版本。
fn capitalize_first(s: &str) -> String {
    s.chars()
        .take(1)
        .flat_map(|f| f.to_uppercase())
        .chain(s.chars().skip(1))
        .collect()
}

1

ICU4X 现在以标题大小写的方式处理很多国际化的边缘情况。从文档中复制而来:

use icu_casemap::TitlecaseMapper;
use icu_locid::langid;

let cm = TitlecaseMapper::new();
let root = langid!("und");

let default_options = Default::default();

// note that the subsequent words are not titlecased, this function assumes
// that the entire string is a single segment and only titlecases at the beginning.
assert_eq!(cm.titlecase_segment_to_string("hEllO WorLd", &root, default_options), "Hello world");
assert_eq!(cm.titlecase_segment_to_string("Γειά σου Κόσμε", &root, default_options), "Γειά σου κόσμε");
assert_eq!(cm.titlecase_segment_to_string("नमस्ते दुनिया", &root, default_options), "नमस्ते दुनिया");
assert_eq!(cm.titlecase_segment_to_string("Привет мир", &root, default_options), "Привет мир");

// Some behavior is language-sensitive
assert_eq!(cm.titlecase_segment_to_string("istanbul", &root, default_options), "Istanbul");
assert_eq!(cm.titlecase_segment_to_string("istanbul", &langid!("tr"), default_options), "İstanbul"); // Turkish dotted i

assert_eq!(cm.titlecase_segment_to_string("և Երևանի", &root, default_options), "Եւ երևանի");
assert_eq!(cm.titlecase_segment_to_string("և Երևանի", &langid!("hy"), default_options), "Եվ երևանի"); // Eastern Armenian ech-yiwn ligature

assert_eq!(cm.titlecase_segment_to_string("ijkdijk", &root, default_options), "Ijkdijk");
assert_eq!(cm.titlecase_segment_to_string("ijkdijk", &langid!("nl"), default_options), "IJkdijk"); // Dutch IJ digraph

0
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
}

0

这是我解决这个问题的方法,注意在转换为大写之前,我必须检查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

ß

བོད་སྐད་ལ 

如果第一个字符有两个字母,则无法使用。 - Markus Laire
更准确地说,它根本不适用于任何Unicode字符的第一个位置,因为它们被绕过了。 - Kaplan

0
在您的项目中运行(或将添加到中)。然后在您的项目代码中使用以下内容:
use ucfirst::ucfirst;

let uppercased = ucfirst("apple banana"); // "Apple banana"

完全公开:这个解决方案是毫不掩饰地从Shepmaster领先回答中借鉴而来。

请查看crates.io和/或github上的ucfirst箱。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接