昨天跟看到一篇帖子,說的是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; } } |