手拉手教你實現一門程式語言 Enkel, 系列 12

KevinOfNeu發表於2018-09-06

本文系 Creating JVM language 翻譯的第 12 篇。 原文中的程式碼和原文有不一致的地方均在新的程式碼倉庫中更正過,建議參考新的程式碼倉庫。

原始碼

Github

為什麼需要命名引數

在 Java 中(多數語言中也是如此)方法呼叫的引數匹配是通過索引值,如果方法呼叫的引數比較少並且引數的型別有差別的情況,是合理的。不幸的是,如果方法呼叫的引數有很多個,並且型別相同,這是個悲劇。

例如: Rect createRectangle(int x1,int y1,int x2, int y2) //createRectangle signature

我打賭你很有可能會傳錯引數。

你發現問題了嗎?這種情況開發者很容易搞混引數的順序,由於是相同型別,編譯器也沒辦幫你檢查問題。

這就是命名引數的有點,你可以給引數指定名字,而不是僅僅通過索引值來指定引數。 使用命名引數有很多好處:

  • 引數的順序不受限制
  • 程式碼可讀性提高
  • 不用再兩個檔案中跳轉對比方法的簽名和實際傳參是否一致

語法規則更改

functionCall : functionName '('argument? (',' argument)* ')';
argument : expression              //unnamed argument
         | name '->' expression   ; //named argument
複製程式碼

方法呼叫的引數之間用逗號分割。argument 有兩種格式,命名引數和未命名引數,這兩種格式不允許同時存在。

記錄引數

在第七部分描述到,方法的解析分為兩個步驟: 首先記錄所有的方法簽名(方法的宣告),下一步是解析方法體,這樣保證在解析方法體的時候,所有的方法簽名都已經被解析過了。

實現命名引數的思路是把命名引數的呼叫轉換成未命名引數的呼叫,引數索引位置通過方法簽名去獲得:

  • 在方法簽名中查詢匹配的引數名字
  • 獲得引數的索引
  • 如果引數的索引值和實際不一致,記錄下來

手拉手教你實現一門程式語言 Enkel, 系列 12

上圖中的示例,x1 的索引和 y1 對調。

{
    //other stuff
    @Override
    public Expression visitFunctionCall(@NotNull EnkelParser.FunctionCallContext ctx) {
        String funName = ctx.functionName().getText();
        FunctionSignature signature = scope.getSignature(funName); 
        List<EnkelParser.ArgumentContext> argumentsCtx = ctx.argument();
        //Create comparator that compares arguments based on their index in signature
        Comparator<EnkelParser.ArgumentContext> argumentComparator = (arg1, arg2) -> {
            if(arg1.name() == null) return 0; //If the argument is not named skip
            String arg1Name = arg1.name().getText();
            String arg2Name = arg2.name().getText();
            return signature.getIndexOfParameter(arg1Name) - signature.getIndexOfParameter(arg2Name);
        };
        List<Expression> arguments = argumentsCtx.stream() //parsed arguments (wrong order)
                .sorted(argumentComparator) //Order using created comparator
                .map(argument -> argument.expression().accept(this)) //Map parsed arguments into expressions
                .collect(toList());
        return new FunctionCall(signature, arguments);
    }
}
複製程式碼

這種方式對位元組碼的生成是透明的,位元組碼生成階段無需瞭解方法呼叫引數是命名還是未命名

示例

如下的 Enkel 程式碼:

NamedParamsTest {

    main(string[] args) {
        createRect(x1->25,x2->-25,y1->50,y2->-50)
    }

    createRect (int x1,int y1,int x2, int y2) {
        print "Created rect with x1=" + x1 + " y1=" + y1 + " x2=" + x2 + " y2=" + y2
    }
}
複製程式碼

編譯後的位元組碼如下:

public class NamedParamsTest {
  public static void main(java.lang.String[]);
    Code:
       0: bipush        25          //x1 (1 index in call)
       2: bipush        50          //y1 (3 index in call)
       4: bipush        -25         //x2 (2 index in call)
       6: bipush        -50         //y2 (4 index in call)
       8: invokestatic  #10                 // Method createRect:(IIII)V
      11: return

  public static void createRect(int, int, int, int);
    Code:
      //normal printing code 
}
複製程式碼

輸出: Created rect with x1=25 y1=50 x2=-25 y2=-50

相關文章