为什么无法将方法参数声明为 var 类型

4
我不知道为什么不能像下面这样使用var类型作为方法参数:
private void myMethod(var myValue) {
   // do something
}

11
因为 var 不是一种类型。 - Krzysztof Kozmic
@Hamish Grubijan:或者任何动态类型语言,这并不特定于Python。 - Sasha Chedygov
3
或者使用更好的类型推导语言。为了避免拼写函数参数类型,你不必放弃静态类型。 - sepp2k
没错。在C#中,“var”与Python、Perl等语言中的基于值类型变形不相等。它是编译时的,可以产生更高效的代码。这有很大的区别。 - codenheim
如果您需要动态地进行操作,请使用C# 4.0和dynamic类型,或者尝试使用object类型。 - Oliver
9个回答

9
你只能在方法体内使用var作为变量。此外,变量必须在声明时被赋值,并且必须能够从右侧表达式中明确地推断出类型。
在所有其他地方,即使理论上可以推断出类型,也必须指定类型。
原因是编译器的设计方式。简化描述是,它首先解析除了方法体之外的所有内容,然后对每个类、成员等的静态类型进行全面分析。然后在解析方法体时使用这些信息,特别是用于推断声明为var的局部变量的类型。如果允许在任何地方使用var,那么它将需要大幅改变编译器的工作方式。
您可以阅读Eric Lippert的文章以获取更多详细信息:

为什么这被标记为答案?它描述了原帖作者已经知道的内容,但没有涉及到 C# 类型系统中的根本限制。 - Juliet
1
它确实回答了他的问题。 - PostMan

7
请查看Juliet的答案,这里有更好的解释。
因为将完整类型推断添加到C#中过于困难。其他语言例如Haskell和ML可以自动推断出最通用的类型,无需您声明它。
其他答案声称编译器无法推断var的类型,但实际上原则上是可能的。例如:
abstract void anotherMethod(double z, double w);

void myMethod<T>(T arg)
{
    anotherMethod(arg, 2.0); // Now a compiler could in principle infer that arg must be of type double (but the actual C# compiler can't)
}

在原则上,具有“var”方法参数与通用方法是相同的:

void myMethod<T>(T arg)
{
    ....
}

很遗憾,你不能仅使用相同的语法,这可能是因为C#的类型推断是后来才添加的。

通常,语言语法和语义上微小的变化会将“确定性”的类型推断算法转变为不可判定的算法。


我说您的反例相当有限,是不是错了?它没有考虑到运算符重载(正如您正确提到的那样),也没有规定类似语句总是存在。 - Boris Callens
是的,这就是为什么看似“好用”的功能,比如隐式类型转换和运算符重载,会让类型推断算法变得更加困难。这也是为什么在 ML 中没有隐式类型转换的原因。要正确地进行类型推断,必须从语言设计的一开始就考虑进去。 - Paul Hollingsworth
但是你实际上不能在第一个示例中编写代码,因为编译器无法在编译时确定arg * 2.0是否合法。除非在C# 4中发生了变化,否则此示例将无法编译。至于C# 3,您会得到Operator '*' cannot be applied to operands of type 'T' and 'double' - Anthony Pegram
安东尼,你没有理解我的观点。当然,我的例子不是有效的C#代码。首先:它使用var作为方法参数的类型!我也没有完成方法体。我只是试图解释编译器如何原则上推断方法参数的类型。朱丽叶的答案是最好的解释。 - Paul Hollingsworth
我已经修改了答案,摆脱了运算符重载的红鲱鱼,并希望澄清即使使用泛型参数语法,它也不是有效的C#。 - Paul Hollingsworth

7

因为编译器通过查看赋值语句右侧的内容确定实际类型。例如,在这里,它被确定为字符串:

var s = "hello";

这里被确定为Foo

var foo = new Foo();

在方法参数中,不存在“赋值语句的右边”,因此不能使用 var。

+1:是的,'s'被声明并初始化一次,方法可以从代码中的多个位置调用,应该使用哪些来推断参数的类型? - Binary Worrier

7
请参考Eric Lippert的文章,了解为什么不允许在字段中使用var关键字,并解释为什么它无法在方法签名中工作。让我简要概括一下C#编译器的工作原理。首先,我们遍历每个源文件并进行“仅限顶级”的解析。也就是说,我们在所有嵌套级别上识别每个命名空间、类、结构、枚举、接口和委托类型声明。我们解析所有字段声明、方法声明等。实际上,我们解析除方法体之外的所有内容;对于方法体,我们会跳过并稍后再回来处理。
[...]
如果我们有“var”字段,则在分析表达式之前无法确定字段的类型,而这发生在我们已经需要知道字段的类型之后。

6

ML、Haskell、Scala、F#、SML等语言可以轻松地从其自身语言的等效表达式中推断类型,主要是因为它们从一开始就考虑了类型推断。C#并非如此,它的类型推断是作为解决访问匿名类型问题的事后补救措施而添加的。

我猜想,在C#中没有真正实现Hindley-Milner类型推断,因为在一个如此依赖类和继承的语言中推导类型很复杂。假设我有以下类:

class Base { public void Print() { ... } }

class Derived1 : Base { }

class Derived2 : Base { }

现在我有这个方法:

var create() { return new Derived1(); }

这里的返回类型是什么?是“Derived1”还是应该是“Base”?同样地,它应该是“object”吗?
好的,现在假设我有这个方法:
void doStuff(var someBase) { someBase.Print(); }

void Main()
{
    doStuff(new Derived1());
    doStuff(new Derived2()); // <-- type error or not?
}

第一个调用 doStuff(new Derived1()),可能会将 doStuff 强制转换为类型 doStuff(Derived1 someBase)。现在假设我们推断出具体类型而不是泛型类型 T
第二个调用 doStuff(new Derived1()) 怎么办?它是类型错误吗,还是我们通用化为 doStuff<T>(T somebase) where T : Base ?如果我们在一个单独的、未被引用的程序集中进行相同的调用,类型推断算法将不知道是否使用窄类型或更通用的类型。因此,我们将根据方法调用是来自程序集内部还是外部而得到两个不同的类型签名。
你不能基于函数的使用情况来概括更广泛的类型。一旦知道传递的具体类型是什么,你基本上需要确定一个单一的具体类型。因此,在上面的示例代码中,除非你显式地向上转换到 Base 类型,否则 doStuff 会被限制为接受 Derived1 类型,第二个调用会导致类型错误。
现在的关键是确定一个类型。以下代码会发生什么:
class Whatever
{
    void Foo() { DoStuff(new Derived1()); } 
    void Bar() { DoStuff(new Derived2()); }
    void DoStuff(var x) { ... }
}
DoStuff的类型是什么?根据上面的内容,我们知道FooBar方法中有一个包含类型错误,但你能从外观上看出哪个有错误吗?
在不改变C#语言语义的情况下,无法解决类型问题。在C#中,方法声明的顺序对编译没有影响(或者至少不应该有; ))。你可以说,首先声明的方法(在这种情况下是Foo方法)决定了类型,因此Bar存在错误。
这样做是可行的,但它也改变了C#的语义:方法顺序的更改将更改方法的编译类型。
但是假设我们进一步进行:
// Whatever.cs
class Whatever
{
    public void DoStuff(var x);
}

// Foo.cs
class Foo
{
    public Foo() { new Whatever().DoStuff(new Derived1()); }
}

// Bar.cs
class Bar
{
    public Bar() { new Whatever().DoStuff(new Derived2()); }
}

现在这个方法正在不同的文件中被调用。它是什么类型?没有强制规定编译顺序是无法确定的:如果 Foo.cs 在 Bar.cs 之前编译,那么类型由 Foo.cs 决定。
虽然我们可以强制 C# 遵循这些规则以使类型推断起作用,但这将极大地改变语言的语义。
相比之下,ML、Haskell、F# 和 SML 很好地支持了类型推断,因为它们有这些限制:不能在声明之前调用方法,对于推断函数的第一个方法调用决定类型,编译顺序对类型推断有影响等等。

tl;dr 版本:Hindley-Milner 类型推断对源代码施加了限制,这将改变 C# 的语义。 - Juliet
这是最好的答案。我希望有一种方法可以将我的回答的投票转移到你的回答上。 - Paul Hollingsworth

1

在C#和VB.NET中,“var”关键字用于类型推断 - 你基本上告诉C#编译器:“你来确定类型吧”。

“var”仍然是强类型的 - 你只是懒得自己写出类型,让编译器根据赋值语句右侧的数据类型来确定。

在这里,在方法参数中,编译器无法确定你真正想表达的意思。怎么办?你真正想表达的是什么类型?编译器无法从方法定义中推断出类型 - 因此它不是一个有效的语句。


1
编译器可以推断类型,只需要更多的工作。编译器会查看方法体内使用的类型,或者查看调用者传入的参数,然后推断类型。 - Judah Gabriel Himango
1
我会将其扩展为“你弄清楚它是什么类型,但显然很明显”。 - nbevans
1
像 ML、Haskell、OCaml、Scala、F# 和其他类型推断语言都可以使用 Hindley-Milner 类型系统来计算类似表达式的类型,所以这绝对不是不可能的。 - Juliet
@Juliet: 嗯,C#并不像Haskell那样是一种纯函数式语言,因此它的类型系统有一些其他语言可能没有的限制。说实话:我觉得“var”有点令人不安,在99%的情况下,知道类型并拼写出来根本不是问题——我发现这样更好——你的意图更清楚,这样的代码更容易维护。不要过度使用“var”! - marc_s

1

因为C#是类型安全和强类型语言。在程序的任何位置,编译器都知道你正在使用的参数类型。var关键字只是为了引入匿名类型的变量。


这些变量不是“匿名”类型 - 它们具有明确定义的强类型 - 您只是不费心指定并让编译器自行解决... - marc_s
实际上,我在谈论的是当你不能没有var关键字的情况 - 例如,你正在使用LINQ声明匿名类型。 - Andrew Bezzub

0

检查 C# 4 中的 dynamic


动态语言有什么新特性?你的意思是C#4中也可能实现吗? - wonde
“dynamic” 在这种情况下会如何帮助?? dynamic!= variant! - marc_s
@Wonde:不,你仍然不能在C# 4中像那样使用“var”,但是C# 4引入了动态类型,这意味着绑定是在运行时而不是在编译时完成的。 - Brian Rasmussen
@marc_s @wonde 新关键字留在动态类型后面。当然不是完全相同的。背后的机制也不同,但它可以在某些情况下使用,我想这就是人们所寻找的。您可以创建一个带有动态参数的方法。 - vittore

0

类型推断是指在本地表达式或全局/过程间中的类型推断。因此,它并不是关于“没有正确的右侧”,因为在编译器理论中,过程调用是一种“右侧”形式。

如果编译器进行全局类型推断,C#可以做到这一点,但它没有。

如果您想要接受任何内容的参数,则可以使用“object”,但是然后您需要自己处理运行时转换和潜在异常。

C#中的“var”不是运行时类型绑定,而是编译时特性,最终得到一个非常具体的类型,但C#类型推断的范围有限。


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