昨天看到一篇文章,說的是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型的變量啦。
然而有趣的是,下面的代碼竟然能夠編譯通過,天!人和人之間的差別咋就這麼大呢?
b += 1;
關于+符号,這個好了解,小容量的類型(byte)和大容量的類型(int)相加的結果應該是按照大容量的類型計算,否則以小容量計算的話就極容易發生溢出。
但是相似的概念也應該應用在+=符号才對呀,為什麼會是上面的結果呢?我們來看看C#規範怎麼說。
在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 = xop 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 explicitlyconvertible 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
additive-expression + multiplicative-expression
additive-expression – multiplicative-expression
multiplicative-expression:
unary-expression
multiplicative-expression * unary-expression
multiplicative-expression / unary-expression
multiplicative-expression % unary-expression
unary-expression:
primary-expression
+ unary-expression
- unary-expression
! unary-expression
~ unary-expression
pre-increment-expression
pre-decrement-expression
cast-expression
primary-expression:
primary-no-array-creation-expression
array-creation-expression
primary-no-array-creation-expression:
literal
simple-name
parenthesized-expression
member-access
invocation-expression
element-access
this-access
base-access
post-increment-expression
post-decrement-expression
object-creation-expression
delegate-creation-expression
anonymous-object-creation-expression
typeof-expression
checked-expression
unchecked-expression
default-value-expression
anonymous-method-expression
literal:
boolean-literal
integer-literal
real-literal
character-literal
string-literal
null-literal
decimal-integer-literal:
decimal-digits integer-type-suffixopt
decimal-digit: one of
0 1 2 3 4 5 6 7 8 9
integer-literal:
decimal-integer-literal
hexadecimal-integer-literal
parenthesized-expression:
( expression )
expression:
non-assignment-expression
assignment
assignment:
unary-expression assignment-operator expression
assignment-operator:
=
+=
-=
*=
/=
%=
&=
|=
^=
<<=
right-shift-assignment
non-assignment-expression:
conditional-expression
lambda-expression
query-expression
conditional-expression:
null-coalescing-expression
null-coalescing-expression ? expression : expression
null-coalescing-expression:
conditional-or-expression
conditional-or-expression ?? null-coalescing-expression
conditional-or-expression:
conditional-and-expression
conditional-or-expression || conditional-and-expression
conditional-and-expression:
inclusive-or-expression
conditional-and-expression && inclusive-or-expression
inclusive-or-expression:
exclusive-or-expression
inclusive-or-expression | exclusive-or-expression
exclusive-or-expression:
and-expression
exclusive-or-expression ^ and-expression
and-expression:
equality-expression
and-expression & equality-expression
equality-expression:
relational-expression
equality-expression == relational-expression
equality-expression != relational-expression
relational-expression:
shift-expression
relational-expression < shift-expression
relational-expression > shift-expression
relational-expression <= shift-expression
relational-expression >= shift-expression
relational-expression is type
relational-expression as type
shift-expression:
additive-expression
shift-expression << additive-expression
shift-expression right-shift additive-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() )
...
...
private bool Additive-expression()
if ( Multiplicative-expression() )
private bool Assignment()
if ( !Unary-expression() )
return false;
if ( !Assignment-operator() )
if ( !Expression() )
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
b += Test();
private static int Test()
return 1;
程式2
b += 1000;
本文轉自 donjuan 部落格園部落格,原文連結:http://www.cnblogs.com/killmyday/archive/2009/02/20/1395102.html ,如需轉載請自行聯系原作者