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
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() )

           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;

    }

}

 

發佈了73 篇原創文章 · 獲贊 2 · 訪問量 39萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章