C#语法造成的小问题(编译原理知识)
昨天看到一篇帖子,说的是C#里面针对byte类型的计算,+号操作符和+=操作符对于数据类型的隐式转换有两种不同的处理方式,例如下面的代码是不能编译通过的:
using System;
public class ByteOp { public static void Main() { byte b = 1; b = b + 1; } } |
使用csc.exe编译的结果是:
ByteOp.cs(8,13): error CS0266: Cannot implicitly convert type 'int' to 'byte'.
An explicit conversion exists (are you missing a cast?)
编译器报告说第8行有错误,因为在第8行,1是当作整型(int)来处理的,而b + 1的结果根据隐式转换的规则是整型,你当然不能将一个整型隐式赋值给byte型的变量啦。
然而有趣的是,下面的代码竟然能够编译通过,天!人和人之间的区别咋就这么大呢?
using System;
public class ByteOp { public static void Main() { byte b = 1; b += 1; } } |
关于+符号,这个好理解,小容量的类型(byte)和大容量的类型(int)相加的结果应该是按照大容量的类型计算,否则以小容量计算的话就极容易发生溢出。
但是相似的概念也应该应用在+=符号才对呀,为什么会是上面的结果呢?我们来看看C#规范怎么说。
发生这个差别实际上是由于C#的语法造成的,而且我怀疑其他C家族的语言都应该有类似的行为。让我们看看C#的语法是怎么说的,你可以在Visual Studio的安装目录找到C#语言的规范:D:"Program Files"Microsoft Visual Studio 9.0"VC#"Specifications"1033"CSharp Language Specification.doc。
在C#规范的第219页(如果你用的也是C# 3.0的话),或者说7.16.2节,有下面一段话:
· If the return type of the selected operator is implicitly convertible to the type of x, the operation is evaluated as x = x op y, except that x is evaluated only once.
· Otherwise, if the selected operator is a predefined operator, if the return type of the selected operator is explicitly convertible to the type of x, and if y is implicitly convertible to the type of x or the operator is a shift operator, then the operation is evaluated as x = (T)(x op y), where T is the type of x, except that x is evaluated only once.
· Otherwise, the compound assignment is invalid, and a compile-time error occurs.
另外,C#规范里面还提供了几个例子:
byte b = 0;
char ch = '"0';
int i = 0;
b += 1; // Ok
b += 1000; // Error, b = 1000 not permitted
b += i; // Error, b = i not permitted
b += (byte)i; // Ok
ch += 1; // Error, ch = 1 not permitted
ch += (char)1; // Ok
注意上面用红色高亮显示的一段话,简单说就是在使用+=符号的时候,如果两端的符号有显示转换的操作符(cast operator)存在的话,并且两端的确可以互相转换的话,那么+=可以使用显示转换操作符将大容量类型转换成小容量类型。
好啦,本来我们讲到上面这些就可以打住了,但是在博客里面我声明过我懂编译原理,一直没有什么文章讲编译方面的事情。那我们就再进一层吧,为什么C#编译器要这样处理呢?我们来看看C#语法里面关于这两个操作符的信息:
additive-expression:
multiplicative-expression: unary-expression:
primary-expression:
primary-no-array-creation-expression:
literal:
decimal-integer-literal:
decimal-digit: one of
integer-literal:
parenthesized-expression:
expression:
assignment: assignment-operator:
non-assignment-expression:
conditional-expression:
null-coalescing-expression:
conditional-or-expression:
conditional-and-expression:
inclusive-or-expression:
exclusive-or-expression:
and-expression:
equality-expression:
relational-expression:
shift-expression:
|
上面是有红色部分高亮显示文本的b + 1相关的语法,即编译器在分析b = b + 1的语法的时候,顺序应该是这样的:
expression -> assignment -> unary-expression assignment-operator expression 其中: unary-expression -> assignment-operator -> = expression -> 编译器重新走下面的流程
expression -> non-assignment-expression -> conditional-expression -> null-coalescing-expression null-coalescing-expression -> conditional-or-expression -> conditional-and-expression conditional-and-expression -> inclusive-or-expression -> exclusive-or-expression exclusive-or-expression -> and-expression -> equality-expression -> relational-expression relational-expression -> shift-expression -> additive-expression -> ...(请看上面红色高亮显示的语法) |
语法之所以会设计的如此复杂,是因为这种语法设计可以将操作符的优先级顺序集成进去,原因请随便找一个编译原理语法分析部分啃一啃,否则我也得另开一大类给你解释这个问题。编译原理的书都不是很贵,40多块的性价比就已经很好了……
一般来说,手工编写语法分析器的编译器都会采用自顶向下的解析方法,而自顶向下解析法最大的一个特征就是每一条语法都会有一个函数对应,例如上面的expression: assignment 语法,就会有一个函数expression(…)对应来解析expressoin的语法。这种方法的好处就是将递归的编程技巧应用到递归的语法解析上面了。编译器在分析b = b + 1的时候,语法解析器的伪码可能就类似下面的样子:
private bool Expression() { if ( Non-assignment-expression() ) return true; else return Assignment(); }
private bool Non-assignment-expression() { if ( Conditional-expression() ) return true; else ... }
...
private bool Additive-expression() { if ( Multiplicative-expression() ) return true; else ... }
private bool Assignment() { if ( !Unary-expression() ) return false;
if ( !Assignment-operator() ) return false;
if ( !Expression() ) return false; else return true; }
...
private bool Assignment-operator() { switch ( currentCharacterInSourceFile ) { case EQUAL: // '=' case PLUS_EQUAL: // '+=' case MINUS_EQUAL: // '-=' case ...: // '=' return true;
default: return false; } } |
从上面的代码你大概可以猜到,b = b + 1实际上要经过至少两个Expression()的递归调用,而在Additive-expression()函数调用里面(具体分析b + 1的那一个函数)已经没有什么上下文来判断b + 1所处的环境了,即编译器没有办法知道b + 1的结果是将会被赋值给一个byte类型的变量,还是会赋值给其它类型的变量(例如什么猫呀,狗呀),因此编译器只好采取默认的隐式转换规则将b + 1的结果的类型设置成整型。而在Assignment ()函数里面负责分析 b = …,Assignment()函数可以知道等号左边的值的类型和等号右边的值的类型,因为C#是强类型语言,因此Assignment()函数里面会执行判断,强制要求等号左边和右边的类型完全相同,这就是为什么本文里面第一个程序不能编译通过的原因。
好了,经过上面的分析,有的哥们可能会讲,从C#的语法来看,Assignment()函数同样需要负责解析 b += 1这个情况,那为什么第二个程序可以编译通过呢?对的,b += 1同样需要经过前段文字里面描述的解析过程,1经过Expression()分析以后,的确也会解释成整型,然而它与b + 1的区别是。经过Expression()解析以后,b + 1会解释成一个整型变量,而1则会被解释成一个常量。对于整型变量编译器不能盲目生成用显示类型转换符(cast operator)转换等号两边的值,否则转换失败的话,程序员都不知道如何调试InvalidCastException的错误!而对于常量就没有这个问题了,因为编译器可以知道+=或者=右边常量是否可以被安全地转换成左边的类型,也就可以生成正确的代码。
不信,你可以试一下下面两个程序是否还能编译通过?
程序1
using System;
public class ByteOp { public static void Main() { byte b = 1; b += Test(); }
private static int Test() { return 1; } } |
程序2
using System;
public class ByteOp { public static void Main() { byte b = 1; b += 1000; } } |