关于C#:是否可以禁止在for循环体内修改循环变量?

Is it possible to ban modifying loop variables inside the body of for-loops?

在 C 或 C 中,修改 for 循环内的循环变量是令人讨厌的错误的来源:

1
2
3
4
5
6
7
8
9
10
11
12
int main() {
    std::vector<int> v (30);
    std::iota(v.begin(), v.end(), 0);
    int j = 0;
    for (size_t i = 0; i < v.size(); i++) {
        std::cout << v[i] << ' ' << i << '\
'
;
        i++; // oops, I mean j++
    }
    std::cout << j << '\
'
;
}

有什么方法可以禁止或警告在编译器或其他东西的帮助下修改循环体内的循环变量?如果可能的话,我该怎么做?


如果你使用 C 的 ranged-for,你可以使循环变量 const。例如

1
2
3
4
5
6
for (const size_t i : boost::irange<size_t>(0, v.size()))
{
    std::cout << v[i] << ' ' << i << '\
'
;
    // i++; // error, can't modify const
}

对于 C,你可以创建一个索引类来使用。因此,以下内容将是一个起点。我确信它可以改进,因为我没有考虑太多。

1
2
3
4
5
6
7
8
9
10
11
12
13
class CIndex {
private:
    size_t  m_index;
public:
    CIndex(size_t i=0) : m_index(i) {}
    ~CIndex() {};

    size_t inc(void) { return ++m_index; }
    size_t val(void) { return m_index; }

    bool operator < (size_t i) { return m_index < i; }
    CIndex & operator =(size_t i) = delete;
};

并且它会被用作:

1
2
3
4
for (CIndex x; x < 10; x.inc()) {
    std::cout << argv[x.val()];
    x = 3;    // generates an error with Visual Studio 2017
}

您可以使用转换运算符修改上述类,使其更直观并类似于标准 size_t 变量。还要添加一个减量运算符。由于想法是使用它代替 size_t 您不再需要比较运算符,因为编译器将进行转换并使用内置比较循环结束。您可能还希望能够指定可选的增量或减量。

修改后的类看起来像:

1
2
3
4
5
6
7
8
9
10
11
12
13
class CIndex {
private:
    size_t  m_index;
public:
    CIndex(size_t i = 0) : m_index(i) {}
    ~CIndex() {};

    size_t inc(size_t i = 1) { return (m_index += i); }    // increment operator
    size_t dec(size_t i = 1) { return (m_index -= i); }    // decrement operator

    CIndex & operator =(size_t i) = delete;
    operator size_t() const { return m_index; }
};

这将允许您在几乎可以使用 size_t 的任何地方使用 CIndex。所以数组索引可以写成 std::cout << argv[x]; 而不是 std::cout << argv[x.val()];.

但是对于 C 语言规范,这并没有允许您将变量标记为在特定范围内不可变或不变。

您真正要求的是能够标记特定的代码行以允许更改变量并标记不允许更改变量的其他代码行。 C 语言规范没有该功能。


在 C 中,你可以隐藏名称并重新声明另一个与 const 同名的标识符,但是你需要使用一些中间对象来帮助,这并不漂亮:

1
2
3
4
5
6
7
for (int i = 0; i < 10; ++i)
{
    const int t = i, i = t;
    printf("i = %d.\
"
, i);  // Works.
    i = 4;                   // Yields compiler error.
}

我不建议这样做,但您可以通过以下方式使其不那么难看:

1
2
#define Protect(Type, Original) \\
    const Type Auxiliary_##Original = Original, Original = Auxiliary_##Original

然后使用:

1
2
3
4
5
6
7
for (int i = 0; i < 10; ++i)
{
    Protect(int, i);
    printf("i = %d.\
"
, i);  // Works.
    i = 4;                   // Yields compiler error.
}


编辑:回答您的最新评论:

Yes, what I'm looking for was just if there is a compiler option that warns it. If there isn't, maybe I should just code more carefully.

不,不幸的是,在 C 中没有。是的,您应该更仔细地编码。一般来说,我建议您考虑一下为什么要拥有这样的功能。如果循环内的"保护"索引变量是一个问题,我会首先问自己我的编码风格是否有意义并且是一致的。

正如 Eric Postpischil 所注意到的,您可以使用一个临时变量将您的索引变量隐藏在一个内部块中作为 const。这样,如果您尝试修改它,编译器就会出错。

然而,这会产生阴影警告(特别是 -Wshadow 标志,这很常见):

1
2
3
4
5
shadow.c:9:14: warning: declaration of ‘i’ shadows a previous local [-Wshadow]
    const int i = t;
              ^
shadow.c:4:11: note: shadowed declaration is here
  for (int i = 0; i < 10; ++i)

为避免这种情况,您可以使用简单的诊断 #pragma 指令并暂时禁用该特定代码块的警告:

1
2
3
4
5
6
7
8
9
10
11
12
13
for (int i = 0; i < 10; ++i)
{
    const int t = i;
    #pragma GCC diagnostic push
    #pragma GCC diagnostic ignored"-Wshadow"
    {
        const int i = t;
        printf("i = %d\
"
, i);
        i = 4; // Will yield a compiler error.
    }
    #pragma GCC diagnostic pop
}

上面的代码有效(当然删除了 i = 4)并且在 GCC 和 Clang 中使用 -Wall -Werror -Wshadow -pedantic 编译时没有警告。

注意:这肯定不是好的做法,但 AFAICT 它是在 C 中实现这种行为的唯一方法。