我该如何使C语言中的方法链式调用更为流畅?

4

有一个现有的C API,看起来像这样:

//data
typedef struct {int properties;} Widget;

//interface
Widget* SetWidth(Widget *const w, int width){
    // ...
    return w;
}
Widget* SetHeight(Widget *const w, int height){
    // ...
    return w;
}
Widget* SetTitle(Widget *const w, char* title){
    // ...
    return w;
}
Widget* SetPosition(Widget *const w, int x, int y){
    // ...
    return w;
}

第一个参数始终是指向实例的指针,将实例转换的函数始终将其作为指针返回。

我认为这样做是为了支持某种方法链接

当函数存在于对象范围内的方法中时,方法链接在语言中是有意义的。鉴于当前API的状态,我只能像这样使用它:

int main(void) {
    Widget w;
    SetPosition(SetTitle(SetHeight(SetWidth(&w,400),600),"title"),0,0);
}

有没有在C语言中可以使用的技术来达到其他语言一样的流畅性?

1
不完全是。流畅的界面通常只存在于面向对象的编程语言中。 - Barmar
1
在C语言中,有两个原因导致它不太有用:第一,没有异常。返回值通常用于表示成功或失败。第二,需要手动管理内存,且没有RAII。 - EOF
1
C语言没有方法,它只有函数。你的问题没有意义。 - user207421
@Barmar C语言可以是面向对象的。在某种程度上。 - MD XF
1
@Barmar:流畅,而不是液体。除非你想通过管道泵送你的界面。 - Kerrek SB
2个回答

3

在C语言中没有语法技巧可以实现像其他语言中使用的方法链。在C语言中,您需要编写单独的函数调用,并将对象指针传递给每个函数:

Widget *w = getWidget();
widgetSetWidth(w, 640);
widgetSetHeight(w, 480);
widgetSetTitle(w, "Sample widget");
widgetSetPosition(w, 0, 0);

在C++和其他面向对象编程语言中,同样可以使用方法调用来实现相同的功能:
Widget *w = getWidget();
w->SetWidth(640);
w->SetHeight(480);
w->SetTitle("Sample widget");
w->SetPosition(0, 0);

使用上述API,并假设每个方法都返回this对象,方法链语法如下:

getWidget()->SetWidth(640)->SetHeight(480)->SetTitle("Sample widget")->SetPosition(0, 0);

无论这种方式是否比分开的语句更易读取,都是一种口味和本地编码约定的问题。个人认为这种方式很笨重且难以阅读。从代码生成的角度来看,有一个小优势:对象指针不需要从局部变量重新加载以进行下一次调用。这种微小的优化几乎无法证明链式语法的合理性。
一些程序员试图通过以下方式使其更易于接受:
getWidget()
 -> SetWidth(640)
 -> SetHeight(480)
 -> SetTitle("Sample widget")
 -> SetPosition(0, 0);

再次,这是一种品味和编码约定的问题...但C语言等效代码看起来确实很笨拙:

Widget *w = widgetSetPosition(widgetSetTitle(widgetSetHeight(widgetSetWidth(getWidget(), 640), 480), "Sample widget"), 0, 0);

没有简单的方法可以将这个链条重新组织成更易读的形式。

请注意,一些最古老的C库函数也可以链接在一起:

const char *hello = "Hello";
const char *world = "World";
char buf[200];
strcpy(buf, hello);
strcat(buf, " ");
strcat(buf, world);
strcat(buf, "\n");

可以重新组织成:

strcat(strcat(strcat(strcpy(buf, hello), " "), world), "\n");

但更加安全和受欢迎的方法是这样的:
snprintf(buf, sizeof buf, "%s %s\n", hello, world);

更多相关信息,请参阅:

Marco Pivetta (Ocramius):不良的流畅接口

请注意,如果C对象具有这些调用的函数指针成员,则仍然可以使用上述所有语法,但对象指针仍必须作为参数传递。函数指针通常分组在一个结构中,该结构的指针存储在对象中,模仿C++虚拟方法的实现,使语法稍微繁重一些:

Widget *w = getWidget();
w->m->SetWidth(w, 640);
w->m->SetHeight(w, 480);
w->m->SetTitle(w, "Sample widget");
w->m->SetPosition(w, 0, 0);

链接这些方法也是可能的,但没有实际收益。
最后,应该注意到方法链不允许显式错误传播。在使用链式编程习惯的面向对象编程语言中,可以抛出异常以更或多或少可接受的方式信号化错误。在 C 中,处理错误的惯用方式是返回错误状态,这与返回对象指针相冲突。
因此,除非保证方法一定成功,否则建议不要使用方法链,而是执行迭代测试。
Widget *w = getWidget();
if (SetWidth(w, 640)
||  SetHeight(w, 480)
||  SetTitle(w, "Sample widget")
||  SetPosition(w, 0, 0)) {
    /* there was an error, handle it gracefully */
}

3
我认为他知道其他语言中流畅接口的外观。问题的重点是如何在C语言中获得类似的东西。这所有的一切似乎只是在含糊其辞地表达你不能做到。 - Barmar
@Barmar:在面向对象编程语言中,“fluid”是被选择的术语,但它在这个范围之外有一个通用的意义。我暗示方法链语法在我看来并没有带来太多的流畅性。这只是我的观点,所以我可能离题了。 - chqrlie
@Barmar:原帖的作者可能知道,但是普通读者,尤其是C程序员,可能不知道。 - chqrlie
流畅还是流体?我在评论中打错了,你是说这是一种真实存在的事情吗?标签维基和问题中的链接展示了支持流畅编程的语言是什么样子的。 - Barmar
@Barmar:不,我是在推断……“流利”这个词吧,我想我的英语水平还不够流利。 - chqrlie

1
据我所知,在C语言中实现面向对象风格的函数链式调用没有很好的方法。可以使用函数指针来实现类似于链式调用的行为。无论如何,这会使C代码更加复杂。
以下是一个示例:
#define SCAN_STR(buf)        str(&SCANER_NAME, buf)
#define SCAN_STR_L(buf, len) strL(&SCANER_NAME, buf,len)
#define SCAN_NUM(number)        num(&SCANER_NAME, number)

typedef struct StrScanner
{
    const char* curPos;
    bool isError;
    uint32_t parsedCnt;

    StrScanner* (*chr)(StrScanner* scan, int*);
    StrScanner* (*str)(StrScanner* scan, char* buf);
    StrScanner* (*strL)(StrScanner* scan, char* buf, uint32_t len);
    StrScanner* (*num)(StrScanner* scan, int*);
} StrScanner;

使用链式结构,就像这样:

char myBuf[30];
char my2Buf[30];
int in;

#define SCANER_NAME scan
if(SCANER_NAME.SCAN_STR(myBuf)->SCAN_STR_L(my2Buf, 5)->SCAN_NUM(&in)->isError)
    printf("error\n");
#undef SCANER_NAME

通常情况下,您只需要按照 C 函数的风格编写类似以下的内容:
char myBuf[30];
char my2Buf[30];
int in;

bool res = str(&scan, myBuf);
res = res && strL(&scan, myBuf2, 5);
res = res && num(&scan, num);
if(!res)
        printf("error\n");

如果您可以确保每次只有一个扫描器处于活动状态,您可以使用全局变量来存储扫描器对象的引用。现在您不必将扫描对象传递给每个链式函数。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接