有没有办法在 Rust 中模拟字符串文字类型?

Chr*_*cki 5 rust

有关字符串文字类型的示例,请参阅此 TypeScript 文档:https://www.typescriptlang.org/docs/handbook/2/everyday-types.html#literal-types

对于示例用例,我希望能够创建一个用于对表格数据执行操作的库,其中表的列被命名并具有异构类型。

假设我有一张这样的表:

| name    | age | quiz1 | quiz2 | midterm | quiz3 | quiz4 | final |
| ------- | --- | ----- | ----- | ------- | ----- | ----- | ----- |
| "Bob"   | 12  | 8     | 9     | 77      | 7     | 9     | 87    |
| "Alice" | 17  | 6     | 8     | 88      | 8     | 7     | 85    |
| "Eve"   | 13  | 7     | 9     | 84      | 8     | 8     | 77    |
Run Code Online (Sandbox Code Playgroud)

我希望能够获得这样的编译时间保证:

| name    | age | quiz1 | quiz2 | midterm | quiz3 | quiz4 | final |
| ------- | --- | ----- | ----- | ------- | ----- | ----- | ----- |
| "Bob"   | 12  | 8     | 9     | 77      | 7     | 9     | 87    |
| "Alice" | 17  | 6     | 8     | 88      | 8     | 7     | 85    |
| "Eve"   | 13  | 7     | 9     | 84      | 8     | 8     | 77    |
Run Code Online (Sandbox Code Playgroud)

我知道我可以使用类似的东西来表示异构类型的通用表,Table<(String, usize, usize, ...)>但不清楚如何嵌入列名称,同时还允许类型能够在以下操作的情况下动态更改:

let result1: Vec<String> = table.get_column("name"); // ["Bob", "Alice", "Eve"]
let result2: Vec<usize> = table.get_column("age"); // [12, 17, 13]
Run Code Online (Sandbox Code Playgroud)

Rust 有没有办法做到这一点?

我的一个想法是使用宏。宏是否能够产生类型错误,因此也许......

table.add_column("quiz5", column_values); // returns a new Table that is typed with one more column
Run Code Online (Sandbox Code Playgroud)

...根据传递的字符串文字输入为Vec<String>or (或作为错误)?Vec<usize>

kmd*_*eko 2

您可以实现您正在寻找的保证,但不能通过字符串文字。

要在编译时检查列类型是否与您要求的列匹配,您需要创建具有关联类型(数据类型)的标记类型(列名称)。像这样的事情是可行的:

use std::any::Any;
use std::collections::HashMap;

trait Column {
    type Data: 'static;
    const NAME: &'static str;
}

struct Name;
impl Column for Name {
    type Data = String;
    const NAME: &'static str = "name";
}

struct Age;
impl Column for Age {
    type Data = usize;
    const NAME: &'static str = "age";
}

struct Table {
    data: HashMap<&'static str, Box<dyn Any>>,
}

impl Table {
    fn new() -> Table {
        Table {
            data: HashMap::new()
        }
    }

    fn set_column<C: Column>(&mut self, data: Vec<C::Data>) {
        self.data.insert(C::NAME, Box::new(data));
    }

    fn get_column<C: Column>(&self) -> &Vec<C::Data> {
        self.data
            .get(C::NAME)
            .and_then(|data| data.downcast_ref::<Vec<C::Data>>())
            .expect("table does not have that column")
    }
}

fn main() {
    let mut table = Table::new();
    
    table.set_column::<Name>(vec!["Bob".to_owned(), "Alice".to_owned()]);
    table.set_column::<Age>(vec![12, 17]);
    
    dbg!(table.get_column::<Name>());
    dbg!(table.get_column::<Age>());
}
Run Code Online (Sandbox Code Playgroud)
use std::any::Any;
use std::collections::HashMap;

trait Column {
    type Data: 'static;
    const NAME: &'static str;
}

struct Name;
impl Column for Name {
    type Data = String;
    const NAME: &'static str = "name";
}

struct Age;
impl Column for Age {
    type Data = usize;
    const NAME: &'static str = "age";
}

struct Table {
    data: HashMap<&'static str, Box<dyn Any>>,
}

impl Table {
    fn new() -> Table {
        Table {
            data: HashMap::new()
        }
    }

    fn set_column<C: Column>(&mut self, data: Vec<C::Data>) {
        self.data.insert(C::NAME, Box::new(data));
    }

    fn get_column<C: Column>(&self) -> &Vec<C::Data> {
        self.data
            .get(C::NAME)
            .and_then(|data| data.downcast_ref::<Vec<C::Data>>())
            .expect("table does not have that column")
    }
}

fn main() {
    let mut table = Table::new();
    
    table.set_column::<Name>(vec!["Bob".to_owned(), "Alice".to_owned()]);
    table.set_column::<Age>(vec![12, 17]);
    
    dbg!(table.get_column::<Name>());
    dbg!(table.get_column::<Age>());
}
Run Code Online (Sandbox Code Playgroud)

此实现的一个缺陷是它不能保证在编译时Table实际包含您正在查找的列。为此,您需要将列类型编码为表类型,就像您建议的那样:Table<(Name, Age, ...)>。它还需要允许编译时查找(是否(Name, Age, ...)包含Age?)和扩展类型的能力((Name,)+ Age=> (Name, Age))。这是您必须处理的令人畏惧的模板杂耍,但有一些板条箱可以提供这种功能。

这是一个使用的工作示例lhlist(不一定提倡它,它只是我发现的一个箱子,可以很好地用于演示目的)。它具有与我们上面类似的 API,不仅具有我们需要的表达能力,而且还允许我们将数据与各个列类型相关联:

#[macro_use]
extern crate lhlist;

use lhlist::{Label, LVCons, LookupElemByLabel, LabeledValue, Value, Nil};

new_label!(Name: Vec<String>);
new_label!(Age: Vec<usize>);
new_label!(Grade: Vec<usize>);

struct Table<Columns> {
    columns: Columns
}

impl Table<Nil> {
    fn new() -> Table<Nil> {
        Table { columns: Nil::default() }
    }
}

impl<Columns> Table<Columns> {
    fn add_column<C>(self, data: C::AssocType) -> Table<LVCons<C, Columns>> 
    where 
        C: Label + 'static
    {
        Table { columns: lhlist::cons(lhlist::labeled_typearg::<C>(data), self.columns) }
    }

    fn get_column<C>(&self) -> &C::AssocType
    where
        C: Label + 'static,
        Columns: LookupElemByLabel<C, Elem = LabeledValue<C>>,
    {
        self.columns.elem().value_ref()
    }
}

fn main() {
    let table = Table::new();
    let table = table.add_column::<Name>(vec!["Bob".to_owned(), "Alice".to_owned()]);
    let table = table.add_column::<Age>(vec![12, 17]);

    dbg!(table.get_column::<Name>());
    dbg!(table.get_column::<Age>());
    // dbg!(table.get_column::<Grade>()); // compile-time error
}
Run Code Online (Sandbox Code Playgroud)
[src/main.rs:50] table.get_column::<Name>() = [
    "Bob",
    "Alice",
]
[src/main.rs:51] table.get_column::<Age>() = [
    12,
    17,
]
Run Code Online (Sandbox Code Playgroud)

这可能在某些方面变得更符合人体工程学,但我希望它能展示如何做到这一点。Rust 显然没有字符串文字类型(我认为没有任何东西可以与 Typescript 所具有的类型灵活性相匹配),但使用更传统的结构类型来实现您的目标并不太困难。