为什么C#在case块中没有局部作用域?


38

我在写这段代码:

private static Expression<Func<Binding, bool>> ToExpression(BindingCriterion criterion)
{
    switch (criterion.ChangeAction)
    {
        case BindingType.Inherited:
            var action = (byte)ChangeAction.Inherit;
            return (x => x.Action == action);
        case BindingType.ExplicitValue:
            var action = (byte)ChangeAction.SetValue;
            return (x => x.Action == action);
        default:
            // TODO: Localize errors
            throw new InvalidOperationException("Invalid criterion.");
    }
}

惊讶地发现一个编译错误:

此范围中已经定义了一个名为“ action”的局部变量

这个问题很容易解决。只是摆脱第二个var就可以了。

显然,在case块中声明的变量具有parent的作用域switch,但是我很好奇这是为什么。鉴于C#不允许执行通过其他案件下降(其需要 breakreturnthrow,或goto case在每个月底语句case块),它似乎很奇怪的是,它将使变量声明内一个case以其它任何使用或冲突与变数case。换句话说,case即使执行不能执行,变量似乎也属于语句的一部分。C#通过禁止混淆或容易滥用的其他语言的某些构造,努力提高可读性。但这似乎注定会引起混乱。请考虑以下情形:

  1. 如果将其更改为此:

    case BindingType.Inherited:
        var action = (byte)ChangeAction.Inherit;
        return (x => x.Action == action);
    case BindingType.ExplicitValue:
        return (x => x.Action == action);
    

    我得到“ 使用未分配的局部变量'action' ”。这令人困惑,因为在C#中我能想到的所有其他构造中,var action = ...都会初始化变量,但是在这里它只是声明了变量。

  2. 如果我要交换这样的情况:

    case BindingType.ExplicitValue:
        action = (byte)ChangeAction.SetValue;
        return (x => x.Action == action);
    case BindingType.Inherited:
        var action = (byte)ChangeAction.Inherit;
        return (x => x.Action == action);
    

    我得到“ 声明前不能使用局部变量'action' ”。因此,case块的顺序在这里似乎并不十分明显,这很重要-通常我可以按照自己希望的任何顺序编写它们,但是由于var必须出现在使用的第一个块中action,因此我必须调整case块相应地。

  3. 如果将其更改为此:

    case BindingType.Inherited:
        var action = (byte)ChangeAction.Inherit;
        return (x => x.Action == action);
    case BindingType.ExplicitValue:
        action = (byte)ChangeAction.SetValue;
        goto case BindingType.Inherited;
    

    然后我没有收到任何错误,但是从某种意义上说,它看起来像在声明变量之前为变量赋了一个值。
    (尽管我想不出您真正想做的任何时间-我什至不知道goto case今天之前存在)

所以我的问题是,为什么C#的设计师不给case块自己的局部作用域?是否有任何历史或技术原因?


4
actionswitch语句之前声明变量,或将每种情况放在大括号中,您将获得明智的行为。
罗伯特·哈维

3
@RobertHarvey是的,我甚至可以不使用甚至进一步编写它switch,我只是对这种设计背后的原因感到好奇。
pswg

如果c#在每种情况下都需要休息,那么我想它必须是出于历史原因,而不是c / java!
tgkprog

@tgkprog我认为这不是出于历史原因,C#的设计人员是故意这样做的,以确保break在C#中不会忘记a的常见错误。
svick

12
有关此相关的SO问题,请参阅Eric Lippert的答案。stackoverflow.com/a/1076642/414076您可能满意也可能不满意。它的意思是“因为他们是1999年选择这么做的。”
安东尼·佩格拉姆

Answers:


24

我认为有一个很好的理由是,在所有其他情况下,“常规”局部变量的范围都是由大括号()分隔的块{}。不正常的局部变量会在语句(通常是块)之前的特殊结构中出现,例如for循环变量或中声明的变量using

另一个例外是LINQ查询表达式中的局部变量,但它们与普通的局部变量声明完全不同,因此我认为那里没有混淆的可能。

作为参考,这些规则在C#规范的§3.7范围中:

  • local-variable-declaration中声明的局部变量的范围是声明所在的块。

  • 在语句的switch-blockswitch声明的局部变量的范围是switch-block

  • 在语句的for-initializerfor声明的局部变量的范围是for-initializerfor-conditionfor-iterator和该语句包含的for语句。

  • 声明为foreach-statementusing-statementlock-statementquery-expression的一部分的变量的范围由给定构造的扩展确定。

(尽管我不完全确定为什么要switch明确提到该块,因为与其他所有提到的结构不同,它没有用于局部变量抽取的任何特殊语法。)


1
+1您能否提供所引用范围的链接?
pswg

@pswg您可以在MSDN上找到用于下载规范的链接。(单击该页面上显示“ Microsoft开发人员网络(MSDN)”的链接。)
2013年

因此,我查阅了Java规范,switches在这方面似乎表现相同(除了要求在cases 末尾进行跳转)。似乎只是从那里复制了此行为。因此,我想简短的答案是- case语句不创建块,它们仅定义switch块的划分-因此本身没有作用域。
pswg

3
回复:我不完全确定为什么会明确提到switch块 -规范的作者只是挑剔,隐含地指出switch块与常规块的语法不同。
埃里克·利珀特

42

但是确实如此。您可以在任意位置通过包装行来创建本地范围{}

switch (criterion.ChangeAction)
{
  case BindingType.Inherited:
    {
      var action = (byte)ChangeAction.Inherit;
      return (x => x.Action == action);
    }
  case BindingType.ExplicitValue:
    {
      var action = (byte)ChangeAction.SetValue;
      return (x => x.Action == action);
    }
  default:
    // TODO: Localize errors
    throw new InvalidOperationException("Invalid criterion.");
}

Yup使用了它,并且按照描述进行工作
Mvision 2013年

1
+1这是一个很好的技巧,但我将接受svick的回答,以最接近解决我的原始问题。
pswg 2013年

10

我将引用埃里克·利珀特(Eric Lippert)的话,这个问题的答案很明确:

一个合理的问题是“为什么这不合法?” 一个合理的答案是“嗯,为什么要这样”?您可以使用以下两种方法之一。这是合法的:

switch(y) 
{ 
    case 1:  int x = 123; ...  break; 
    case 2:  int x = 456; ...  break; 
}

或这是合法的:

switch(y) 
{
    case 1:  int x = 123; ... break; 
    case 2:  x = 456; ... break; 
}

但是您不能同时拥有这两种方式。C#的设计师选择第二种方法似乎是更自然的方法。

这项决定是在1999年7月7日做出的,距现在还不到十年。当天的注释中的注释非常简短,仅说明“开关案例不会创建其自己的声明空间”,然后提供一些示例代码来显示有效和无效的代码。

要了解有关这一特定日子设计师的想法的更多信息,我不得不让很多人对他们十年前的想法感到烦恼,并让他们烦恼最终是一件微不足道的问题。我不会那样做。

简而言之,没有特别令人信服的理由选择一种或另一种方式。两者都有优点。语言设计团队选择了一种方法,因为他们必须选择一种方法。他们选择的那个对我来说似乎很合理。

因此,除非您比Eric Lippert在1999年的C#开发人员团队中工作得更多,否则您将永远不知道确切的原因!


这不是贬低者,但这是昨天对这个问题的评论。
Jesse C. Slicer 2013年

4
我看到了,但是由于评论者没有发布答案,所以我认为通过引用链接的内容显式创建anwser可能是一个好主意。而且我不在乎那票选票!OP询问历史原因,而不是“如何做”答案。
西里尔·甘登2013年

5

解释很简单-这是因为它就像在C语言中那样。为了熟悉起见,像C ++,Java和C#这样的语言已经复制了switch语句的语法和作用域。

(如另一个答案所述,C#的开发人员没有关于如何做出有关大小写范围的决定的文档。但是C#语法的未阐明原则是,除非他们有令人信服的理由进行不同的操作,否则他们将复制Java。 )

在C中,case语句类似于goto-labels。对于已计算的goto,switch语句实际上是一种更好的语法。这些案例定义了进入开关块的入口点。默认情况下,其余代码将被执行,除非有显式退出。因此,只有它们使用相同的范围才有意义。

(从根本上讲。Goto的结构不是结构化的-它们不定义或分隔代码,它们仅定义跳转点。因此goto标签不能引入作用域。)

C#保留了语法,但是通过在每个(非空)case子句后要求退出来引入防止“失败”的保护措施。但这改变了我们对转换的看法!现在,案例是替代分支,就像if-else中的分支一样。这意味着我们希望每个分支都可以定义自己的范围,就像if子句或迭代子句一样。

简而言之:案例共享相同的范围,因为这在C语言中是这样。但是在C#中,这似乎很奇怪且不一致,因为我们将案例视为替代分支而不是转到目标。


1
在C#中,这似乎很奇怪且不一致,因为我们认为案例是替代分支,而不是转到目标。 ”这正是我的困惑。+1,用于解释C#中感觉错误的背后的美学原因。
pswg

4

观察范围的一种简化方法是逐块考虑范围{}

由于switch不包含任何块,因此不能具有不同的作用域。


3
for (int i = 0; i < n; i++) Write(i); /* legal */ Write(i); /* illegal */呢 没有障碍,但是有不同的范围。
svick

@svick:正如我所说,为简化起见,该for语句创建了一个块。switch不会仅在顶层为每种情况创建块。相似之处在于,每个语句创建一个块(计switch为该语句)。
Guvante

1
那你为什么不算数case呢?
svick

1
@svick:因为case不包含块,除非您决定选择添加一个。for除非添加{},否则下一条语句将持续到下一个语句,除非它始终具有一个块,但case一直持续到某些原因导致您离开实际的块(该switch语句)为止。
Guvante
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.