为什么在Rust中将大写的字符串首字母大写?


81

我想将的第一个字母大写&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这样设计?

类似问题


43
这是一个简单的问题-不,不是。ß解释为德语时请大写。提示:不是一个字符。甚至问题陈述也可能很复杂。例如,大写姓氏的第一个字符将是不合适的von Hagen。这是生活在一个全球化的世界中的一个方面,这个世界拥有数千年的不同文化,并且有着不同的实践,我们正在尝试将所有这些压缩为8位2行代码。
Shepmaster

3
您摆出的姿势似乎是字符编码问题,而不是数据类型问题。我认为char :: to_uppercase已经正确处理了Unicode。我的问题是,为什么需要所有数据类型转换?索引似乎可以返回一个多字节的Unicode字符(而不是一个单字节字符,只能假定为ascii),并且to_uppercase可以返回所用语言的大写字符(如果该语言可用)。
marshallm

3
@marshallmchar::to_uppercase确实解决了此问题,但是您仅通过采用第一个代码点(nth(0))而不是构成大写字母的所有代码点来放弃它的工作

正如Joel在Software:Unicode上指出的那样,字符编码不是一个简单的过程。
内森

@Shepmaster,通常您是正确的。这是一个简单的英语问题(实际上是编程语言和数据格式的标准库)。是的,在某些脚本中,“大写”甚至不是一个概念,而在其他脚本中则非常复杂。
保罗·德雷珀

Answers:


98

为什么这么复杂?

让我们逐行将其分解

let s1 = "foobar";

我们创建了一个以UTF-8编码的文字字符串。UTF-8使我们能够以非常紧凑的方式对Unicode的1,114,112个编码点进行编码,如果您来自世界某个地区,该地区键入的大部分字符都是ASCII(一种1963年创建的标准)中的字符。UTF-8是可变长度的编码,这意味着单个代码点可能需要1到4个字节。较短的编码保留给ASCII,但是许多汉字在UTF-8中占用3个字节

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

这创建了char参与者的载体。字符是直接映射到代码点的32位数字。如果我们从仅ASCII文本开始,那么我们的内存需求就增加了三倍。如果我们从星体平面中获得了一堆字符,那么也许我们没有使用太多了。

v[0] = v[0].to_uppercase().nth(0).unwrap();

这将获取第一个代码点,并请求将其转换为大写形式。不幸的是,对于那些长大说英语的人来说,并不总是简单地将“小字母”映射为“大字母”。旁注:我们称它们为大写和小写,因为在一天中,一个字母框位于另一字母框上方

当代码点没有相应的大写字母变体时,此代码将出现恐慌。我不确定这些是否确实存在。当代码点具有包含多个字符的大写变体(例如German)时,在语义上也会失败ß。请注意,在《真实世界》中,ß可能永远不会大写,这是我永远记得并寻找的唯一示例。截至2017年6月29日,事实上,德语拼写的官方规则已经这样更新了两个“ẞ”和“SS”是有效的市值

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

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

let s3 = &s2;

现在我们参考一下String

这是一个简单的问题

不幸的是,事实并非如此。也许我们应该努力将世界转变为世界语

我认为char::to_uppercase已经正确处理了Unicode。

是的,我当然希望如此。不幸的是,在所有情况下,Unicode都是不够的。由于胡恩您指出土耳其我,其中两个上(İ)和较低的情况下,()的版本有一个点。也就是说,没有一个适当的大写字母i;它也取决于源文本的语言环境

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

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

索引可能返回一个多字节Unicode字符

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

如果逐字节进行切片,则可以对字符串进行切片,但是如果不在字符边界上,则标准库将崩溃。

从来没有实现索引字符串以获取字符的原因之一是因为太多的人将字符串用作ASCII字符数组。索引字符串以设置字符永远不会有效-您必须能够将1-4个字节替换为也是1-4个字节的值,从而导致字符串的其余部分反弹很多。

to_uppercase 可能返回大写字符

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

解决方案

另请参见trentcl的答案,该答案仅大写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上搜索大写unicode,然后让比我聪明的人来处理它。

已改善

在谈到“比我聪明的人”时,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(),
    }
}

34
考虑了很多之后,我对这些设计选择有了更好的了解。标准库应选择最通用,性能最佳和安全的折衷方案。否则,它将迫使开发人员进行可能不适合其应用程序,体系结构或语言环境的折衷。否则可能导致歧义和误解。如果我喜欢其他折衷方案,则可以选择一个第三方库或自己编写。
marshallm

13
@marshallm真是太好了!我担心很多Rust的新手都会误解Rust设计师所做的决定,而只是因为过于复杂而没有收益而将其注销。通过在这里提问和回答问题,我对需要进行此类设计并希望成为一名更好的程序员的关心表示赞赏。保持开放的胸怀并乐于学习更多是程序员的一大特点。
Shepmaster

6
“土耳其i”的是语言环境的依赖性是更直接相关比排序该特定问题的一个例子。

6
我很惊讶他们有to_uppercase和to_lowercase但没有to_titlecase。IIRC,某些Unicode字符实际上具有特殊的标题大小写变体。
蒂姆(Tim)

6
顺便说一下,即使是单个代码点也可能不是转换的正确单位。如果第一个字符是一个字形簇,那么在使用大写字母时应该进行特殊处理怎么办?(碰巧,如果仅将基本字符大写,分解的变音符号就可以工作,但是我不知道这是否普遍适用。)
塞巴斯蒂安·雷德尔

21

有比这更简单的方法吗?如果不是,为什么Rust这样设计?

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

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

extern crate inflector;
use inflector::Inflector;

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

4
用户的问题听起来更像他想要的.to_sentence_case()
Christopher Oezbek,

1
不幸的是,这对命名没有帮助...这是一个很棒的库,我从没见过,但是(对我来说)它的名字很难记住,并且它的功能与实际的变形几乎没有关系,其中之一是你的榜样。
Sahsahae

11

如果能够将输入限制为仅ASCII字符串,则不会特别麻烦。

由于Rust 1.23str具有make_ascii_uppercase方法(在较旧的Rust版本中,可以通过AsciiExttrait获得)。这意味着您可以相对轻松地将仅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"。(游乐场

请谨慎使用。


2
帮助Rust新手,为什么r可变?我看到s是可变的str。哦,好吧:我有我自己的问题的答案:(get_mut在这里带一个范围)显式返回Option<&mut>
史蒂文·卢

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
🦀
ß

བོད་སྐད་ལ 

-1

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

-1

我这样做是这样的:

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>())
}
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.