【Rust每周一知】Rust为什么会有String和&str?!长文预警!

本文是Amos博客文章“Working with strings in Rust”的翻译。

原文地址:https://fasterthanli.me/blog/2020/working-with-strings-in-rust/

人们选择Rust编程语言时总会遇到一个问题:为什么会有两种字符串类型?为什么会出现String和&str?

Amos在其另一篇文章"declarative-memory-management"中部分回答了这个问题。但是在本文中又进行了一些实验,看看是否可以为Rust的做法“辩护”。文章主要分为C和Rust两大部分。

C语言部分:

  • print程序示例

  • UTF-8编码

  • print程序处理UTF-8编码

  • 传递字符串

C语言的print程序示例

让我们从简单C程序开始,打印参数。

// in `print.c`

#include <stdio.h> // for printf

int main(int argc, char **argv) {
    for (int i = 0; i < argc; i++) {
        char *arg = argv[i];
        printf("%s\n", arg);
    }

    return 0;
}
$ gcc print.c -o print
$ ./print "ready" "set" "go"
./print
ready
set
go

好的!很简单。程序使用的是标准的C11主函数签名,该签名用int定义参数个数(argc,参数计数),和用char**char *[]“字符串数组”定义参数(argv,参数向量)。然后,使用printf格式说明符%s将每个参数打印为字符串,其后跟\n换行符。确实,它将每个参数打印在自己的行上。

在继续之前,请确保我们对正在发生的事情有正确的了解。修改以上的程序,使用%p格式说明符打印指针!

// in `print.c`

int main(int argc, char **argv) {
    printf("argv = %p\n", argv); // new!
    for (int i = 0; i < argc; i++) {
        char *arg = argv[i];
        printf("argv[%d] = %p\n", i, argv[i]); // new!
        printf("%s\n", arg);
    }

    return 0;
}
$ gcc print.c -o print
$ ./print "ready" "set" "go"
argv = 0x7ffcc35d84a8
argv[0] = 0x7ffcc35d9039
./print
argv[1] = 0x7ffcc35d9041
ready
argv[2] = 0x7ffcc35d9047
set
argv[3] = 0x7ffcc35d904b
go

好的,argv是一个地址数组,在这些地址上有字符串数据。像这样:

printf%s格式符怎么知道什么时候停止打印?因为它只获得一个地址,而不是起始地址和结束地址,或者起始地址和长度。让我们尝试自己打印每个参数:

// in `print.c`

#include <stdio.h> // printf

int main(int argc, char **argv) {
    for (int i = 0; i < argc; i++) {
        char *arg = argv[i];
        // we don't know where to stop, so let's just print 15 characters.
        for (int j = 0; j < 15; j++) {
            char character = arg[j];
            // the %c specifier is for characters
            printf("%c", character);
        }
        printf("\n");
    }

    return 0;
}
$ gcc print.c -o print
$ ./print "ready" "set" "go"
./printreadys
readysetgoCD
setgoCDPATH=.
goCDPATH=.:/ho

哦哦~我们的命令行参数相互“渗入”。让我们尝试将我们的程序通过管道xxd传输到一个十六进制的转储程序中,以查看发生了什么事:

$ # note: "-g 1" means "show groups of one byte",
$ # xxd defaults to "-g 2".
$ ./print "ready" "set" "go" | xxd -g 1
00000000: 2e 2f 70 72 69 6e 74 00 72 65 61 64 79 00 73 0a  ./print.ready.s.
00000010: 72 65 61 64 79 00 73 65 74 00 67 6f 00 43 44 0a  ready.set.go.CD.
00000020: 73 65 74 00 67 6f 00 43 44 50 41 54 48 3d 2e 0a  set.go.CDPATH=..
00000030: 67 6f 00 43 44 50 41 54 48 3d 2e 3a 2f 68 6f 0a  go.CDPATH=.:/ho.

啊啊!它们确实彼此跟随,但是两者之间有一些区别:这是相同的输出,用^^进行注释的位置是分隔符:

00000000: 2e 2f 70 72 69 6e 74 00 72 65 61 64 79 00 73 0a  ./print.ready.s.
          .  /  p  r  i  n  t  ^^ r  e  a  d  y  ^^

似乎每个参数都由值0来终止。确实,C具有以null终止的字符串。因此,我们可以“修复”我们的打印程序:

#include <stdio.h> // printf

int main(int argc, char **argv) {
    for (int i = 0; i < argc; i++) {
        char *arg = argv[i];
        // note: the loop condition is gone, we just loop forever.
        // well, until a 'break' at least.
        for (int j = 0;; j++) {
            char character = arg[j];

            // technically, we ought to use '\0' rather than just 0,
            // but even `gcc -Wall -Wextra -Wpedantic` doesn't chastise
            // us, so let's just go with it.
            if (character == 0) {
                break;
            }
            printf("%c", character);
        }
        printf("\n");
    }

    return 0;
}
$ gcc print.c -o print
$ ./print "ready" "set" "go"
./print
ready
set
go

一切都更好!虽然,我们也需要修复图:

提示:可能已经注意到,当我们的打印程序超出参数范围时,CDPATH=.:/ho也会显示出来。那是(一部分)环境变量!这些都在GNU C库glibc中程序参数旁边。但是具体细节不在本文讨论范围之内,需要查看制作自己的可执行打包程序系列。

好的!现在我们完全了解发生了什么,让我们做一些更有趣的事情:将参数转换为大写。因此,如果我们运行./print hello,它应该打印HELLO。我们也将跳过第一个参数,因为它是程序的名称,现在对我们而言这并不是很有趣。

#include <stdio.h> // printf
#include <ctype.h> // toupper

int main(int argc, char **argv) {
    // start from 1, skips program name
    for (int i = 1; i < argc; i++) {
        char *arg = argv[i];
        for (int j = 0;; j++) {
            char character = arg[j];
            if (character == 0) {
                break;
            }
            printf("%c", toupper(character));
        }
        printf("\n");
    }

    return 0;
}
$ gcc print.c -o print
$ ./print "hello"
HELLO

好的!太好了!在我看来功能齐全,可以发货了。出于谨慎考虑,让我们运行最后一个测试:

$ gcc print.c -o print
$ ./print "élément"
éLéMENT

哦~我们真正想要的是“ÉLÉMENT”,但显然,我们还没有弄清正在发生的一切。好的,也许现在大写字母太复杂了,让我们做些简单的事情:打印每个字符并用空格隔开。

// in `print.c`

#include <stdio.h> // printf

int main(int argc, char **argv) {
    for (int i = 1; i < argc; i++) {
        char *arg = argv[i];
        for (int j = 0;; j++) {
            char character = arg[j];
            if (character == 0) {
                break;
            }
            // notice the space following `%c`
            printf("%c ", character);
        }
        printf("\n");
    }

    return 0;
}
$ gcc print.c -o print
$ ./print "élément"
  l   m e n t

不好了。这不会做,根本不会做。让我们回到最后一个行为良好的版本,该版本仅打印每个字符,中间没有空格,并查看输出的实际内容。

// in main
// in for
// in second for
            printf("%c", character); // notice the lack of space after `%c`
$ gcc print.c -o print
$ ./print "élément" | xxd -g 1
00000000: c3 a9 6c c3 a9 6d 65 6e 74 0a                    ..l..ment.
          ^^^^^    ^^^^^

如果正确阅读此信息,则“é”不是一个char,实际上是2个char。好像...很奇怪。

让我们快速编写一个JavaScript程序,并使用Node.js运行它:

// in `print.js`

const { argv, stdout } = process;

// we have to skip *two* arguments: the path to node,
// and the path to our script
for (const arg of argv.slice(2)) {
    for (const character of arg) {
        stdout.write(character);
        stdout.write(" ");
    }
    stdout.write("\n");
}
$ node print.js "élément"
é l é m e n t

啊! 好多了!Node.js能正确转换为大写吗?

// in `print.js`

const { argv, stdout } = process;

for (const arg of argv.slice(2)) {
    stdout.write(arg.toUpperCase());
    stdout.write("\n");
}
$ node print.js "élément"
ÉLÉMENT

它可以。让我们看一下十六进制转储:

$ node print.js "élément" | xxd -g 1
00000000: c3 89 4c c3 89 4d 45 4e 54 0a                    ..L..MENT.
          ^^^^^    ^^^^^

虽然Node.js程序行为与预期相同,但我们可以看到,É也与其他字母不同,“c3 a9”的大写字母对应为“c3 89”。

C程序没有正常工作,因为它将“c3”和“a9”独立对待,它应将其看作一个单一的“Unicode值”。为什么将“é”编码为“c3 a9”?现在是时候进行快速的UTF-8编码入门了。

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值