天天看點

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型的變量啦。

然而有趣的是,下面的代碼竟然能夠編譯通過,天!人和人之間的差別咋就這麼大呢?

        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   ,如需轉載請自行聯系原作者

繼續閱讀