【问题标题】:Is there a way to define a "common" structure for multiple parameter numbers and types有没有办法为多个参数编号和类型定义一个“通用”结构
【发布时间】:2020-08-02 06:45:45
【问题描述】:

我想创建一个通用结构,可以用来将多种长度和类型的参数传递给不同的函数。

例如,考虑以下结构:

typedef struct _list_t {
    int ID;
    char *fmt;
    int nparams;
} list_t;
list_t infoList[100]; //this will be pre-populated with the operations my app offers 


typedef struct _common {
    int ID;
    char *params;
} common;

如果格式已经填充,则使用可变大小函数传入参数:

int Vfunc(common * c, ...) {
    va_list args;
    va_start(args, c);
    
    //code to search for ID in infoList and fetch its fmt
    char params_buff[100]; //max params is 100
    vsprintf(str_params, fmt, args);

    va_end(args);

    c->params = (char *)malloc(sizeof(char)*(strlen(params_buff)+1));
    strncpy(c->params, params_buff, strlen(params_buff)+1);
}
int execute(common * c) { 
    if (c->ID == 1) { //add 2 numbers
        int x, y; // i expect 2 numbers 
        //code to find ID in infoList and fetch its fmt
        sscanf(c->params, fmt, &x, &y);
    
        return (x + y);
    }
    else if (c->ID == 2) {
    //do another operation, i expect an unsigned char array?
    }
    
}

主程序看起来有点像这样:

int main()
{
    common c;
    c.ID = 1;

    Vfunc(&c, 12, 2);
    
    execute(&c);
    
    return 0;
}

现在我可以将结构传递给任何函数,它会适当地处理参数。但是,我看不到将 unsigned char[] 作为参数之一的方法,因为 unsigned char 数组没有“格式”。 char[] 的格式为 %s。基本上我想通过这个结构传入一些原始数据。

有没有办法做到这一点或更好的实现来实现目标?

编辑:

问题的目标似乎不明确。假设我的应用程序可以提供算术运算(如计算器)。假设我的应用程序的用户想要添加 2 个数字。我想让他们做的就是填写这个通用结构,然后将它传递给让我们说一个函数来执行它。所有操作的 ID 都可以从手册中得知,因此用户将知道他们可以传递多少个参数以及什么 ID 做什么。作为应用程序所有者,我将使用我提供的 ID 填写 infoList

所以这只是为了让您了解我所说的“通用结构”是什么意思。它也可以通过其他方式实现,也许你有更好的方法。但我的目标是让实现能够传入一个无符号字符数组。我可以这样做吗?

【问题讨论】:

  • 第一个问题可以查看variable argument list man page。
  • 在不了解更多信息的情况下很难回答,但我会考虑一种面向对象的方法。然后,您的通用事物将是结构,每个结构都包含一个子结构(或指针),指向将充当方法的函数。这样很容易获得通用行为(尽管您不会有继承或类似的东西)。然后,您的通用接口可以更加丰富,因为您可以为任何您想要的通用操作需要多种方法,而不必尝试将它们全部塞进一个函数中。
  • 您展示的示例只是使用 printf/scanf 作为后端(顺便说一句,使用 asprintf)从/到它的 ASCII 表示的数据序列化和反序列化的示例。因此,我认为您的问题过于广泛 - 为抽象数据编写序列化程序是一项非常艰巨的工作。而且,无论如何,您的myAddFunc 无论如何都必须知道数据的类型(并且错过了错误检查),因此您不妨传递一个二进制 memcpy'ied blob。使用现有的协议缓冲区。杰森。原型。平面缓冲区。等等。
  • @Sarahcartenz,我刚刚看到你的更新......我明白你在你的代码中做了什么,但我的问题是为什么不直接在myAddFunc() 中传递(12, 2),为什么还要打扰创建struct _common 并使用va_arg 保存这些数据?这给你带来什么好处?您正在检索(x, y),请注意,您必须知道no of elements 并且您还必须知道their types?它没有给你任何优势......没有多余的代码......
  • @reyad 是的,就是这样。

标签: c


【解决方案1】:

据我了解您的问题,您希望将所有参数值保存在文本字符串中,以便以后可以使用sscanf 重构这些值。此外,您希望能够处理数字数组,例如一个无符号字符数组。

你问:

有没有办法做到这一点

要使您的想法生效,sscanf 必须能够解析(也称为匹配)您要在程序中使用的数据类型。而且-正如您在问题中所写-scanf 无法解析数字数组。所以答案是:

不行,标准函数做不到。

因此,如果您希望能够处理数字数组,则必须编写自己的扫描函数。这包括

  1. 选择一个转换说明符来告诉代码扫描一个数组(例如 %b),
  2. 为数组选择文本格式(例如“{1, 2, 3}”)
  3. 一种存储数组数据大小的方法,例如struct {unsiged char* p; size_t nb_elements;}

此外,vsprintf 也会遇到同样的问题。同样,您需要编写自己的函数。

编辑

另一种选择(我自己不太喜欢)是存储指针值。也就是说 - 您可以存储指向数组的指针,而不是将数组值存储在字符串 params 中。

这种方法的好处是您可以使用标准函数。

缺点是调用者必须确保数组存在直到execute被调用。

换句话说:

unsigned char auc[] = {1, 2, 3};
Vfunc(&c, auc);
execute(&c);

会很好,但是

Vfunc(&c, (unsigned char[]){1, 2, 3});
execute(&c);

会编译但在运行时失败。

而且 - 与 C 中的数组一样 - 您可能需要一个额外的参数来表示数组元素的数量。

这种“另存为指针”方法的示例代码可以是:

#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <stdarg.h>

typedef struct _common {
    int ID;
    char *params;
} common;

void Vfunc(common * c, ...) {
    va_list args;
    va_start(args, c);

    //code to search for ID in infoList and fetch its fmt
    // For this example just use a fixed fmt
    char fmt[] ="%p %zu";


    char params_buff[100]; //max params is 100
    vsprintf(params_buff, fmt, args);

    va_end(args);

    c->params = (char *)malloc(sizeof(char)*(strlen(params_buff)+1));
    strncpy(c->params, params_buff, strlen(params_buff)+1);
}

int execute(common * c)
{
    if (c->ID == 1) {
      // expect pointer and number of array elements
      unsigned char* a;
      size_t nbe;

      //code to find ID in infoList and fetch its fmt
      // For this example just use a fixed fmt
      char fmt[] ="%p %zu";

      if (sscanf(c->params, fmt, &a, &nbe) != 2) exit(1);

      // Calculate average
      int sum = 0;
      for (size_t i = 0; i < nbe; ++i) sum += a[i];
      return sum;
    }

    return 0;
}

int main(void)
{
  common c;
  c.ID = 1;

  unsigned char auc[] = {1, 2, 3, 4, 5, 6};

  Vfunc(&c, auc, sizeof auc / sizeof auc[0]);

  printf("The saved params is \"%s\"\n", c.params);
  printf("Sum of array elements are %d\n", execute(&c));

  return 0;
}

可能的输出

The saved params is "0xffffcc0a 6"
Sum of array elements are 21

注意不是保存的数组数据,而是一个指针值。

【讨论】:

  • 关于 c.params = "12 {1, 2, 3, 4, 5} 2",如果用户想要插入来自他们进行的另一个函数调用的数组怎么办?这意味着他们需要在将参数放入 c.params 之前以某种方式附加参数?
  • @Sarahcartenz 啊,如果你想允许像Vfunc(&amp;c, some_func_returning_int(), some_func_returning_pointer_to_array_of_numbers()); 这样的东西,你不能使用建议的直接文本字符串。顺便说一句:请注意,函数不能 返回一个数字数组——它只能返回一个指向数组第一个元素的指针。而且你不知道大小......
  • @Sarahcartenz 你考虑过将数组解析为指针吗?
  • 您的意思是使用问题中的实现?那将需要我将大小添加为函数中结构或参数的成员,对吗?我不需要 char 指针,因为我可以将标准 strlen 用于字符串
  • @Sarahcartenz 在您当前的实现中,c.params 是一个包含所有数据的文本字符串。使用标准函数无法做到这一点,因为无法扫描数字数组。但是,您可以做的是将 pointers 存储到例如一个数字数组。我不太喜欢这种方法,但可以使用标准功能来完成。因此,如果它对您有用,那么它非常简单......几乎是您已经拥有的代码
【解决方案2】:

我再次阅读了这个问题,发现它比你描述的要简单得多。

根据您的说法,您已经知道execute()函数中数据检索的类型和顺序。这让这个问题变得更容易了。

我必须说,这个问题在c 中有点难以解决,因为c 无法在运行时解析类型或在运行时动态转换类型。 c 必须事先知道所有类型,即在编译时。

也就是说,c 提供了一种处理可变长度参数的方法。这是一个优势。

所以,我们要做的是:

  1. 缓存来自可变长度参数的所有参数,即 va_list。
  2. 并且,提供一种从该缓存中检索提供的参数的方法。

首先,如果您知道类型,我将向您展示如何从缓存中检索元素。我们将使用宏来完成。我将其命名为sarah_next()。好吧,毕竟,我要写它是因为你。你可以随意命名。其定义如下:

#define sarah_next(cache, type)                        \
        (((cache) = (cache) + sizeof(type)),           \
        *((type*) (char *) ((cache) - sizeof(type))))

所以,简单来说,sarah_next()cache 中检索next element 并将其转换为type

现在,让我们讨论第一个问题,我们必须缓存来自 va_list 的所有参数。您可以通过以下方式轻松完成:

void *cache = malloc(sizeof(char) * cacheSize);
// itr is an iterator, which iterates over cache
char *itr = (char *)cache;
// now, you can do
*(type *)itr = va_arg(buf, type);
// and then
itr += sizeof(type);

我想讨论的另一点是,我使用类型提示来确定缓存大小。为此,我使用了函数getSize()。如果您只看它就会明白(另请注意:这使您能够使用自己的自定义类型):

// getSize() is a function that returns type size based on type hint
size_t getSize(char type) {
    if(type == 's') {
        return sizeof(char *);
    }
    if(type == 'c') {
        return sizeof(char);
    }
    if(type == 'i') {
        return sizeof(int);
    }
    if(type == 'u') { // 'u' represents 'unsigned char'
        return sizeof(unsigned char);
    }
    if(type == 'x') { // let's, say 'x' represents 'unsigned char *'
        return sizeof(unsigned char *);
    }
    // you can add your own custom type here
    // also note: you can easily use 'unsigned char'
    //            use something like 'u' to represent 'unsigned char'
    //            and you're done
    // if type is not recognized, then
    printf("error: unknown type while trying to retrieve type size\n");
    exit(1);
}

好的,我想,这些想法已经完成了。在继续之前,请尝试正确掌握这些想法。

现在,让我提供完整的源代码:

#include <stdio.h>
#include <stdlib.h>
#include <stdarg.h>
#include <string.h>

// note: it is the most fundamental part of this solution
//       'sarah_next' is a macro, that
//       returns *(type *)buf means a value of type "type", and also
//       increments 'buf' by 'sizeof(type)', so that
//       it may target next element
//       'sarah_next' is used to retrieve data from task cache

// I've named it after you, you may choose to name it as you wish
#define sarah_next(cache, type)                        \
        (((cache) = (cache) + sizeof(type)),           \
        *((type*) (char *) ((cache) - sizeof(type))))


// defining pool size for task pool
#define POOL_SIZE 1024


// notice: getSize() has been updated to support unsigned char and unsigned char *
// getSize() is a function that returns type size based on type hint
size_t getSize(char type) {
    if(type == 's') {
        return sizeof(char *);
    }
    if(type == 'c') {
        return sizeof(char);
    }
    if(type == 'i') {
        return sizeof(int);
    }
    if(type == 'u') { // 'u' represents 'unsigned char'
        return sizeof(unsigned char);
    }
    if(type == 'x') { // let's, say 'x' represents 'unsigned char *'
        return sizeof(unsigned char *);
    }
    // you can add your own custom type here
    // also note: you can easily use 'unsigned char'
    //            use something like 'u' to represent 'unsigned char'
    //            and you're done
    // if type is not recognized, then
    printf("error: unknown type while trying to retrieve type size\n");
    exit(1);
}


typedef struct __task {
    int id;
    void *cache;
} Task;

// notice: constructTask has been updated to support unsigned char and unsigned char *
// note: here, types contains type hint
Task *constructTask(int id, char *types, ...) {
    // determine the size of task cache
    int cacheSize = 0;
    for(int i=0; types[i]; i++) {
        cacheSize += getSize(types[i]);
    }
    // allocate memory for task cache
    void *cache = malloc(sizeof(char) * cacheSize);
    
    va_list buf;
    va_start(buf, types);
    
    // itr is an iterator, which iterates over cache
    char *itr = (char *)cache;
    for(int i=0; types[i]; i++) {
        if(types[i] == 's') {
            *(char **)itr = va_arg(buf, char *);

        } else if(types[i] == 'x') { // added support for 'unsigned char *'
            *(unsigned char **)itr = va_arg(buf, unsigned char *);

        } else if(types[i] == 'c') {
            // notice: i used 'int' not 'char'
            // cause: compiler-warning: 'char' is promoted to 'int' when passed through '...'
            // also note: this promotion helps with 'unsigned char'
            *(char *)itr = (char)va_arg(buf, int); // so cast it to char

        } else if(types[i] == 'u') { // added support 'unsigned char'
            // notice: i used 'int' not 'unsigned char'
            // cause: compiler-warning: 'unsigned char' is promoted to 'int' when passed through '...'
            // also note: this promotion helps with 'unsigned char'
            *(unsigned char *)itr = (unsigned char)va_arg(buf, int); // so cast it to unsigned char

        } else if(types[i] == 'i') {
            *(int *)itr = va_arg(buf, int);

        }
        // it won't come to else, cause getSize() would
        // caught the type error first and exit the program
        itr += getSize(types[i]);
    }

    va_end(buf);

    // now, construct task
    Task *task = malloc(sizeof(Task));
    task->id = id;
    task->cache = cache;
    // and return it
    return task;
}

// destroyTask is a function that frees memory of task cache and task
void destroyTask(Task *task) {
    free(task->cache);
    free(task);
}

// notice: that 'task->id == 4' processing part
// it is equivalant to your 'execute()' function
int taskProcessor(Task *task) {
    // define ret i.e. return value
    int ret = 999; // by default it is some code value, that says error

    // note: you already know, what type is required in a task
    if(task->id == 1) {
        // note: see usage of 'sarah_next()'
        int x = sarah_next(task->cache, int);
        int y = sarah_next(task->cache, int);

        ret = x + y;

    } else if(task->id == 2) {
        char *name = sarah_next(task->cache, char *);
        if(strcmp(name, "sarah") == 0) {
            ret = 0; // first name
        } else if (strcmp(name, "cartenz") == 0) {
            ret = 1; // last name
        } else {
            ret = -1; // name not matched
        }
    } else if(task->id == 3) {
        int x = sarah_next(task->cache, int);
        char *name = sarah_next(task->cache, char *);
        int y = sarah_next(task->cache, int);

        printf("%d %s %d\n", x, name, y); // notice: we've been able to retrieve
        // both string(i.e. char *) and int
        // you can also see for ch and int, but i can assure you, it works

        ret = x + y;

    } else if(task->id == 4) { // working with 'unsigned char *'
        int a = sarah_next(task->cache, int);
        unsigned char *x = sarah_next(task->cache, unsigned char *); // cast to unsigned char *
        // char *x = sarah_next(task->cache, char *); // this won't work, would give wrong result
        int b = sarah_next(task->cache, int);

        printf("working with 'unsigned char *':");
        for(int i=0; x[i]; i++) {
            printf(" %d", x[i]); // checking if proper value is returned, that's why using 'integer'
        }
        printf("\n");

        ret = a + b;
    } else {
        printf("task id not recognized\n");
    }

    return ret;
}


int main() {
    Task *taskPool[POOL_SIZE];

    int taskCnt = 0;

    taskPool[taskCnt++] = constructTask(1, "ii", 20, 30); // it would return 50
    taskPool[taskCnt++] = constructTask(1, "ii", 50, 70); // it would return 120
    taskPool[taskCnt++] = constructTask(2, "s", "sarah"); // it would return 0
    taskPool[taskCnt++] = constructTask(2, "s", "cartenz"); // it would return 1
    taskPool[taskCnt++] = constructTask(2, "s", "reyad"); // it would return -1
    taskPool[taskCnt++] = constructTask(3, "isi", 40, "sarah", 60); // it would print [40 sarah 60] and return 100

    // notice: I've added an exmaple to showcase the use of unsigned char *
    // also notice: i'm using value greater than 127, cause
    // in most compiler(those treat char as signed) char supports only upto 127
    unsigned char x[] = {231, 245, 120, 255, 0}; // 0 is for passing 'NULL CHAR' at the end of string
    // 'x' is used to represent 'unsigned char *'
    taskPool[taskCnt++] = constructTask(4, "ixi", 33, x, 789); // it would print ['working with unsigned char *': 231 245 120 255] and return 822
    // note: if you used 'char *' cast to retrieve from 'cache'(using a compiler which treats char as signed), then
    //       it would print [-25 -11 120 -1] instead of [231 245 120 255]
    //       i guess, that makes it clear that you can perfectly use 'unsigned char *'

    for(int i=0; i<taskCnt; i++) {
        printf("task(%d): %d\n", i+1, taskProcessor(taskPool[i]));
        printf("\n");
    }

    // at last destroy all tasks
    for(int i=0; i<taskCnt; i++) {
        destroyTask(taskPool[i]);
    }

    return 0;
}

输出是:

// notice the updated output
task(1): 50                                        
                                                   
task(2): 120                                       
                                                   
task(3): 0                                         
                                                   
task(4): 1                                         
                                                   
task(5): -1                                        
                                                   
40 sarah 60                                        
task(6): 100                                       
                                                   
working with 'unsigned char *': 231 245 120 255    
task(7): 822

因此,您可能想知道,与您给定的解决方案相比,它可能会产生什么优势。好吧,首先您不必使用%s %d 等确定格式,这不容易为每个任务更改或创建,并且您可能已经写了很多(对于每个任务,您可能必须写不同的fmt),并且你没有使用 vsprintf 等......它只处理内置类型。

第二点也是很重要的一点是,您可以使用自己的custom type。声明一个您自己的struct type 并且您可以使用。而且addnew type也很方便。

更新:

我忘了提到另一个优点,你也可以使用 unsigned char 。看,更新的getSize() 函数。 unsigned char 可以使用 'u' 符号,因为 unsigned char 被提升为 int,你可以将其转换为 (unsigned char) 并完成...

update-2(支持unsigned char *):

我已更新代码以支持unsigned charunsigned char *。为了支持新类型,您需要更新的功能是getSize()constructTask()。比较之前的代码和新更新的代码……你就会明白如何添加新类型(你也可以添加自己的自定义类型)。

另外,看看task-&gt;id == 4 部分在taskProcessor() 函数中。我添加了这个来展示unsigned char * 的用法。希望这能清除一切。

如果您有任何问题,请在评论中问我...

【讨论】:

  • 感谢您提供优雅的解决方案 reyad!下一个宏给了我很多见识。有了这个解决方案,如果我想要一个数组作为参数,我需要将指针传递给 unsigned char 数组,然后我需要像第一个答案一样将大小作为参数之一传递?
  • 嗨,@Sarahcartenz,我已经解释并展示了如何添加对 unsigned charunsigned char * 的支持。使用这种方式,您可以添加对任何 builtin type 以及您的 own custom type 的支持...如果您有这个想法,请告诉我...
  • 因为 0 被用来表示无符号字符数组的结束 (NULL),这也意味着我的数组不能有 0 作为值。但这不是问题,因为我也可以将数组的大小作为参数传递给任务。谢谢雷亚德
  • @Sarahcartenz,'\0' 的 ascii 值是数字'0'。 '\0' 用于表示字符串的终止,'c' lang 是如何设计的,不是我的选择。如果你在字符串的情况下遵循规则会更好,还要注意 unsigned 不允许负值。是的,当然,您可以将长度作为参数传递。不客气……
【解决方案3】:

我认为您从描述中想要的可能是结构的联合,联合的第一个成员是一个枚举器,它定义了正在使用的结构的类型,这就是 C 中经常实现多态性的方式。看看Xevents 的一个巨大示例的 X11 标头

一个简单的例子:

//define our types
typedef enum  {
chicken,
cow,
no_animals_defined
} animal;

typedef struct {
animal species;
int foo;
char bar[20];
} s_chicken;

typedef struct {
animal species;
double foo;
double chew;
char bar[20];
} s_cow;

typedef union {
animal species; // we need this so the receiving function can identify the type.
s_chicken chicken ;
s_cow cow ;
} s_any_species;

现在,这个结构体可以被传递给一个函数并采用任何一种身份。 s_any_species 类型的接收函数可以取消引用。

void myfunc (s_any_species any_species)
{
if (any_species.species == chicken)
   any_species.chicken.foo=1 ;
}

这里的函数指针数组比 long if else 序列更可取,但两者都可以工作

【讨论】:

    【解决方案4】:

    我认为您是在询问如何将一系列不同类型的对象传递给函数。作为一个特殊的细节,您希望函数只接收一个实际参数,但这并不是特别重要,因为通过将多个参数包装在相应的结构中,总是可以将一个接受多个参数的函数转换为另一个只接受一个参数的函数.此外,我将示例 Vfunc() 代码对 vsprintf() 的使用作为实现细节,而不是所需解决方案的基本组件。

    在这种情况下,尽管我严重怀疑您想要的东西是否有用,但作为一个 C 编程练习,它似乎并不那么困难。您似乎正在寻找的基本想法称为tagged union。它也有其他名称,但它与相关的 C 语言概念和关键字非常匹配。中心思想是,您定义一个类型,该类型可以容纳各种其他类型的对象,一次一个,并且带有一个附加成员,该成员标识每个实例当前拥有的类型。

    例如:

    enum tag { TAG_INT, TAG_DOUBLE, TAG_CHAR_PTR };
    union tagged {
        struct {
            enum tag tag;
            // no data -- this explicitly gives generic access to the tag
        } as_any;
        struct {
            enum tag tag;
            int data;
        } as_int;
        struct {
            enum tag tag;
            double data;
        } as_double;
        struct {
            enum tag tag;
            char *data;
        } as_char_ptr;
        // etc.
    };
    

    然后你可以将它与一个简单的列表包装器结合起来:

    struct arg_list {
        unsigned num;
        union tagged *args;
    };
    

    然后,给定一个这样的函数:

    int foo(char *s, double d) {
        char[16] buffer;
        sprintf(buffer, "%15.7e", d);
        return strcmp(s, buffer);
    }
    

    你可以这样包装它:

    union tagged foo_wrapper(struct arg_list args) {
        // ... validate argument count and types ...
    
        return (union tagged) { .as_int = {
            .tag = TAG_INT, .data = foo(args[0].as_char_ptr.data, args[1].as_double.data)
        } };
    }
    

    并像这样调用包装器:

    void demo_foo_wrapper() {
        union tagged arg_unions[2] = {
            { .as_char_ptr = { .tag = TAG_CHAR_PTR, .data = "0.0000000e+00" },
            { .as_double =   { .tag = TAG_DOUBLE,   .data = 0.0 }
        };
        union tagged result = foo_wrapper((struct arg_list) { .num = 2, .args = arg_unions});
        printf("result: %d\n", result.as_int.data);
    }
    

    更新:

    我建议使用 tagged 联合,因为标签对应于问题中描述的格式字符串中的字段指令,但如果它们在实践中对您没有用,那么它们不是必不可少的细节这种方法。如果被调用的函数将在调用者正确打包参数的假设下工作,并且您没有其他用途来使用它们的类型标记数据,那么您可以用更简单、简单的 union 替换标记的联合:

    union varying {
        int as_int;
        double as_double;
        char *as_char_ptr;
        // etc.
    };
    
    struct arg_list {
        unsigned num;
        union varying *args;
    };
    
    union varying foo_wrapper(struct arg_list args) {
        return (union vaying) { .as_int = foo(args[0].as_char_ptr, args[1].as_double) };
    }
    
    void demo_foo_wrapper() {
        union varying arg_unions[2] = {
            .as_char_ptr = "0.0000000e+00",
            .as_double   = 0.0
        };
        union varying result = foo_wrapper((struct arg_list) { .num = 2, .args = arg_unions});
        printf("result: %d\n", result.as_int);
    }
    

    【讨论】:

    • 认为它是一个客户端和一个服务,客户端将打包参数,但不指定类型。该服务将根据请求知道他们将收到的类型。当然,您可以在两者之间进行某种验证。我可以看到用户从不需要的包装器中指定了您的解决方案中的类型?
    • @Sarahcartenz,越来越糟。但是,如果您希望被调用的函数仅通过假设参数类型来工作,那么我描述的方法仍然有效——只需删除标签。事实上,您可以完全删除中间结构,并拥有as_double etc。工会成员直接参考数据。我会立即更新这个答案。
    猜你喜欢
    • 1970-01-01
    • 2021-01-10
    • 1970-01-01
    • 2021-02-23
    • 2012-12-23
    • 2022-11-01
    • 2021-05-17
    • 2019-01-24
    • 2016-08-10
    相关资源
    最近更新 更多