变量声明中使用"var"类型的用法

37

我们的内部审计建议我们使用显式变量类型声明,而不是使用关键字var。他们认为使用var“在某些情况下可能导致意外的结果”。

一旦代码编译为MSIL,我不知道显式类型声明和使用var之间有任何区别。

审核员是一位备受尊敬的专业人士,因此我不能简单地拒绝这样的建议。


13
他使用了一个很好的论点。有没有什么事情在某些情况下不能导致“意外结果”? - Arnis Lapsa
1
请查看此帖子 https://dev59.com/fHVD5IYBdhLWcg3wQJKT - Gage
10
也许您的审计员会很高兴前来打字,例如 IDictionary<ISomeHideouslyLongKey, IList<ISomeRidiculouslyComplicatedValue>> boogidy = new Dictionary<ISomeHideouslyLongKey, IList<ISomeRidiculouslyComplicatedValue>>(); 这样的声明。 - Jesse C. Slicer
18个回答

50

这个怎么样...

double GetTheNumber()
{
    // get the important number from somewhere
}

然后在其他地方...

var theNumber = GetTheNumber();
DoSomethingImportant(theNumber / 5);

在将来的某个时候,有人注意到GetTheNumber只返回整数,因此对其进行了重构,并改为返回int而不是double

糟糕!没有编译器错误,但你开始看到意外的结果,因为之前的浮点运算现在成为了整数运算,却没有被任何人注意到。

话虽如此,这种情况应该被你的单元测试等发现,但它仍然是一个潜在的陷阱。


4
如果函数只返回整数,你能写出什么样的代码会产生意外结果? - badbod99
4
@badbod99:发挥你的想象力!举个简单的例子:考虑Console.WriteLine("97 divided by 5 is " + (97 / 5));Console.WriteLine("97 divided by 5 is " + ((double)97 / 5)); - LukeH
3
如果因为某些浮点运算无意中变成整数运算而没有人注意到,导致你的薪水被无意中舍去了一部分,你会感到满意吗? - LukeH
4
问题在于,这并不是 var 关键字的错。而是 curvedHands.dll 的问题。 - Arnis Lapsa
8
有趣的是,在这个例子中,您很愉快地依赖编译器推断写入的值5(一个int声明)应该被解释为5.0(一个double值)。此外,在“真正”的生产代码中,常量永远不会像这样内联声明,而是作为实际类型为double的常量声明,因为假定这个常量的含义很重要,所以代码的实际语义不会改变。此外,像ReSharper这样的工具会警告“可能会丢失小数部分”。我认为在编写良好的实际代码中,这不是个问题。 - Greg Beech
显示剩余6条评论

22

我倾向于遵循以下方案:

var myObject = new MyObject(); // OK as the type is clear

var myObject = otherObject.SomeMethod(); // Bad as the return type is not clear
如果SomeMethod的返回类型发生变化,这段代码仍将保持编译状态。在最好的情况下,您会得到更深入的编译错误,但在最坏的情况下(取决于如何使用myObject),您可能不会收到任何错误信息。在这种情况下,您可能会遇到运行时错误,这些错误可能非常难以追踪。

7
我们使用Intellisense来显示返回类型。该方法本身应该明显地表明它返回什么。 - Claus Jørgensen
3
@Claus - 有时候你不想悬停在“var”上查看它的类型,而方法名称也不总是符合理想标准。 - ChrisF
2
@Claus:但是如果你能够直接查看声明并知道它的含义,那就更容易了,而不必积极地去获取它。 - Oren A
1
@Oren 你应该清楚你所处的类和方法的上下文。一旦你知道了它,告诉编译器“apple是苹果”只是浪费时间。更糟糕的是 - 它已经知道这一点了。 - Arnis Lapsa
2
Arnis L:这只是傲慢。每个人都有想要重写的遗留代码,但由于时间限制而无法实现。而且你不应该为了其他人在以后修复同一文件中的错误而设置陷阱。 - thecoop
显示剩余7条评论

13

var不是动态类型,它只是一种语法糖。唯一的例外是匿名类型。来自Microsoft文档

在许多情况下,使用var是可选的,只是一种语法上的便利。但是,当一个变量被初始化为匿名类型时,如果您需要在以后访问对象的属性,则必须将变量声明为var。

编译成IL之后没有区别,除非您明确将类型定义为与暗示的类型不同(尽管我想不出为什么要这样做)。编译器不会允许您在任何时候更改使用var声明的变量的类型。

来自Microsoft文档(再次引用)

隐式类型的本地变量与您自己声明类型一样强类型化,但编译器确定类型

在某些情况下,var可能会影响可读性。更多Microsoft文档指出:

使用var至少有可能使其他开发人员更难理解您的代码。因此,C#文档通常仅在需要时使用var。


6
@0xA3,您能提供更多细节吗? - Arnis Lapsa
@0xA3 - 是的,请详细说明。 - ChaosPandion
1
IL 中没有任何区别,如果您使用 var 推断出与显式声明相同的类型。如果您实际上想要一个与推断出的类型不同的类型,那么是有区别的。 - Tim Goodman
4
如果你指的是Tim指出的内容,那么使用0xA3这个观点就有点无关痛痒了。这是一句恶意攻击。 - badbod99
有时候我认为在匿名类型中应该有一个类似于“anon”关键字而不是“var”的东西。 - Jonas Elfström

13

有些情况确实会导致意料之外的结果。我自己是一个var粉丝,但这样可能会出错:

var myDouble = 2;
var myHalf = 1 / myDouble;

显然这是一个错误而不是"意外的结果"。但这确实是一个坑点...


8
在非泛型世界中,当进行隐式转换时,使用var而不是类型可能会导致不同的行为,例如在foreach循环内。在下面的示例中,发生了从objectXmlNode的隐式转换(非泛型的IEnumerator接口仅返回object)。如果你只是用var关键字替换循环变量的显式声明,那么这个隐式转换将不再发生:
using System;
using System.Xml;

class Program
{
    static void Foo(object o)
    {
        Console.WriteLine("object overload");
    }

    static void Foo(XmlNode node)
    {
        Console.WriteLine("XmlNode overload");
    }

    static void Main(string[] args)
    {
        XmlDocument doc = new XmlDocument();
        doc.LoadXml("<root><child/></root>");

        foreach (XmlNode node in doc.DocumentElement.ChildNodes)
        {
            Foo(node);
        }

        foreach (var node in doc.DocumentElement.ChildNodes)
        {
            // oops! node is now of type object!
            Foo(node);
        }
    }
}

结果是,这段代码实际上会根据你使用的是var还是显式类型而产生不同的输出。使用var时,将执行Foo(object)重载,否则将执行Foo(XmlNode)重载。因此,以上程序的输出为:

XmlNode overload
object overload

请注意,这种行为完全符合C#语言规范。唯一的问题在于,var推断出了一个与您预期不同的类型(object),而且这种推断从代码中看不出来。

为了保持简洁,我没有添加IL。但如果您想要,可以使用ildasm查看,编译器实际上会为这两个foreach循环生成不同的IL指令。


3
无论如何,我更喜欢System.Xml.Linq - ChaosPandion
1
@ChaosPandion:如果你的接口要求使用 XmlDocument,那么使用 Linq 可能并不总是你的选择。此外,XmlDocument 只是一个例子,仍然有很多非泛型代码存在(例如几乎所有 Windows Forms 代码),在这些代码中你会遇到我回答中描述的问题。 - Dirk Vollmar
1
@Adkins:如果您正在枚举IEnumerator,那么明确的类型是object。我也遇到过这个问题。另一个解决方案是添加.Cast<YourType>(),但直接指定类型似乎更简单明了。 - recursive
1
@Adkins:这被称为方法重载,是 C# 的一项非常基本的功能。查看基类库并了解它是一个广泛使用的核心特性。它可以防止您拥有大量仅因类型而异的不同方法名称。例如,请参见许多不同的 Convert.ToString() 重载。 - Dirk Vollmar
4
当然,这里有趣的是var声明实现了你所期望的功能,而显式类型声明则没有。使用var时需要手动转换类型,而使用显式类型声明时编译器会自动进行类型转换。如果代码出现错误并出现InvalidCastException异常,使用var的代码更容易调试以找出其原因,因为你自己写了转换类型的代码;而使用显式类型声明时,你必须知道编译器自动插入了类型转换代码。 - Greg Beech
显示剩余11条评论

7

这是一个奇怪的说法,认为使用var永远不应该被使用,因为它“可能会在某些情况下导致意外结果”,因为C#语言中还有比使用var更复杂的微妙之处。

其中之一是匿名方法的实现细节,这可能会导致R#警告“访问修改的闭包”和行为与您从代码中看到的完全不同。与可以用几句话解释的var不同,这种行为需要三篇长博客文章来完全解释,其中包括反汇编器的输出:

这是否意味着您也不应使用匿名方法(即委托、Lambda)以及依赖它们的库,如Linq或ParallelFX,只是因为在某些奇怪的情况下行为可能不符合您的预期?

当然不是。

这意味着您需要了解您正在编写的语言,知道其限制和边缘情况,并测试事物是否按您所期望的方式工作。基于“可能导致某些情况下出现意外结果”的基础上排除语言特性将意味着您只剩下很少的语言特性可用。

如果他们真的想争辩,那就让他们证明你的一些错误可以直接归因于使用var,而显式类型声明可以防止这些错误。我怀疑你不会很快收到他们的回复。

1
如果他们真的想争论这个问题,那就让他们证明一下,你的一些错误可以直接归因于使用 var,而显式类型声明可以防止这些错误。我怀疑你很快就不会再听到他们的回复了。 - Jeroen Wiert Pluimers

4
他们认为使用var“可能会在某些情况下导致意外结果”。如果意外是“我不知道如何阅读代码并弄清楚它正在做什么”,那么是的,它可能会导致意外结果。编译器必须根据变量周围编写的代码来确定要使变量成为哪种类型。
var关键字是一个编译时特性。编译器将在声明中放入适当的类型。这就是为什么你不能做像这样的事情:
var my_variable = null
or
var my_variable;

var关键字很棒,因为在代码本身中需要定义的信息更少。编译器会为您找出它应该执行的操作。当您使用它时,它几乎就像总是编程到接口一样(其中接口方法和属性由变量声明空间内使用的内容定义)。如果变量的类型需要更改(当然是在合理范围内),则无需担心更改变量声明,编译器将为您处理。这听起来可能不重要,但如果您必须更改函数中的返回值,并且该函数在整个程序中都被使用,会发生什么情况呢?如果您没有使用var,则必须查找并替换调用该变量的每个位置。而使用var关键字,则无需担心这些问题。

4
当审计员制定指南时,最好以“保险起见”为原则,即将良好实践列入白名单/将不良实践列入黑名单,而不是告诉人们根据手头情况的评估“只需明智行事”。如果你只说“不要在代码中任何地方使用var”,那么就可以消除许多编码准则中的歧义。这应该使代码看起来更加标准化,而无需解决何时应该这样做和何时应该那样做的问题。我个人非常喜欢var。我将其用于所有本地变量。一直如此。如果产生的类型不清楚,则这不是var的问题,而是初始化变量的方法(命名)的问题...

1
我认为一个不错的折中方案是在这种情况下使用 varIDictionary<string,MyVeryLongGenericClassName<int,string,DateTime>> - ChaosPandion

1

当你有明显的声明时,最好使用var。

ArrayList<Entity> en = new ArrayList<Enity>()

复杂化了可读性

var en = new ArrayList<Entity>()

懒惰、简洁的代码,我喜欢它


1
我在使用var关键字时遵循一个简单的原则。如果您事先知道类型,请不要使用var。 在大多数情况下,我会在LINQ中使用var,因为我可能想返回匿名类型。

但是在任何地方使用var关键字真的很糟糕吗? - x2.
3
这是个人喜好的问题。将 var 用于 所有 变量可能会使得在没有智能感知的情况下很难弄清楚正在发生什么。 - thecoop

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