在C#中进行类型推断常量

54
在 C# 中,以下类型推断是有效的:
var s = "abcd";

但是为什么当变量是一个常量时,类型不能被推断出来呢?

以下代码会抛出编译时异常:

const var s = "abcd"; // <= Compile time error: 
                      //    Implicitly-typed local variables cannot be constant

2
我的直觉是你滥用了变量。如果你知道类型,就将它声明为只读字符串或常量字符串。 - Hamish Grubijan
4
实际上,这主要与好奇心有关。 - Andreas Grech
同意。我认为var关键字更多的是为了简化整个“Dictionary<string,int> myDictionary = new Dictionary<string,int>()”的过程。虽然Andreas提出了一个有趣的问题,但我想不出任何技术原因为什么“const var s =“abcd””不合法。这将是向Eric Lippert(http://blogs.msdn.com/ericlippert/)提出的完美问题。如果您还没有阅读过他的博客,并且您对此类东西感兴趣,那么这是一个不能错过的机会。 - Brian Hasden
4
我认为 var 关键字的主要目的是允许使用匿名类型;这是唯一一种你真正需要 var 关键字的情况。 - Fredrik Mörk
@Fredrik:是的,对不起。我可以看出我的评论可能会误导人。显然,在我描述的情况下,var关键字并不是必需的,但在那里它很有用。正如你所说,它对于匿名类型是必需的。 - Brian Hasden
@Brian:我搜索了一下,发现他的博客上提到了我们两个建议的原因作为引入“var”关键字的推动力:http://blogs.msdn.com/ericlippert/archive/2005/09/27/c-3-0-is-still-statically-typed-honest.aspx - Fredrik Mörk
11个回答

46

我真的希望Lippert过来看一下这个问题

如果有什么想让我注意的事情,你可以在文本中留下我的名字(不是评论),我最终会找到它。或者更好的方法是,你可以“发送推特”给@ericlippert。请注意,这并不构成服务级别协议;我会在空闲时间处理。

为什么当变量是常量时不能推断类型?

“常量”和“变量”是相反的概念。const var让我毛骨悚然。常量是一个永远不会改变且没有存储位置的值;变量是一个存储位置,其内容会发生变化。它们完全不同,因此不要尝试将它们结合起来。选择var语法是为了强调“这是一个变量”,我们将坚持使用它。

var可以代替具体类型声明,但与const结合使用会严重混淆编译器对值的处理方式。因此,禁止使用const var以避免混淆,你必须显式地为常量指定类型。

如果有不使用var的推断常量,我会非常满意:

const Pi = 3.14159;

对我来说看起来没问题。然而,我不知道是否有将其添加到C#的计划。


5
设计师为什么选择不实现常量的类型推断,却允许使用 var 关键字作为已知类型的语法糖(例如 var s = "abc";)? - Andreas Grech
2
如果我在数学中有一个方程,它包含一个“变量”,那么这个变量通常实际上是一个常数。例如,在“2x = 6”中,x是并且始终将是值3。有些人会对“x = x + 1”之类的东西感到震惊,这是一个无法解决的方程,但在许多编程语言中,包括C#,这很常见。 - helium
4
@helium,问题在于编程语言滥用=运算符,将其解释为“将这个值分配给这个存储位置”,而不是将其解释为“评估这个等式的真假”或“声明这两个实体的等同性”。我个人更希望=运算符在C/C++/C#等语言中被定义为像<--这样的符号,但由于历史原因,我们只能使用=表示赋值。 - Eric Lippert
2
@Michael:这两种情况并不完全相同,但它们是类似的。我注意到我们需要担心在分析“var”字段时的循环定义问题。今天,我们需要担心常量分析中的循环问题。如果你说const int x = y + 10;,那么const int y的定义最好不要依赖于x。 - Eric Lippert
2
FSharp对“let x = 10.0”的类型推断没有问题。右手边表达式的类型推断似乎是引用可变性的正交概念。“var”用于可变引用,“const”用于不可变引用。两者的类型推断都是有意义的。 - bradgonesurfing
显示剩余8条评论

19

我同意Eric的观点,这实在是丑陋得令人发指:

const var s = "abcd"

但为什么不直接这样呢?

const s = "abcd"

对我来说,这似乎是一种合理的语法。


7
同意。如果变量可以推断类型,为什么常量不能呢? - Ian Kemp
确实,这将允许代码中少量的冗余。 - Josh Heitzman

12
我只是猜测,但我认为原因可能与编译时将const值放入元数据(这本身就有微妙的后果)有关。也许编译器在如何将var转换为元数据方面存在一些问题。
在Richter的CLR VIA C#(第177页)中, "定义常量会创建元数据。当代码引用常量符号时,编译器会查找定义该常量的程序集的元数据,提取常量的值,并将该值嵌入到生成的IL代码中。" 他接着指出,由于这个原因,你不能得到常量的内存引用。为了更明确一些,在假想的C#代码中,如果程序集A定义一个常量:
//Assembly A, Class Widget defines this:
public static const System.Decimal Pi = 3.14

那么您就有一个 A 的消费者:

//somewhere in the Program.exe assembly
decimal myCircleCurcum = 2 * Widget.pi

程序.exe的编译后中间语言(IL)大致会执行以下伪代码:

// pseudo-IL just to illustrate what would happen to the const
myCircleCurcum = 2*3.14
注意,消费程序集并不知道十进制数3.14与程序集A有任何关系 - 对于program.exe来说,它只是一个文字值。对我来说,这是C#编译器的一种合理行为 - 毕竟,程序集A明确声明pi是一个常量(这意味着该值始终为pi=3.14)。但我敢说,99%的C#开发人员都不了解这个问题的影响,并可能随意将π更改为3.1415。

常量在跨程序集版本方面存在很大问题(这也来自Richter),因为使用包含常量的程序集的消费者不会看到程序集中的常量更改(即重新编译)。这可能会导致消费程序集A的人非常难以找出的错误......以至于我禁止我的团队使用常量。它们稍微提升的性能并不值得它们可能引起的微妙错误。

你只能在确定该值永远都不会更改时才能使用常量 - 即使像π这样被设置为const,你也不能肯定你将来不想要改变精度。

如果程序集A定义:

decimal const pi = 3.14

那么你构建它,然后其他程序集使用它,如果你之后更改了程序集A:

decimal const pi = 3.1415
即使重新构建程序集A,程序集A的消费者仍然会保留旧值3.14!为什么?因为最初的3.14被定义为常量,这意味着程序集A的消费者已经被告知该值不会改变--因此他们可以将pi的值嵌入自己的元数据中(如果重新构建程序集A的消费者,它将得到新的pi值)。再次强调,我认为这并不是C#编译器处理常量的问题--只是开发人员可能不希望在某些情况下无法安全地更改常量的值,在其他情况下则可以安全地更改。安全:消费者永远不会仅通过.dll进行引用(即他们每次都会从源代码构建),不安全:消费者不知道您定义在程序集中的const何时会发生更改。 .NET文档中应该明确指出常量意味着无法在源代码中更改其值
因此,我强烈建议不要使用常量,而是将小部件设置为只读属性。你真的能确定有多少值永远都是const吗?
在我看来,使用常量的唯一真正原因是可能会产生性能影响......但如果你遇到了这种情况,我会想知道C#是否真的是解决你问题的正确语言。简而言之,在我看来,几乎从来不应该使用常量。极少数情况下,微小的性能提升值得潜在的问题。

那么,可以这样说,由于常量的引用值只是像C语言中的#define一样嵌入,编译器甚至不需要推断类型,对吗?(显然,IntelliSense需要类型推断。)如果是这样,那么将const更改为支持问题所需的语法会相当简单,不需要进行任何编译器更改吗? - Ian Kemp
@IanKemp:这是不正确的。将int乘以int常量与将int乘以一个值可以适合intlong常量非常不同。 - supercat
哦,这就是我忘记的事情。const 就像一个替代品,而不是像 JavaScript 或者 C 一样可以在运行时初始化动态值的东西。所以,除了 usingforeach 变量之外,C# 并没有提供一种标记本地变量为不可变的方法。 - binki

8
简短的回答是因为语言设计者(微软)这么说。
来自MSDN
编译器错误CS0822
错误信息:隐式类型局部变量不能是const。
隐式类型局部变量仅在存储匿名类型时才是必需的。在所有其他情况下,它们只是一种方便。如果变量的值永远不会更改,请给它一个显式类型。尝试使用readonly修饰符与隐式类型局部变量将生成CS0106。
要纠正此错误,如果需要将变量设置为常量或只读,请给它一个显式类型。

1
啊,看起来你比我快 - 我认为这已经足够解释了 - “既然没有理由做你正在做的事情,那么你所做的就是错误的,编译器的工作就是阻止你做这样的事情 :p” - Bobby
那么,根据你的论点,为什么编译器不在我执行 var s = "abc"; 时阻止我呢? - Andreas Grech
这也是他们的论点:“如果变量的值永远不会改变,就给它一个明确的类型。”请参见下文。 - Bobby

4
我的回答是什么?由于当前不可能使用"const var",所以甚至不用担心它。这种限制毫无理由地使C#在对待常量和变量方面不平衡,从而产生了一种不对称性。你最好不要使用它。
"var"语法被选择为"这是一个变量"的标识,并且我们将坚持使用它。
我发现Eric Lippert的论点在多个层面上都不令人信服。
Eric,我不知道"我们"是谁,我真的不想听起来粗鲁,但是使用(作为原因)和含义(为什么var是适当的名称)与您试图附加到其上的含义毫不相关。 "Var" 替换类型声明。让我们不要假装它会做其他事情,因为类型和值(以及这个值是否可以更改)是两个不同的东西。奥卡姆剃刀适用于此,没有必要扩展var的含义超出其功能范围。
更重要的是,即使在不能使用隐式声明的时代,var关键字仍然在使用中,人们仍然认为他们的对象是变量,并且没有问题将其变量声明为常量。
"var"的引入是因为有这样的需要。而这种需求不是为了使变量免受成为常量的影响。这种有限的解释产生了另一个需要,目前尚未满足。
你的整个立场可以归结为语义论点——我们只是不喜欢"const var的发音"(例如"让我毛骨悚然地输入")。这很奇怪,因为人们可以输入类似"dynamic static"的东西而没有编译错误,而且这听起来也很别扭。
那么,为什么要强调一些根本没有被歧义化的东西呢?"const var = "Hello World""或其变体真的会让人们感到困惑,无论它是常量还是不是。我认为人们将能够完全理解它的含义,就像他们理解"dynamic static"的含义一样。
真正的底线是,能够隐式声明常量既有道理,而且实际上可能很有用。目前似乎没有办法做到这一点,而且声明"const var"比引入另一个关键字来服务于隐式声明的常量更加合理。

如果你认为艾瑞克的论点完全是基于不必要的复杂语义解释,那么就试着围绕“变量”这个意思来构建同样的论点,假设它被称为另一个名字,比如impl。难道impl不能与const一起使用吗?我很难想出任何一个理由。因此,问题归结为不喜欢“const var”听起来的方式而已,别无他意。我认为我们大多数人都可以轻松克服这个问题。


3

虽然我不同意Lippert先生的推理,但是有一个很好的理由不允许命名常量的隐式类型转换:考虑以下代码的含义,如果类型化的常量不必明确指定它们的类型:

const var ScaleFactor = 2500000000; // Type 'Int64'

...
int thisValue = getNextInt();
total += thisValue * ScaleFactor;

现在假设比例因子需要降低20%。将值更改为2000000000会产生什么影响?即使在代码中指定了该值[例如,当将total += thisValue * 2500000000; 更改为 total += thisValue * 2000000000;时],出现Int64变为Int32的问题仍然会发生,因为更改会相邻于要求该值为 Int64 的代码。相比之下, const 声明可能远离其作用的代码,因此没有可见的方法来知道某个代码是否依赖长类型的常量。


这是一个好观点,即使你仍然可以用后缀指示类型(long0L,带有 . 的任何内容默认为 doublefloat0Fdecimal0M,没有 . 的任何内容都是 int)。当然,说 const s = 25000L 并不比 const long s = 25000 更容易,而且你仍然需要指定类型。 - drzaus
@drzaus:对于纯粹由数字字面量定义的值,类型后缀可能会有所帮助,但如果常量是根据表达式定义的,则对该表达式进行轻微更改可能会意外更改常量的类型。顺便说一句,我认为关于floatdouble转换的规则是相反的。如果合法,语句float foo = 0.1;将完全产生预期的效果,但是const float ten=10.0f; double wrong = 1 / ten;不会,即使ten保存了精确的float值。让我感到恼火的是错误的那个甚至没有产生警告,但正确的那个却无法编译。 - supercat
2
我同意,但你不必强制使用隐式类型。如果你想要这种类型的常量是显式的,你可以始终明确地写出来,编译器会检查不一致性。我不认为这与变量有什么不同。 - Lensflare
@Lensflare:局部变量必须在声明它们的上下文中使用,因此修改隐式类型局部变量定义的人将能够看到类型是否重要以及需要什么类型。常量通常用于远离其定义的地方;虽然允许本地作用域常量并允许这些常量进行类型推断可能是有意义的,但相比于局部变量,局部常量的好处通常会非常小。 - supercat
@Lensflare:如果有一种声明仅在函数内部有效的“const”值的方法,那么这些值就应该具有与“var”相同的推断规则,没有理由不这样做。然而,允许使用任何类型的公共成员的“var”将引入一些重大的复杂性,除非对其使用进行了严格限制。个人认为,即使是带有公共成员的“var”,也应该被允许,并且可以确定某些形式的右侧表达式的类型,而无需查看外部代码;如果这样的规则甚至适用于局部变量... - supercat
显示剩余5条评论

3

我不同意 @Eric 的观点。

var 关键字并不意味着“这是一个变量”,而是意味着“类型将被推断”。

int、long 等是否是用于标识变量的“关键字”?不,它们只是数据类型,可以用于变量或常量。

我认为 var 关键字的名称被认为类似于 Javascript,我认为这是不合适的。

auto 呢?(http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2004/n1705.pdf)


1
我同意你的推理。但是说 var 不是它的创建者在编写时所想的意思,这需要勇气...也许你的意思是 不应该 :) - nawfal
@nawfal 你说得对!但是,我指的是Eric Lippert写的以下句子。 “选择“var”语法是为了强调“这是一个变量”,我们将坚持使用它。” 根据“作者”的说法,“var”的含义不确定。 ECMA-334已经过时,我想知道下面的网页是否可以被视为规范: http://msdn.microsoft.com/en-us/library/bb383973.aspx - Fernando Pelliccioni

2
我意识到我(们)实际上想要的是JavaScript或C中定义的const关键字的行为。也就是说,在运行时计算它的能力,但在后续代码中禁止更新它。当您只想计算一次值时,这很有用,因为它可以强制执行纪律并明确。
换句话说,这个问题真正需要的是能够在本地变量(变量/方法参数)上使用readonly关键字。也就是说,这种语法可能很有用:
// This variable should never be overwritten!
readonly var target = 4;

在C#中,这并不是没有先例。 using() 和迭代(foreach)变量已经表现出了这种方式:

class Program
{
    static void Main(string[] args)
    {
        foreach (var x in new[] { "asdf", })
        {
            System.Console.WriteLine(x);
            // error CS1656: Cannot assign to 'x' because it is a 'foreach iteration variable'
            x = "food";
        }
    }
}

哦,看到了我得到了类型推断和只读行为!太棒了!然而,在实际代码中使用foreach关键字会让人感觉非常笨拙。你试图保护自己或同事不经思考就添加修改x的代码,很难显露出来(除非在编译时发现错误)。这就是为什么如果它成为一种语言特性将会很棒


2
在这种情况下,显然您知道引用类型将是常量,并且是相当原始的类型(常量只能是值类型、字符串等),因此您应该声明该类型,而不是使用隐式类型。换句话说,因为类型明显是常量和已知的,所以没有任何理由使用var。
引用类型隐式声明只有在存储匿名类型时才是必需的,在所有其他情况下它们仅仅是一种方便。如果变量的值从未更改,请给它一个显式类型。尝试在隐式声明的本地变量上使用readonly修饰符将生成CS0106错误。

http://msdn.microsoft.com/en-us/library/bb310881.aspx

编译器错误 CS0822
要纠正此错误,如果您需要变量是常量或只读的,请为其指定明确的类型。

但是使用 var s = "abc";,类型也是已知的,但是没有编译器错误,因为类型是推断出来的。所以你的答案并没有真正回答我的问题。 - Andreas Grech
我理解你的观点,但这并不是const——如果他们阻止了var s = "abc",那么显然就没有隐式类型转换了。const var = 将永远不会更改,所以它显然不是他们想要用于的东西,所以要明确——这就是文档所说的。隐式类型转换是为了使某些场景更容易处理——即linq。语言设计者希望防止开发人员编写糟糕的代码(例如const var),同时使诸如IEnumerable<Foo<Bar>> = ...之类的冗长类型更易于处理。最终,我认为这不是CLR的限制。 - Bobby

1

有趣。我不知道这是C#编译器的限制还是语言本身的基本限制。

为了解释我的意思,请考虑VB。

在VB 9中,您也无法推断常量,但这只是编译器的限制。 在VB 10中,他们能够添加常量类型推断,而不对语言进行任何重大更改。


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