一、前言
C#中有一个 var
类型,不管什么类型的变量,都可以用它接收,实属懒人最爱了。
我没有了解过它的底层,甚至没看过它的说明文档,也不知道怎么就用上它了。凭我经验,这种 “隐式也可能是动态” 类型 应该只是效率比较低,其他不会有多少负面影响。所以在不关心效率的场合,我也就一直用着。
不过使用时,偶尔也会有一些困扰,比如:
var c;
...
此时会报错,
于是我就知道了,使用var类型时,你必须声明与初始化一体,否则就会报错。
还有时,我会用var来接收一些写起来很复杂类型(比如集合嵌套类型),时间一长,自己也不知道此处的var到底是啥类型。
因此,我想进一步学习一下var,了解它的用法和注意项,不管能否解决上述困扰都不亏,毕竟它的使用频率不低。
二、隐式类型的局部变量
局部变量可在不给出显式类型的情况下声明。 var 关键字指示编译器从初始化语句右侧的表达式推断变量的类型。 推断的类型可能是内置类型、匿名类型、用户定义类型或.NET类库中定义的类型。有关如何使用 var
来初始化数组的详情,请参阅 隐式类型数组(Implicity Typed Arrays) 章节。
注意
这边说了是指示编译器从表达式推断,表明var的解析要么是开始编译前,要么是边指示边编译,即在正式编译时已确定,反正肯定是在运行之前确定的。故var不是动态类型。
下面示例展示了用 var
来声明局部变量的多种方式:
// i is compiled as an int
var i = 5;
// s is compiled as a string
var s = "Hello";
// a is compiled as int[]
var a = new[] { 0, 1, 2 };
// expr is compiled as IEnumerable<Customer>
// or perhaps IQueryable<Customer>
var expr =
from c in customers
where c.City == "London"
select c;
// anon is compiled as an anonymous type
var anon = new { Name = "Terry", Age = 34 };
// list is compiled as List<int>
var list = new List<int>();
首先重要一点是,理解 var 关键字并不意味着 “variant(变体,类型变形,类似动态类型吧,即类型会发生变化)” ,也不表示该变量是松散类型(loosely type,也叫弱类型)的或后绑定的(late-bound)。它仅表示编译器会确定并分配最合适的类型。
var
关键字可在以下情况使用:
- 局部变量(变量声明在方法体中),如上例所示。
- 在 for 初始化语句中。
for(var x = 1; x < 10; x++)
- 在 foreach 初始化语句中
foreach(var item in list) {...}
- 在 using 语句中
using (var file = new SteamReader("C:\\myfile.txt")) {...}
显然,用作方法体中局部变量的情况比较多。
2.1 var和匿名类型
许多情况下,var
的使用是可选的,只是为了语法上的便利。
然而,当使用匿名类型初始化变量时,若你之后要访问对象的属性,则必须将该变量声明为 var
。这是 LINQ Query表达式中的常见场景。详细信息,参阅 匿名类型 章节。
从源码角度看,匿名类型没有名称(匿名类就是将几个变量用花括号括起来,类本身没有名称)。
因此,若查询变量已使用 var
初始化,则访问返回的对象序列中属性的唯一方法是在 foreach
语句中使用 var
作为迭代变量的类型。(var相当于变成访问匿名类型对象的入口)
class ImplicitlyTypedLocals2
{
static void Main()
{
string[] words = { "aPPLE", "BLUeBeRrY", "cHeRry" };
// 若一次查询产生了一个匿名类型的序列
// 可在foreach语句只能使用var来访问属性
var upperLowerWords =
from w in words
select new { Upper = w.ToUpper(), Lower = w.ToLower() };
// 执行查询
foreach(var ul in upperLowerWords)
{
Console.WriteLine("Uppercase:{0}, Lowercase:{1}", ul.Upper, ul.Lower);
}
}
}
/*
输出:
Uppercase:APPLE, Lowercase:apple
Uppercase:BLUEBERRY, Lowercase:blueberry
Uppercase:CHERRY, Lowercae:cherry
*/
2.2 批注
以下限制用于隐式类型变量声明中:
var
只能在同一语句中声明和初始化局部变量时才能使用;且变量不能为空、方法组或匿名函数。var
不能用于类中字段。- 使用
var
声明的变量不能用于初始化表达式中。
换句话说,这个表达式是合法的:int i = (i = 20);
,
而该表达式则会生成编译时错误(compile-time error):var i = (i = 20);
- 多个隐式类型变量无法在同一语句中初始化。
- 若作用域内有一个名为
var
的类型,则var
关键字将解析为该类型名称,并且不会被视为隐式类型局部变量声明。class var {} void Method() { var v; ... }
使用 var
关键字的隐式类型只能应用于局部方法作用域(local method scope)内的变量。隐式类型不适用于类字段,因为C#编译器在处理代码时会遇到逻辑悖论(logical paradox):编译器需要知道字段的类型,但在分析赋值表达式之前无法确定类型,且表达式在不知道类型的情况下无法进行评估。考虑下面代码:
private var bookTitles;
bookTitles 是一个 var
类型的类字段。因为该字段没有求值表达式,因此编译器不可能推断出 bookTitles 是什么类型。此外,向字段添加表达式(就像局部变量一样)也是不够的:
private var bookTitles = new List<string>();
当编译器在代码编译期间遇到字段时,它会在处理与之关联的任何表达式之前记录每个字段的类型(即在字段解析时,我会先确认字段类型,再处理表达式;而字段是var,就不知道字段类型了)。编译器在尝试解析 bookTitles
时遇到了同样的悖论:它需要知道字段的类型,但编译器通常会通过分析表达式来确定 var
类型,如果事先不知道类型,这是不可能的。
补充
这部分比较抽象,因为和C#编译器本身的设计有关。
简单介绍一下C#编译器的工作流程:
- 首先,遍历每个源文件并进行顶层的解析。即在所有嵌套的层级上识别每个名称空间、类、结构、枚举、接口和委托类型声明。
- 然后解析字段声明、方法声明等。实际上,除了方法体的内容都会被解析;
- 解析完外面的,之后会回来解析方法体。
如果有"var"字段,那么在表达式被解析之前,就无法确定字段的类型,而这发生在我们知道字段的类型之后,这就冲突了。
你也许会发现 var
对于难以确定查询变量的确切构造类型的查询表达式也很有用。这可能出现在分组和排序操作中。
当特定类型的变量在键盘上输入很繁琐,或者完整输入类型无法增加代码可读性时,var
关键字也很有用。var
以这种方式发挥作用的一个例子是嵌套泛型类型,例如与分组操作一起使用。在以下查询中,查询变量的类型为 IEnumerable<IGrouping<string, Student>>
。只要你和其他维护代码的人都知道这点,为了方便和简洁而使用隐式类型就没有问题。
// 和前面例子相同,除了我们使用了整个last name作为关键字
// 查询变量是一个 IEnumerable<IGrouping<string, Student>>
var studentQuery3 =
from student in students
group student by student.Last;
使用 var
有助于简化代码,但其使用应仅限于需要它的情况下,或者当它使你的代码更易于阅读时。
三、总结
总结一下, var
的内容其实不多,主要就是,
- 要了解它并不是动态类型,它指示编译器根据表达式来推断类型。故它是编译时的代码,不会影响运行效率。
- 主要应用场景就是接收LINQ Query、访问匿名类变量的属性、接收一些比较难写的类型以增强代码可读性(所以若你使用var,但不知道它对应啥类型,那很可能是你自己没理清)。