第1条:优先使用隐式类型的局部变量

第1条:优先使用隐式类型的局部变量
隐式类型的局部变量是为了支持匿名类型机制而加入C#语言的。之所以要添
加这种机制,还有一个原因在于:某些查询操作所获得的结果是
I Query able, 而其他一些则返回IEnumerable。如果硬要把前者当
成后者来对待, 那就无法使用由l Query Provider所提供的很多增强功能了
(参见第42条) 。用var来声明变量而不指明其类型, 可以令开发者把注意
力更多地集中在名称上面,从而更好地了解其含义。例如,
jobs Queued By Region这个变量名本身就已经把该变量的用途说清楚了
即便将它的类型Dictionary>写出来, 也不会给人提
供多少帮助。
对于很多局部变量, 笔者都喜欢用var来声明, 因为这可以令人把注意力放
在最为重要的部分,也就是变量的语义上面,而不用分心去考虑其类型。如
果代码使用了不合适的类型,那么编译器会提醒你,而不用你提前去操心。
变量的类型安全与开发者有没有把变量的类型写出来并不是同一回事。在很
多场合, 即便你费心去区分I Query able与IEnumerable之间的差别, 开发者
也无法由此获得有用的信息。如果你非要把类型明确地告诉编译器,那么有
时可能会改变代码的执行方式(参见第42条)。在很多情况下,完全可以使
用var来声明隐式类型的局部变量, 因为编译器会自动选择合适的类型。但
是不能滥用这种方式,因为那样会令代码难于阅读,甚至可能产生微妙的类
型转换bug。
局部变量的类型推断机制并不影响C#的静态类型检查。这是为什么呢?首先
必须了解局部变量的类型推断不等于动态类型检查。用var来声明的变量不
是动态变量,它的类型会根据赋值符号右侧那个值的类型来确定。var的意
义在于,你不用把变量的类型告诉编译器,编译器会替你判断。
笔者现在从代码是否易读的角度讲解隐式类型的局部变量所带来的好处和问
题。其实在很多情况下,局部变量的类型完全可以从初始化语句中看出来:
懂C#的开发者只要看到这条语句, 立刻就能明白foo变量是什么类型。此
外,如果用工厂方法的返回值来初始化某个变量,那么其类型通常也是显而
易见的:
某些方法的名称没有清晰地指出返回值的类型,例如
这个例子当然是笔者刻意构造的,大家在编写代码的时候应该把方法的名字
起好,使得调用方可以据此推断出返回值的类型。对于刚才那个例子来说,
其实只需要修改变量的名称,就能令代码变得清晰:
尽管方法名本身没有指出返回值的类型,但是像这样修改之后,很多开发者
就可以通过变量的名称推断出该变量的类型应该是Product。
Highest Selling Product变量的真实类型当然要由Do Some Work方法的签
名来决定, 因此, 它的类型可能并不是Product本身, 而是继承自Product
的类, 或是Product所实现的接口。总之, 编译器会根据Do Some Work方
法的签名来认定Highest Selling Product变量的类型。无论它在运行期的实
际类型是不是Product, 只要没有明确执行类型转换操作, 那么一律以编译
器判断的类型为准。
用var来声明变量可能会令阅读代码的人感到困惑。比方说, 如果像刚才那
样用方法的返回值来给这样的变量做初始化,那么就会造成此类问题。查看
代码的人会按照自己的理解来认定这个变量的类型,而他所认定的类型可能
恰好与变量在运行期的真实类型相符。但是编译器却不会像人那样去考虑该
对象在运行期的类型,而是会根据声明判定其在编译期的类型。如果声明变
量的时候直接指出它的类型,那么编译器与其他开发者就都会看到这个类
型, 并且会以该类型为准, 反之, 若用var来声明, 则编译器会自行推断其
类型,而其他开发者却看不到编译器所推断出的类型。因此,他们所认定的
类型可能与编译器推断出的类型不符。这会令代码在维护过程中遭到错误地
修改, 并产生一些本来可以避免的bug。
如果隐式类型的局部变量的类型是C#内置的数值类型,那么还会产生另外一
些问题,因为在使用这样的数值时,可能会触发各种形式的转换。有些转换
是宽化转换(widening conversion) , 这种转换肯定是安全的, 例如从
float到double就是如此, 但还有一些转换是窄化转换(narrowing
conversion) , 这种转换会令精确度下降, 例如从long到int的转换就会产
生这个问题。如果明确地写出数值变量所应具备的类型,那么就可以更好地
加以控制,而且编译器也会把有可能因转换而丢失精度的地方给你指出来。
现在看这段代码:
请问total的值是多少?这个问题取决于Get Magic Number方法的返回值是
什么类型。下面这5种输出结果分别对应5个Get Magic Number版本, 每个
版本的返回值类型都不一样:
total变量在这5种情况下会表现出5种不同的类型, 这是因为该变量的类型
由变量f来确定,而变量f的类型又是编译器根据Get Magic Number()的返
回值类型推断出来的。计算total值的时候, 会用到一些常数, 由于这些常数
是以字面量的形式写出的,因此,编译器会将其转换成和f一致的类型,并
按照那种类型的规则加以计算。于是,不同的类型就会产生不同的结果。
这并不是C#编译器的缺陷,因为它只是按照代码的含义照常完成了任务而
已。由于代码采用了隐式类型的局部变量,因此编译器会自己来设定变量的
类型,也就是根据赋值符号右侧的那一部分做出最佳的选择。用隐式类型的
局部变量来表示数值的时候要多加小心,因为可能会发生很多隐式转换,这
不仅容易令阅读代码的人产生误解,而且其中某些转换还会令精确度下降。
这个问题当然也不是中var所引发的, 而是因为阅读代码的人不清楚
var foo=new My Type O) ;
var thing=Account Factory.Create Savings Account O) ;
var result=some Object.Do Some Work(another Parameter) ;
var Highest Selling Product=some Object
var f=Get Magic Number() ;
Declared Type:Double, Value :166. 666666666667
Declared Type:Decimal, Value :166. 66666666666666666666666667
.Do Some Work Can other Parameter) ;
var total=100*f/6;
Console.WriteLine(
Declared Type:Single, Value :166. 6667
Declared Type:Int 32, Value : 166
S”Declared Type:
Declared Type:
(total.GetType() .Name) ,
Value:
(total) ”) ;
Int 64, Value : 166

Get Magic-Number
的返回值究竟是什么类型,也不知道运行过程中会
(total.GetType() .Name) , Value:(total) ”) ;
(total.GetType() .Name) , Value:(total) ”) ;
的返回值类型可以隐式地转换为变量f所具备的类型
发生哪些默认的数值转换。把变量f的声明语句拿掉之后,问题依然存在:
就算明确指出total变量的类型,也无法消除疑惑:
total的类型虽然是double, 但如果Get Magic Number()返回的是整数
那么程序就会按照整数运算的规则来计算
100*Get Magic Number() /6的值, 而无法把小数部分也保存到total中。
代码之所以令人误解, 是因为开发者看不到Get Magic Number()的实际
段代码就会好读一点,因为编译器会把开发者所犯的错误指出来。当
时, 编译器不会报错。例如当方法返回的是int且变量f的类型是decimal时,
就会发生这样的转换。反之,若不能执行隐式转换,则会出现编译错误,这
会令开发者明白自己原来理解得不对,现在必须修改代码。这样的写法使得
开发者能够仔细审视代码,从而看出正确的转换方式。
刚才那个例子说明局部变量的类型推断机制可能会给开发者维护代码造成困
难。与不使用类型推断的情况相比,编译器在这种情况下的运作方式其实并
没有多少变化,它还是会执行自己应该完成的类型检查,只是开发者不太容
易看出相关的规则与数值转换行为。在这些场合中,局部变量的类型推断机
制起到了阻碍作用,使得开发者难以判断相关的类型。
但是在另外一些场合里面,编译器所选取的类型可能比开发者手工指定的类
型更为合适。下面这段简单的代码会把客户姓名从数据库里面拿出来,然后
寻找以字符串start开头的那些名字, 并把查询结果保存到变量q 2中:
这段代码有严重的性能问题。第一行查询语句会把每一个人的姓名都从数据
I Query able类型, 但是开发者却把保存该返回值的变量q声明成了
IEnumerable, 因此编译器并不会报错, 但是这样做将导致后续的代码
无法使用由I Query able所提供的某些特性。接下来的那行查询语句, 就受到
了这样的影响, 它本来可以使用Query able.Where去查询, 但是却用了
I Query able类型了。假如l Query able不能隐式地转换成
IEnumerable, 那么刚才那种写法会令编译器报错。但实际上是可
以完成隐式转换的,因此编译器不会报错,这使得开发者容易忽视由此引发
第二条查询语句调用的并不是Query able.Where,而是
Enumerable.Where, 这对程序性能有很大影响。第42条会讲到,
I Query able能够把与数据查询有关的多个表达式树组合成一项操作, 以便一
次执行完毕,而且通常是在存放数据的远程服务器上面执行的。刚才那段代
码的第二条查询语句相当于SQL查询中的where子句, 由于执行这部分查询
时所针对的数据源是IEnumerable类型, 因此, 程序只会把第一条
查询语句所涉及的那部分操作放在远程电脑上面执行。接下来,必须先把从
数据库中获取到的客户姓名全都拿到本地,然后才能执行第二条查询语句
这次的变量q是I Query able类型, 该类型是编译器根据第一条查询
语句的返回类型推断出来的。C#系统会把接下来那条用于表示Where子句
的查询语句与第一条查询语句相结合,从而创建一棵更为完备的表达式树。
只有调用方真正去列举查询结果里面的内容时,这棵树所表示的查询操作才
会得到执行。由于过滤查询结果所用的那条表达式已经传给了数据源,因
此,查到的结果中只会包含与过滤标准相符的联系人姓名,这可以降低网络
流量,并提高查询效率。这段范例代码是笔者特意构造出来的,现实工作中
如果遇到此类需求,直接把两条语句合起来写成一条就行了,不过这个例子
所演示的情况却是真实的,因为工作中经常遇到需要连续编写多条查询语句
这段代码与刚才那段代码相比,最大的区别就在于变量q的类型不再由开发
者明确指定,而是改由编译器来推断,这使得其类型从原来的
IEnumerable变成了现在的I Query able。由于扩展方法
是静态方法而不是虚方法,因此,编译器会根据对象在编译期的类型选出最
为匹配的调用方式,而不会按照其在运行期的类型去处理,也就是说,此处
不会发生后期绑定。即便运行期的那种类型里面确实有实例成员与这次调用
相匹配,编译器也看不到它们,因而不会将其纳入候选范围。
一定要注意:由于扩展方法可以看到其参数的运行期类型,因此,它能够根
据该类型创建另一套实现方式。比方说, Enumerable.Reverse() 方法如
果发现它的参数实现了I List或I Collection接口, 那就会改用另一种
方式执行,以求提升效率(关于这一点,请参见本章稍后的第3条)
写程序的时候,如果发现编译器自动选择的类型有可能令人误解代码的含
义,使其无法立刻看出这个局部变量的准确类型,那么就应该把类型明确指
出来, 而不要采用var来声明。反之, 如果读代码的人根据代码本身的语义
所推测出的类型与编译器自动选择的类型相符, 那就可以用var来声明。比
方说,在刚才那个例子里面,变量q用来表示一系列联系人的姓名,看到这
var total=100*f/6;
Console.WriteLine(
S”Declared Type:
S”Declared Type:
double total=100*Get Magic Number()
Console.WriteLine(
值,
返回类型,
如果把Get Magic Number
Get Magic Number()
库里取出来,由于它要查询数据库,
IEnumerable
Enumerable.Where。
IEnumerable
的性能问题。
(相当于SQL查询中的where子句)
与之相符的结果。
下面这种写法比刚才那种写法要好:
的地方。
/6;
也无法轻易观察出计算过程中所发生的数值转换。
from c in db.Customers
select c.Contact Name;
var q 2=q.Where(s=>s.Starts With(start) ) ;
return g 2;
(string start)
var q 2=q.Where(sms.Starts With(start) ) :
return q 2;
的返回值保存在类型明确的变量中,那么这

如果开发者不把变量q的类型明确指定为
那么编译器就可以将其设为更加合适的
public IEnumerableFind Customers Starting With l(
string start)
(
IEnumerable cstring>q=
)
public IEnumerableFind Customers Starting With
var q-
因此,其返回值实际上是
由于l Query able继承自
,以便从中搜索指定的字符串,并返回
from c in db.Customers
select c.Contact Name;

条初始化语句的人肯定会把q的类型理解成字符串,而实际上,编译器所判
定的类型也正是字符串。像这样通过查询表达式来初始化的变量,其类型通
常是较为明确的, 因此, 不妨用var来声明。反之, 若是初始化变量所用的
那条表达式无法清晰地传达出适当的语义,从而令阅读代码的人容易误解其
类型, 那么就不应该用var来声明该变量了, 而是应该明确指出其类型。
总之,除非开发者必须看到变量的声明类型之后才能正确理解代码的含义,
否则,就可以考虑用var来声明局部变量(此处所说的开发者也包括你自己
在内,因为你将来也有可能要查看早前写过的代码)。注意,笔者在标题里
面用的词是优先, 而非总是, 这意味着不能盲目地使用var来声明一切局部
变量, 例如对int、float、double等数值型的变量, 就应该明确指出其类
型, 而对其他变量则不妨使用var来声明。有的时候, 即便你多敲几下键
盘,把变量的类型打上去,也未必能确保类型安全,或是保证代码变得更容
易读懂。如果你选用了不合适的类型,那么程序的效率就有可能会下降,这
样做的效果还不如让编译器自动去选择。

你可能感兴趣的:(Efective,C#,改善代码的50个有效方法)