第 88 讲:C# 3 之隐式变量类型声明
欢迎来到 C# 3!今天我们来看第一个新语法:隐式变量声明(Implicit-typed Variables)。
Part 1 var
关键字
考虑一种情况。如果我现在有一个巨长无比的数据类型 Dictionary<int, Pair<string, IReadOnlyDictionary<string, decimal>>>
。我们按照基本拆分的规则来看,这个可以表达一个学生的基本信息。Dictionary<int, ..>
类型的 int
作为键,搜索和唯一确定学生的学号;而值则是 Pair<string, IReadOnlyDictionary<string, decimal>>
类型。Pair<,>
类型是随便写的,它用来表示一个数据对,因为单个数据没办法存到这个字典里面,所以只能用一个单独的数据类型表达;接着,Pair
里带有两个泛型参数,第一个实际类型为 string
想表示学生的名字,而右边的 IReadOnlyDictionary<string, decimal>
则是一个字典,表示和存储这个学生的各个科目的学习成绩。string
是表示键,搜索和表示科目名,而 decimal
则对应值,表示当前科目的成绩分数。
可以看到,如果你这么实例化的话:
光一个实例化就得写得巨长。于是,我们可以借助 C# 2 的语法“类型别名”来完成:
或者:
不过,这么做也不合理的地方在于,这种类型没必要单独写一个 using
。要遇到一堆这样的东西,全部写在 using
指令上去,一来是很不方便,二来是太长了。放在 using
指令上还需要你手写和写全整个数据类型的所有位置上的类型的全名。
C# 3 引入了一个新的概念:隐式变量类型。我们在之前学到的所有语法里,变量左侧必须配上一个名字。这个名字一定是类型的名称,不管是带命名空间的,还是直接只写名字,这种写法我们都称为显式变量类型(Explicit-typed Variable)。有些时候名字比较长,或者是带泛型的时,泛型参数巨长的时候,这么写起来都不方便。
C# 3 使用关键字 var
来表示数据类型,于是你只需要在 new
后面写一次变量的实际类型的实例化过程,就可以了:
在变量左侧使用 var
代替掉原本完全写出的显式数据类型,这个 var
就显得非常方便了。
Part 2 用法
我们试着来表示和使用 var
吧!
2-1 举例
这个是排序的代码。我们这次请看第 3、5 和第 9 行,我们都用到了 var
关键字。可以发现,var
的用法其实就是代替变量的数据类型,一劳永逸地使用 var
即可。
2-2 会不会降低了可读性?
不过有人可能会问我,这样不就降低了可读性?我使用 var
看似是好事情,但此时变量 i
、j
和 temp
变量的实际类型就不够清楚了。这样说不定会降低可读性。不过我要告诉你的是,可读性其实基本上没有任何的降低,只是你不习惯。为什么我这么说呢?你仔细看看这些变量,它的实际类型其实我们并不关心。拿 i
和 j
来看,难道你还在意它的数据类型具体是什么么?它是循环变量,我只需要知道它能 ++
操作,它能当成索引放在数组的中括号里去获取元素不就行了?而下面的 temp
也是如此。我在操作里仅用到了交换,实际上它是啥类型我们并不关心。它是 T
,而具体被啥类型替代,这些我们完全都不关心。
可能你会觉得,T
只打一个字符,而 var
却要打三个字符出来,这不是反而复杂了?可我想说,第一,泛型参数用 var
完全是出于你熟悉了 var
语法后的习惯问题。你看到 var
既然广泛被用上了,到处临时变量都用 var
关键字代替了之后,突然在代码里用了一个显式数据类型,反而会觉得不统一,强迫症患者表示也非常不舒服。第二是,真的时候,代替 T
这种少于三个字符的单词的情况也不多。所有我们需要替代的数据类型,要么是关键字表示的(什么 sbyte
啊什么的),要么就是自己定义的数据类型。我们定义的数据类型很难取名只取只需要一个字符和两个字符就可以的类型名称。使用 var
再怎么说都比它们打起来方便。
真要抬杠的话,var
在少数情况也确实不及显式数据类型,但没有必要争论这些细节上的问题,因为我们需要的是在书写代码的时候要做到执行效率和可读性里找到折衷的方案,那么 var
确实达到了我们需求和想要的目的——替代复杂类型名称。
Part 3 不能使用 var
声明变量类型的情况
下面我们介绍一些关于 var
不能用的情况。
3-1 不能将 var
用于成员类型声明、参数和返回值上
是的。考虑一种情况,假设我在类型声明里用到了字段,是 var
类型的话:
这样合理吗?其实并不合理。我们要的目的是替代复杂类型名称,但如果我们这里代替了的话,就会出现刚才说过的降低代码可读性的问题。这个字段是我们确确实实随时随地都可以用到的东西,而只有临时变量只有小范围才用得到,所以,超过临时变量的使用范围的话,代码可读性就会出现实打实地降低。所以,C# 3 不允许我们将 var
用在可能会重复使用的地方上。
当然,返回值和参数也都是如此。返回值不多说,但参数呢?因为参数是必须规定起来才能使用的,它也相当于变量。但是问题就在于,参数的数值是从外部传入的,而在方法里我们是无从知晓它的数据类型的,因此在定义和声明方法的时候,参数的具体类型必须我们提前给出来。那么用 var
呢?你就不清楚是啥类型了,自然而然就是一种错误使用了。
所以,总的来说,var
只用来表示一个临时变量的类型,作为一种代替方案。
3-2 const
关键字不能和 var
一起用
C# 早期定义了 const
关键字,可以用在字段上表示字段是编译前就得到的常量数据。而 const
实际上也可以修饰临时变量,表示临时使用的常量信息和数据:
const
修饰符后跟的类型名称用上 var
,即:
你可能会问,为什么啊?因为可读性呗。一旦 const var
放在一起了,由于它是编译前常量,因此它的作用会比正常的变量来说要大一些(不然,你为啥要修饰 const
呢?)。所以,这种情况下是有可能降低可读性。
3-3 多变量同语句声明的时候不能用 var
多变量的同语句声明的语句是使用逗号分隔的方式:
var
替代。因为使用 var
我们虽然可以按照从逻辑上的数据类型兼容的原则发现,多变量定义放在同一个语句里的时候,必须要确保这些变量的数据类型全部要一样,才能写成“变量类型 变量1 = 数值, 变量2 = 数值, ...”的格式。可问题就出在,现在按字面量来看,1 是 int
类型的,而 1.2 则是 double
类型的。那么,这个 var
是表示啥类型呢?这就不一定了。这 a
和 b
定义语句显然不允许同行,类型上就不匹配。
因此,C# 不允许同行多变量定义在同一个语句里面的时候,是不能用 var
的,否则会混淆编译器的处理过程。
3-4 委托类型不能使用 var
这是一个奇怪的现象,也是可以说得通的现象。
考虑这个代码。f
应该表示什么?Console.WriteLine
是静态方法组,表示执行 Console.WriteLine
这个方法。但问题就在于,这个方法是有重载的,也就意味着参数类型我们无法从方法组里知道。因此,委托类型不允许使用 var
来赋值。
你必须改成这样:
这样,编译器才知道,你这里调用的是 Console.WriteLine
方法里,传入一个 int
类型当参数的那个重载版本。
3-5 声明 var
的时候必须初始化
可以看到,i
在稍后被赋值了 20。可问题就出现在编译。因为代码执行和编译看的是逐行的内容,而 i
肯定不能因为我后面的赋值而反推前面代码给出的 i
的数据类型,至少 C# 语法的严谨性不允许我们这样做。因此,var
在使用的时候必须确保变量有赋值部分。如果变量没有赋值部分,只是一个纯变量定义的话,那么 var
是不能用在这种情况上的。
Part 4 是否选用 var
的抉择
那么,啥时候用 var
合适呢?可以从前面的文字里看出,var
只要用在临时变量上的话,随便你啥时候定义类型,都可以用 var
。但,可以用不代表必须这么用。所以选取 var
作为类型声明还是需要我们抉择一下的。
我这里做一个建议。我在写代码的时候,我会遵循这个规则去书写代码:
如果是内置类型具有关键字别名的,那么就写关键字写法,比如
int
、float
、string
等等;如果是枚举类型,当且仅当它必须配合
const
修饰符的时候写全名,其它的时候都写var
;其它任何临时变量定义写类型的时候,全部写
var
。
这是我认为最合适的书写习惯。
对没错,枚举类型是可以用
const
修饰的。因为const
修饰的是有编译前常量的数据类型,而枚举类型具有特征数值类型一个概念,因此枚举类型基本上可以认为是整数类型的命名数据类型,即相当于给整数的数值都取了一个名字。那么本质也是整数,所以整数是有常量一说,自然枚举类型也有常量一说了。
下面我们还是拿排序的例子来展示我使用 var
的情况。
var
var
var
只可能是 int
。因为 var
只代替掉的是匹配的赋值方的表达式的整体类型。0 是 int
字面量,所以它最合适的类型是 int