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

Why is capitalizing the first letter of a string so convoluted in Rust?

我想将 &str 的第一个字母大写。这是一个简单的问题,我希望有一个简单的解决方案。直觉告诉我应该这样做:

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

但是 &strs 不能像这样被索引。我能够做到的唯一方法似乎过于复杂。我将 &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 是这样设计的?

为什么这么复杂?

让我们逐行分解它

let s1 = "foobar";

我们创建了一个编码为 UTF-8. UTF-8 allows us to encode the 1,114,112 code points of Unicode in a manner that's pretty compact if you come from a region of the world that types in mostly characters found in ASCII, a standard created in 1963. UTF-8 is a variable length encoding, which means that a single code point might take from 1 to 4 bytes. The shorter encodings are reserved for ASCII, but many Kanji take 3 bytes in UTF-8 的文字字符串。

let mut v: Vec<char> = s1.chars().collect();

这将创建一个包含 char 个角色的向量。字符是直接映射到代码点的 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". Side note: we call them upper- and lower-case because one box of letters was above the other box of letters back in the day.

当代码点没有对应的大写变体时,此代码将出现错误。实际上,我不确定这些是否存在。当代码点具有包含多个字符的大写变体时,它也可能在语义上失败,例如德语 ß。请注意,在现实世界中 ß 可能永远不会大写,这是我永远记得和搜索的唯一例子。截至2017-06-29,事实上,德语拼写的官方规则已经更新,因此 both "ẞ" and "SS" are valid capitalizations!

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

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

let s3 = &s2;

现在我们参考那个 String

It's a simple problem

不幸的是,这不是真的。或许我们应该努力把世界变成Esperanto?

I presume char::to_uppercase already properly handles Unicode.

是的,我当然希望如此。不幸的是,Unicode 在所有情况下都不够。 也感谢源文本的 the Turkish I, where both the upper (İ) and lower case (i) versions have a dot. That is, there is no one proper capitalization of the letter i; it depends on the locale

why the need for all data type conversions?

因为当您担心正确性和性能时,您使用的数据类型很重要。 char 是 32 位,字符串是 UTF-8 编码的。它们是不同的东西。

indexing could return a multi-byte, Unicode character

这里可能有一些不匹配的术语。 char 多字节 Unicode 字符。

切片一个字符串如果你逐个字节是可能的,但如果你不在字符边界上标准库会崩溃。

从未实施索引字符串以获取字符的原因之一是因为太多人误将字符串用作 ASCII 字符数组。将字符串索引为 set 一个字符永远不会有效 - 您必须能够用同样为 1-4 字节的值替换 1-4 字节,导致其余弦弹跳很多。

to_uppercase could return an upper case character

如上所述,ß 是单个字符,当大写时,变成 两个字符

解决方案

另请参阅 ,它仅大写 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 or unicode,然后让比我聪明的人来处理。

改进

谈到 "someone smarter than me", 在访问第一个大写代码点后将迭代器转换回切片可能更有效。这允许其余字节的 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(),
    }
}

Is there an easier way than this, and if so, what? If not, why is Rust designed this way?

好吧,是的,也不是。正如另一个答案指出的那样,您的代码不正确,如果您给它类似 བོད་སྐད་ལ་ 之类的内容,您的代码将会恐慌。因此,使用 Rust 的标准库执行此操作比您最初想象的还要难。

但是,Rust 旨在鼓励代码重用并使引入库变得容易。因此,将字符串大写的惯用方式实际上非常可口:

extern crate inflector;
use inflector::Inflector;

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

如果您能够将输入限制为纯 ASCII 字符串,则不会特别复杂。

从 Rust 1.23 开始,str 有一个 make_ascii_uppercase 方法(在旧的 Rust 版本中,它可以通过 AsciiExt 特性获得)。这意味着您可以相对轻松地大写 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)

谨慎使用。

这是一个比@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()
}

我是这样做的:

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

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

ß

བོད་སྐད་ལ 

OP 的方法更进一步:
用大写形式替换第一个字符

let mut s = "foobar".to_string();
let r = s.remove(0).to_uppercase().to_string() + &s;
println!("{}", r);

也适用于 Unicode 字符,例如。 "foobar"

受到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
}

由于方法 to_uppercase() returns 是一个新字符串,您应该能够像这样添加字符串的其余部分。

这已在 Rust 版本 1.57+ 中测试,但可能适用于任何支持 slice 的版本。

fn uppercase_first_letter(s: &str) -> String {
        s[0..1].to_uppercase() + &s[1..]
    }