1个不稳定版本
使用旧的Rust 2015
0.1.0 | 2018年6月6日 |
---|
#2812 在 Rust模式
9KB
54 行
常量字符串拼接
Rust内置了一些很有用的宏,您可以使用。对于在编译时构建路径和其他文本特别有帮助的一个宏是concat!
。这个宏接受两个字符串并将它们拼接在一起
const HELLO_WORLD: &str = concat!("Hello", ", ", "world!");
assert_eq!(HELLO_WORLD, "Hello, world!");
这很方便,但它的速度很快就会下降。您可以使用concat!
在从env!
和include_str!
等魔法宏返回的字符串上,但是不能在常量上使用它
const GREETING: &str = "Hello";
const PLACE: &str = "world";
const HELLO_WORLD: &str = concat!(GREETING, ", ", PLACE, "!");
这将产生错误
error: expected a literal
--> src/main.rs:3:35
|
3 | const HELLO_WORLD: &str = concat!(GREETING, ", ", PLACE, "!");
| ^^^^^^^^
error: expected a literal
--> src/main.rs:3:51
|
3 | const HELLO_WORLD: &str = concat!(GREETING, ", ", PLACE, "!");
| ^^^^^
有了const_concat!
,您就可以做到!它的工作方式与concat!
宏一样
#[macro_use]
extern crate const_concat;
const GREETING: &str = "Hello";
const PLACE: &str = "world";
const HELLO_WORLD: &str = const_concat!(GREETING, ", ", PLACE, "!");
assert_eq!(HELLO_WORLD, "Hello, world!");
这一切都是通过完全不与编译器挂钩实现的。那么它是如何工作的呢?通过黑暗、邪恶的魔法。首先,为什么这不能像运行时字符串拼接一样工作?运行时字符串拼接会分配一个新的String
,但在编译时不可能进行分配——我们必须在栈上做所有事情。此外,我们无法在编译时进行迭代,因此无法将字符从源字符串复制到目标字符串。让我们看看实现。这个宏的“主力”是concat
函数
pub const unsafe fn concat<First, Second, Out>(a: &[u8], b: &[u8]) -> Out
where
First: Copy,
Second: Copy,
Out: Copy,
{
#[repr(C)]
#[derive(Copy, Clone)]
struct Both<A, B>(A, B);
let arr: Both<First, Second> =
Both(*transmute::<_, &First>(a), *transmute::<_, &Second>(b));
transmute(arr)
}
我们做的是将输入的数组(任意大小)转换为固定大小的数组的指针,然后解引用它们。这非常不安全——没有任何说明表明 a.len()
与 First
类型参数的长度相同。我们将它们相邻放在一个 #[repr(C)]
元组结构体中——这实际上在内存中将它们连接在一起。最后,我们将其转换为 Out
类型参数。如果 First
是 [u8; N0]
且 Second
是 [u8; N1]
,那么 Out
应该是 [u8; N0 + N1]
。为什么不直接使用一个具有关联常量的特质呢?嗯,这里有一个这样的例子
trait ConcatHack {
const A_LEN: usize;
const B_LEN: usize;
}
pub const unsafe fn concat<C>(
a: &[u8],
b: &[u8],
) -> [u8; C::A_LEN + C::B_LEN]
where
C: ConcatHack,
{
#[repr(C)]
#[derive(Copy, Clone)]
struct Both<A, B>(A, B);
let arr: Both<First, Second> =
Both(*transmute::<_, &[u8; C::A_LEN]>(a), *transmute::<_, &[u8; C::B_LEN]>(b));
transmute(arr)
}
但这不起作用,因为 在计算固定大小数组长度时,不尊重类型参数。所以,我们为每个固定大小数组使用单个类型参数。
等等,如果你查看 撰写时 std::mem::tranmute
的文档,它不是一个 const fn
。那么这里发生了什么?嗯,我写了自己的 transmute
#[allow(unions_with_drop_fields)]
pub const unsafe fn transmute<From, To>(from: From) -> To {
union Transmute<From, To> {
from: From,
to: To,
}
Transmute { from }.to
}
在 const fn
中这被允许,其中 std::mem::transmute
不允许。最后,让我们看看宏本身
#[macro_export]
macro_rules! const_concat {
($a:expr, $b:expr) => {{
let bytes: &'static [u8] = unsafe {
&$crate::concat::<
[u8; $a.len()],
[u8; $b.len()],
[u8; $a.len() + $b.len()],
>($a.as_bytes(), $b.as_bytes())
};
unsafe { $crate::transmute::<_, &'static str>(bytes) }
}};
($a:expr, $($rest:expr),*) => {{
const TAIL: &str = const_concat!($($rest),*);
const_concat!($a, TAIL)
}};
}
首先我们创建一个 &'static [u8]
,然后将其转换为 &'static str
。这目前有效,因为 &[u8]
和 &str
有相同的布局,但这并不保证永远有效。将赋值右侧的值转换为 &'static [u8]
有效,即使赋值的右侧在这个作用域内是局部的,这是因为某种称为 "rvalue static promotion" 的东西。
这目前不能在特质关联常量中工作。虽然我有一种支持特质关联常量的方法,但是不幸的是,你不能在数组长度中访问类型参数,所以这不起作用。最后,它需要相当多的夜间功能
#![feature(const_fn, const_str_as_bytes, const_str_len, const_let, untagged_unions)]