【问题标题】:How to avoid using global in C to detect setting of variable?如何避免在 C 中使用 global 来检测变量的设置?
【发布时间】:2019-07-21 12:46:15
【问题描述】:

我有从两个不同的代码流调用的函数 foo()。 假设这两个代码流有两个不同的函数调用 foo()

bar() 和 tar()

我想根据哪个函数(bar() 或 tar()) 调用了 foo() 来做一些决定。 目前,我正在设置全局变量 IN_BAR = 1;在 bar() 和 IN_BAR = 0 的情况下;在焦油的情况下。 然后我检查 foo() 中“IN_BAR”的值并做一些事情。

int IN_BAR = 0; // global

void bar () {
...
IN_BAR = 1;
foo();
IN_BAR = 0;
..
}

void tar() {
...
foo();
...
}

void foo() {
...
if (IN_BAR)
   do_this();
else
   do_that();
}

目前,有很多地方(在代码库中)看起来像这样。我想避免使用全局变量以及设置和重置全局变量。 有没有办法处理上述情况?还是这里有设计缺陷?

编辑:
在某些情况下,bar() 和 tar() 没有直接调用 foo(),即中间有很多调用,但必须在 bar() 和 tar() 做出决定。

bar()->filler1()->filler2()->foo()
tar()->func1()->func2()->foo()

test.c

#include <stdio.h>
// globals
int data = 0;
int IN_BAR = 0;

int calculate()
{
    // Some algorithm.
    return 10;
}

void foo()
{
    if (!IN_BAR)
        data = calculate();
    // Use data in this method.
    printf("Data: %d\n", data);
}

// This function is a place holder for multiple functions.
void func_bar()
{
    foo();
}

void bar()
{
    IN_BAR = 1;
    data = calculate();
    func_bar();
    IN_BAR = 0;
}

// This function is a place holder for multiple functions.
void func_tar()
{
    foo();
}

void tar()
{
    func_tar();
}

int main()
{
    int c = 1;
    if (c == 1)
        bar();
    else
        tar();
    return 1;
}

我在 bar 内计算一些东西,想在 foo() 中重用,但在 tar 的情况下,我没有计算,想再次计算,因为没有完成计算。

我该如何处理这种情况?

【问题讨论】:

  • 添加参数是一个选项吗?
  • 要么添加一个参数,要么将函数拆分为两个——bar_foo()tar_foo(),并在每个函数中执行相应的工作,使用一个通用函数来完成通用工作。 (void bar_foo(void) { common_foo(); do_this(); }` 和void tar_foo(void) { common_foo(); do_that(); }) 避开全局变量;这是一场可怕的噩梦。
  • 避免全局变量传统上是通过引入额外的函数参数来完成的。
  • 考虑扩展您的minimal reproducible example,添加一层中间函数,作为中间多层的占位符。 (实际上,您可能必须将代码引号/伪代码转换为 MCVE,然后使用占位符函数对其进行扩展...)。顺便说一句,StackOverflow 用户喜欢 MCVE,他们中的一些人(包括我不小心的时候)抱怨没有看到一个,即使问题不需要一个。尽一切努力制造一个,即使不是完全必要的,因此(不公平地)让你获得更好的感知......就像未来的“把戏”。 ;-)
  • 我认为现在你为什么隐含不喜欢另一个参数变得更清楚了。我想我还是会走那条路,这基本上是 calebs 的回答。唉,我应该立即做出自己的回答,而不是先澄清你的情况和需求。

标签: c design-patterns globals


【解决方案1】:

如果你想避免这种情况,那么不要bar 调用foo。因为显然bar 知道foo,它可以调用适当的版本并将该决定留给调用者。

bar 调用footar 调用foo 的情况创建一个单独的函数:

void bar () {
    ...
    foobar();
    ..
}

void tar() {
    ...
    footar();
    ...
}

void foobar() {
    ...
    do_this();
}
void footar() {
    ...
    do_that();
}


编辑:

为避免进行许多代码更改,您只能更改当前使用全局变量的位置(并添加了 Jonathan Leffler 的建议):

void bar () {
    ...
    foobar(); // change only here
    ..
}

void tar() {
    ...
    foo();
    ...
}

void foobar() {
    foocommon();
    do_this();
}
void foo() {
    foocommon();
    do_that();
}
void foocommon()
{
    ...
}

【讨论】:

  • foobar()footar() 中的共同位(以... 表示)不应重复;您可以添加一个由foobar()footar() 调用的foocommon() 函数。当所有函数都采用零参数时,这相当容易。 (我更希望看到用 (void) 而不是 () 定义的函数。)
  • 谢谢保罗!我会检查是否可以使用此方法进行更改。同时,我更新了问题以反映我目前的情况。请参阅“编辑”部分。感谢您的回复。
  • 如果你有一个函数链最终调用foo,那么你需要将上下文作为参数传递。在许多情况下,无论如何您都需要重构代码,您可能需要进行(部分)重新设计。
【解决方案2】:

有没有办法处理上述情况?

听起来foo() 需要一些关于它运行的上下文的信息才能做正确的事情。如果你这样想,那么无论它是什么上下文,显然都是foo() 的输入,因此应该作为参数传入。换句话说,你应该把它叫做foo(context)

或者这里有设计缺陷?

这取决于具体情况。一个函数不应该真正知道或关心它是从哪里调用的,但也许有一些与调用者相关的信息作为输入是有意义的。例如,如果您知道bar() 仅在有一些数据可用时调用,而tar() 仅在数据不可用时调用,那么您可以传入一个指示数据是否可用的参数。或者,如果数据可用,您可以直接传入数据本身;如果不可用,您可以传入 nil

函数不应该关心谁在调用它的原因是,这种知识会使函数变得脆弱、难以测试和使用。它很脆弱,因为如果调用者的情况发生变化,函数可能会中断。很难测试,因为测试通常涉及调用函数,并且期望函数的行为无论从哪里调用都是相同的。而且它很难使用,因为你需要知道函数对调用者的依赖:如果你想从其他地方调用它,你必须更新函数。

在某些情况下,bar() 和 tar() 不直接调用 foo(),即中间有很多调用,但必须在 bar() 和 tar() 中做出决定。

答案仍然几乎相同:调用者有一些信息是被调用者的输入,因此它应该将其作为参数传递。让我们考虑一下您的调用链:

tar()->func1()->func2()->foo()

由于tar() 调用func1() 而不是foo(),它不应该对func1() 的工作方式做出任何假设——它应该只传递func1() 需要完成其工作并返回的信息func1() 返回的结果。 tar()bar() 做出的“决定”,无论实际上是什么,根据您的观点,它是foo() 的必要输入,因此它是func2() 的必要输入,因此它是@ 的必要输入987654337@,即使func1()func2() 除了调用其他函数之外不使用该信息。因此,一种选择是让中间函数各自接受一个新参数,然后将其传递给链中的下一个函数。

如果可能有多个“决定”影响foo(),另一个可能效果更好的选择是创建某种提供“上下文”或“环境”信息的结构,并让中间函数每次通过那个。您在图形系统中经常看到这种风格,其中有一个绘图上下文,其中包含大量可以更改的参数,并且该上下文是传递给系统中大多数函数的参数。

您面临的问题是面向对象编程中经常出现的“我如何获得所需数据”问题的过程版本。选择是: 1) 让对象假设从哪里获取所需的数据;或者 2) 告诉对象从哪里获取它需要的数据。选项 1 通常涉及单例或其他全局可访问的数据块。选项 2 称为依赖注入,通常概括为“告诉,不要问”。

另一个您可能没有考虑过的解决方案是将foo() 分解为两个或多个函数,以便tar()bar() 最终调用两个不同的函数,每个函数都针对各自的情况执行正确的操作。如果tar()bar() 的调用链确实不同,这可能会起作用。在这种情况下,您传递的信息本质上仍然是相同的——您只是使用函数名称隐式地进行。

让我们通过查看您提供的代码来尝试使这个建议更加具体:

void foo()
{
    if (!IN_BAR)
        data = calculate();
    // Use data in this method.
    printf("Data: %d\n", data);
}

这里foo()依赖IN_BAR的原因是bar()调用calculate()并将结果存储在另一个全局变量data中。如果foo() 作为tar() 的一部分被调用,那么calculate() 还没有被调用并且data 可能没有有用的值,所以在这种情况下你需要在foo() 中调用它。

您的具体问题似乎是关于如何避免需要IN_BAR,所以让我们先来看看。这里的一种解决方案是仅将有效的data 作为调用foo() 的要求。如果你这样做,那么foo() 不需要IN_BAR——它可以假设data 总是有效的,因为没有它调用它是一个错误。它将调用calculate() 的职责移出foo() 的领域,它允许你调用链上更高的任何地方calculate()——你可以在tar()func_tar() 或其他任何地方调用,只要在调用foo()之前做,foo()就变得简单了:

void foo()
{
    // Use data in this method.
    printf("Data: %d\n", data);
}

如果由于某种原因您不能这样做,那么您至少可以将data 的有效性与那个变量联系起来。您可以通过为data 定义一些表示“无效”的值来做到这一点,这样您就可以只需查看数据并知道它是否有效。例如,您可以将其类型更改为int* 并将其初始化为nil,然后让calculate() 返回一个指向它计算的int 的指针:

void foo()
{
    if (data == nil)
        data = calculate();
    // Use data in this method.
    printf("Data: %d\n", data);
}

不过,最好重新实现foo(),以便显式传入data

void foo(int data)
{
    // Use data in this method.
    printf("Data: %d\n", data);
}

现在您不需要dataIN_BAR 作为全局变量,foo() 需要data 的值是明确的。如果在没有data 值的情况下foo() 本身可以调用calculate(),那么func_tar() 在调用foo() 之前立即调用calculate() 肯定是可以的:

void func_tar()
{
    foo( calculate() );
}

bar() 本身调用 calculate() 的情况下,您已经获得了一个 data 值,应该将其传递给它的附属函数:

void func_bar(int data)
{
    foo(data);
}

即使func_bar() 是多个功能的替身也是如此。 底线是使用全局变量来避免在参数中传递data 信息是一个坏主意。 也许data 也是几个不同值的替代;在这种情况下,将它们全部组合成一个结构,正如我在上面的讨论中所描述的那样。

【讨论】:

  • 谢谢迦勒!这是思考功能及其预期行为的好方法。我会尝试重构函数以采纳您的建议。
  • 顺便说一句,我已经更新了这个问题。如果你能再看看。谢谢!
  • @Deeps 我已经根据您的示例代码添加了示例。
  • 哇!这是一些很好的解释和深入的解决方案。我采纳了您的建议,将 foo() 分成两部分,并从两个代码流中调用每个版本,这样可以轻松地将信息正确地传递给每个版本。非常感谢!
猜你喜欢
  • 2023-02-16
  • 1970-01-01
  • 2015-02-18
  • 2011-03-24
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2023-03-06
  • 1970-01-01
相关资源
最近更新 更多