1个不稳定版本

使用旧的Rust 2015

0.1.0 2018年6月6日

#2812Rust模式

Unlicense

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)]

没有运行时依赖