兩周自制腳本語(yǔ)言-第9天 設(shè)計(jì)面向?qū)ο笳Z(yǔ)言

第9天 設(shè)計(jì)面向?qū)ο笳Z(yǔ)言

目標(biāo):為Stone語(yǔ)言添加類和對(duì)象的支持琼蚯。僅支持單一繼承

9.1 設(shè)計(jì)用于操作類與對(duì)象的語(yǔ)法

添加的類與對(duì)象的處理功能后,下面的Stone語(yǔ)言就能被正確執(zhí)行了

class Position {
    x = y = 0
    def move(nx,ny) {
        x = nx; y = ny
    }
}
p = Position.new
p.move(3, 4)
p.x = 10
print p.x + p.y

首先定義一個(gè)Position類互纯,方法由def語(yǔ)句定義。類中字段通過(guò)變量表示醉拓,并賦了初始值伟姐。上面的例子定義了move方法以及字段x與y。

類名后接.new組成的代碼表示創(chuàng)建一個(gè)對(duì)象亿卤。為簡(jiǎn)化實(shí)現(xiàn)愤兵,這里規(guī)定Stone語(yǔ)言無(wú)法定義帶參數(shù)的構(gòu)造函數(shù)。

如果希望繼承其他的類排吴,只需在類名之后接著寫上extends即可秆乳。例如,下面的代碼能夠定義一個(gè)及程序Position類的子類Pos3D

class Pos3D extends Position {
    z = 0
    def set(nx,ny,nz) {
        x = nx;y = ny;z = nz
    }
}
p = Pos3D.new
p.move(3,4)
print p.x
p.set(5,6,7)
print p.z

Stone不支持方法重載钻哩。在同一個(gè)類中無(wú)法定義參數(shù)個(gè)數(shù)或類型不同的同名方法

9.2 實(shí)現(xiàn)類所需的語(yǔ)法規(guī)則

代碼清單9.1是與類相關(guān)的語(yǔ)法規(guī)則修改屹堰。
這里只顯示了代碼清單7.1和代碼清單7.13的不同之處。
其中街氢,非終結(jié)符postfix與program的定義發(fā)生了變化扯键,同時(shí)語(yǔ)法規(guī)則中新增了一些其他的非終結(jié)符。

非終結(jié)符class_body表示由大括號(hào){}括起的由分號(hào)或換行符分割組成的若干個(gè)member珊肃。非終結(jié)符postfix經(jīng)過(guò)修改荣刑,支持基于句點(diǎn).的方法調(diào)用與字段訪問(wèn)馅笙。

代碼清單9.2是根據(jù)代碼清單9.1的語(yǔ)法規(guī)則更新的語(yǔ)法分析器程序。代碼清單9.3厉亏、代碼清單9.4與代碼清單9.5是其中用到的類定義董习。

postfix與program通過(guò)insertChoice方法添加了新的or分支選項(xiàng)。

代碼清單9.1 與類相關(guān)的語(yǔ)法規(guī)則

member  :def | simple
class_body  : "{" [ member ] {(";" | EOL) [ member ]} "}"
defclass  : "class" IDENTIFIER [ "extends" IDENTIFIER ] class_body
postfix  :"." IDENTIFIER | "(" [ args ] ")"
program  :[ defclass | def | statement ] (";" | EOL)

代碼清單9.2 支持類的語(yǔ)法分析器ClassPraser.java

package Stone;
import static Stone.Parser.rule;
import Stone.ast.ClassBody;
import Stone.ast.ClassStmnt;
import Stone.ast.Dot;

public class ClassParser extends ClosureParser {
    Parser member = rule().or(def, simple);
    Parser class_body = rule(ClassBody.class).sep("{").option(member)
                            .repeat(rule().sep(";", Token.EOL).option(member))
                            .sep("}");
    Parser defclass = rule(ClassStmnt.class).sep("class").identifier(reserved)
                          .option(rule().sep("extends").identifier(reserved))
                          .ast(class_body);
    public ClassParser() {
        postfix.insertChoice(rule(Dot.class).sep(".").identifier(reserved));
        program.insertChoice(defclass);
    }
}

9.3 實(shí)現(xiàn)eval方法

下一步爱只,需要為新增的抽象語(yǔ)法樹的類添加eval方法皿淋。代碼清單9.6是所需的修改器。

首先恬试,修改器為用于類定義的class語(yǔ)句添加了eval方法窝趣。class語(yǔ)句以class一詞起始,它對(duì)應(yīng)的非終結(jié)符是defclass忘渔,在抽象語(yǔ)法樹中以ClassStmnt(代碼清單9.4)類的形式表現(xiàn)高帖。ClassStmnt類新增的eval方法將創(chuàng)建一個(gè)ClassInfo對(duì)象,向環(huán)境添加由類名與該對(duì)象組成的名值對(duì)畦粮。class語(yǔ)句定義的類的名稱散址。之后,解釋器通過(guò).new從環(huán)境中獲取類的信息宣赔。例如

class Position { 省略 }

這條語(yǔ)句能夠創(chuàng)建一個(gè)ClassInfo對(duì)象预麸,該對(duì)象保存了Stone語(yǔ)言中Position類的定義信息。對(duì)象在創(chuàng)建后儒将,將與類名Position一起添加至環(huán)境中吏祸。

ClassInfo對(duì)象保存了class語(yǔ)句的抽象語(yǔ)法樹。它與保存函數(shù)定義的抽象語(yǔ)法樹的Function類有些相似(第七章的代碼清單7.8)钩蚊。包括本章新增的ClassInfo對(duì)象贡翘,現(xiàn)在的環(huán)境已經(jīng)能夠記錄各種類型的名值對(duì)。表9.1總結(jié)了至今為止介紹過(guò)的所有的值砰逻。

代碼清單9.3 ClassBody.java

package Stone.ast;
import java.util.List;

public class ClassBody extends ASTList {

    public ClassBody(List<ASTree> c) {
        super(c);
    }
}

代碼清單9.4 ClassStmnt.java

package Stone.ast;
import java.util.List;

public class ClassStmnt extends ASTList {

    public ClassStmnt(List<ASTree> c) {
        super(c);
    }

    public String name() {
        return ((ASTLeaf) child(0)).token().getText();
    }

    public String superClass() {
        if (numChildren() < 3)
            return null;
        else
            return ((ASTLeaf) child(1)).token().getText();
    }

    public ClassBody body() {
        return (ClassBody) child(numChildren() - 1);
    }

    public String toStirng() {
        String parent = superClass();
        if (parent == null)
            parent = "*";
        return "(class " + name() + " " + parent + " " + body() + ")";
    }
}

Dot.java

package Stone.ast;
import java.util.List;

public class Dot extends Postfix {

    public Dot(List<ASTree> c) {
        super(c);
    }

    public String name() {
        return ((ASTLeaf) child(0)).token().getText();
    }

    public String toString() {
        return "." + name();
    }
}
file

接下來(lái)需要添加一個(gè)新的eval方法鸣驱,使程序能夠通過(guò)句點(diǎn).進(jìn)行實(shí)現(xiàn)方法調(diào)用與字段訪問(wèn)。相應(yīng)的抽象語(yǔ)法樹是一個(gè)Dot類(代碼清單9.5)蝠咆。Dot類是Postfix的一個(gè)子類踊东。Dot類的eval方法由PrimaryExpr類的evalSubExpr方法直接調(diào)用,PrimaryExpr類的eval方法會(huì)通過(guò)evalsubExpr方法來(lái)獲取調(diào)用結(jié)果

修改器向Dot類添加的eval方法需要兩個(gè)參數(shù)刚操。其中一個(gè)是環(huán)境闸翅,另一個(gè)是句點(diǎn)左側(cè)的計(jì)算結(jié)果。

如果句點(diǎn)右側(cè)是new菊霜,句點(diǎn)表達(dá)式將用于創(chuàng)建一個(gè)對(duì)象坚冀。其中句點(diǎn)左側(cè)是需要?jiǎng)?chuàng)建的類,它的計(jì)算結(jié)果是一個(gè)ClassInfo對(duì)象鉴逞。eval方法將根據(jù)該ClassInfo對(duì)象提供的信息創(chuàng)建對(duì)象并返回遗菠。

如果句點(diǎn)的右側(cè)不是new联喘,該句點(diǎn)表達(dá)式將用于方法調(diào)用或字段訪問(wèn)。句點(diǎn)左側(cè)是需要訪問(wèn)的對(duì)象辙纬,它的計(jì)算結(jié)果是一個(gè)StoneObject對(duì)象。如果這是一個(gè)字段叭喜,解釋器將調(diào)用的read方法獲取字段的值并返回贺拣。

代碼清單9.6中的AssignEx修改器實(shí)現(xiàn)了字段賦值功能。該修改器繼承于BinaryEx捂蕴,同時(shí)譬涡,BinaryEx本身也是一個(gè)修改器(第6章代碼清單6.3)。AssignEx修改器將修改BinaryExpr類啥辨。AssignEx修改器覆蓋了由BinaryEx修改器添加的computeAssign方法涡匀,使字段的賦值功能得以實(shí)現(xiàn)

經(jīng)過(guò)AssignEx修改器修改的computeAssign方法將在賦值運(yùn)算的左側(cè)為一個(gè)字段時(shí)調(diào)用stoneobject的write方法,執(zhí)行賦值操作溉知。如果不是陨瘩,它將通過(guò)super調(diào)用原先的computeAssign方法

在為字段賦值時(shí)必須注意的是羡铲,賦值運(yùn)算的左側(cè)并不一定總是單純的字段名稱瞳抓。例如,字段可以通過(guò)下面的方式表現(xiàn)

table.get().next.x = 3

解釋器將首先調(diào)用變量table所指對(duì)象的get方法军洼,再將返回對(duì)象中next字段指向的對(duì)象包含的字段x賦值為3玫荣。其中甚淡,僅有.x將計(jì)算運(yùn)算符的左值并賦值,table.get().next仍以通常方式計(jì)算最右側(cè)的值捅厂。computeAssign方法通過(guò)內(nèi)部的evalsubExpr方法執(zhí)行這一計(jì)算贯卦。賦值給變量t的返回值同時(shí)也是上面例子中table.get().next的右值計(jì)算結(jié)果。

代碼清單9.6 ClassEvaluator.java

package chap9;
import java.util.List;
import Stone.StoneException;
import Stone.ast.*;
import chap6.BasicEvaluator.ASTreeEx;
import chap6.BasicEvaluator;
import chap6.Environment;
import chap7.FuncEvaluator;
import chap7.FuncEvaluator.EnvEx;
import chap7.FuncEvaluator.PrimaryEx;
import chap7.NestedEnv;
import chap9.StoneObject.AccessException;
import javassist.gluonj.*;

@Require(FuncEvaluator.class)
@Reviser public class ClassEvaluator {
    @Reviser public static class ClassStmntEx extends ClassStmnt {
        public ClassStmntEx(List<ASTree> c) {
            super(c);
        }

        public Object eval(Environment env) {
            ClassInfo ci = new ClassInfo(this, env);
            ((EnvEx) env).put(name(), ci);
            return name();
        }
    }

    @Reviser public static class ClassBodyEx extends ClassBody {
        public ClassBodyEx(List<ASTree> c) {
            super(c);
        }

        public Object eval(Environment env) {
            for (ASTree t : this)
                ((ASTreeEx) t).eval(env);
            return null;
        }
        
        @Reviser public static class DotEx extends Dot {
            public DotEx(List<ASTree> c) {
                super(c);
            }
            
            public Object eval(Environment env,Object value) {
                String member = name();
                if (value instanceof ClassInfo) {
                    if ("new".equals(member)) {
                        ClassInfo ci = (ClassInfo)value;
                        NestedEnv e = new NestedEnv(ci.environment);
                        StoneObject so = new StoneObject(e);
                        e.putNew("this", so);
                        initObject(ci,e);
                        return so;
                    }
                } else if (value instanceof StoneObject) {
                    try {
                        return ((StoneObject)value).read(member);
                    } catch (AccessException e) {}
                }
                throw new StoneException("bad member access: " + member,this);
            }
            
            protected void initObject(ClassInfo ci,Environment env) {
                if (ci.superClass() != null)
                    initObject(ci.superClass(),env);
                ((ClassBodyEx)ci.body()).eval(env);
            }
        }
        @Reviser public static class AssignEx extends BasicEvaluator.BinaryEx {
            public AssignEx(List<ASTree> c) {
                super(c);
            }
            
            protected Object computeAssign(Environment env,Object rvalue) {
                ASTree le = left();
                if (le instanceof PrimaryExpr) {
                    PrimaryEx p = (PrimaryEx) le;
                    if (p.hasPostfix(0) && p.postfix(0) instanceof Dot) {
                        Object t = ((PrimaryEx)le).evalSubExpr(env, 1);
                        if (t instanceof StoneObject)
                            return setField((StoneObject)t,(Dot)p.postfix(0),rvalue);
                    }
                }
                return super.computeAssign(env, rvalue);
            }
            
            protected Object setField(StoneObject obj,Dot expr,Object rvalue) {
                String name = expr.name();
                try {
                    obj.write(name,rvalue);
                    return rvalue;
                } catch (AccessException e) {
                    throw new StoneException("bad member access " + location() + ": " + name);
                }
            }
        }
    }
}

代碼清單9.7 ClassInfo.java

package chap9;
import Stone.StoneException;
import Stone.ast.ClassBody;
import Stone.ast.ClassStmnt;
import chap6.Environment;

public class ClassInfo {
    protected ClassStmnt definition;
    protected Environment environment;
    protected ClassInfo superClass;

    public ClassInfo(ClassStmnt cs, Environment env) {
        definition = cs;
        environment = env;
        Object obj = env.get(cs.superClass());
        if (obj == null)
            superClass = null;
        else if (obj instanceof ClassInfo)
            superClass = (ClassInfo) obj;
        else
            throw new StoneException("unkonw super class: " + cs.superClass(), cs);
    }
    
    public String name() {
        return definition.name();
    }
    
    public ClassInfo superClass() {
        return superClass;
    }
    
    public ClassBody body() {
        return definition.body();
    }
    
    public Environment environment() {
        return environment;
    }
    
    public String toString() {
        return "<class " + name() + ">";
    }
}

代碼清單9.8 StoneObject.java

package chap9;
import chap6.Environment;
import chap7.FuncEvaluator.EnvEx;

public class StoneObject {
    public static class AccessException extends Exception {
    }

    protected Environment env;

    public StoneObject(Environment e) {
        env = e;
    }

    public String toString() {
        return "<object:" + hashCode() + ">";
    }

    public Object read(String member) throws AccessException {
        return getEnv(member).get(member);
    }

    public void write(String member, Object value) throws AccessException {
        ((EnvEx) getEnv(member)).putNew(member, value);
    }

    protected Environment getEnv(String member) throws AccessException {
        Environment e = ((EnvEx) env).where(member);
        if (e != null && e == env)
            return e;
        else
            throw new AccessException();
    }
}

9.4 通過(guò)閉包表示對(duì)象

從實(shí)現(xiàn)的角度來(lái)看焙贷,如何設(shè)計(jì)StoneObject對(duì)象的內(nèi)部結(jié)構(gòu)才是最重要的撵割。也就是說(shuō),如何通過(guò)Java語(yǔ)言的對(duì)象來(lái)表現(xiàn)Stone語(yǔ)言的對(duì)象盈厘。其實(shí)睁枕,實(shí)現(xiàn)的方式多種多樣,我們將利用環(huán)境能夠保存字段值的特性來(lái)表示對(duì)象沸手。

StoneObject對(duì)象主要應(yīng)保存Stone語(yǔ)言中對(duì)象包含的字段值外遇,可以說(shuō)它是字段名稱與字段值的對(duì)應(yīng)關(guān)系表。從這個(gè)角度來(lái)看契吉,環(huán)境作為變量名稱與變量值的對(duì)應(yīng)關(guān)系表跳仿,與對(duì)象的作用非常類似。

如果將對(duì)象視作一種環(huán)境捐晶,就很容易實(shí)現(xiàn)對(duì)該對(duì)象自身(也就是Java語(yǔ)言中this指代的對(duì)象)的方法調(diào)用與字段訪問(wèn)菲语。方法調(diào)用與字段訪問(wèn)可以通過(guò)this.x實(shí)現(xiàn)妄辩,其中,指代自身的this.能夠省略山上。下面是一個(gè)例子眼耀。

class Positon {
    x = y = 0
    def move(nx,ny) {
        x = ny;y = ny
    }
}

move方法內(nèi)的x乍看是一個(gè)局部變量,其實(shí)它是this.x的省略形式佩憾,表示x字段哮伟。這類x的實(shí)現(xiàn)比較麻煩。如果將move方法的定義視作函數(shù)定義妄帘,x與y都屬于自由變量(自由變量指的是函數(shù)參數(shù)及局部變量以外的函數(shù))楞黄。參數(shù)nx與ny則是約束變量。

如果方法內(nèi)部存在x這樣的自由變量抡驼,該變量就必須指向(綁定)在方法外部定義的字段鬼廓。這與閉包的機(jī)制類似。例如致盟,下面的函數(shù)position將返回一個(gè)閉包碎税。

def position () {
    x = y = 0
    fun (nx,ny) {
        x = ny;y = ny
    }
}

此時(shí),position函數(shù)的局部變量x將賦值給返回的閉包中的變量x(與x綁定)勾邦。對(duì)比兩者即可發(fā)現(xiàn)蚣录,閉包與方法都會(huì)將內(nèi)部的變量名與外部的變量(字段)綁定。

在通過(guò).new創(chuàng)建新的StoneObject對(duì)象時(shí)眷篇,解釋器將首先創(chuàng)建新的環(huán)境萎河。StoneObject對(duì)象將保存該環(huán)境,并向該環(huán)境添加由名稱this與自身組成的鍵值對(duì)蕉饼。

之后虐杯,解釋器將借助該環(huán)境執(zhí)行類定義中由大括號(hào){}括起的主體部分。與執(zhí)行函數(shù)體時(shí)一樣昧港,只需調(diào)用表示主體的調(diào)用表示主體的抽象語(yǔ)法樹的eval方法即可完成這一操作擎椰。這對(duì)應(yīng)于Java等語(yǔ)言中的構(gòu)造函數(shù)調(diào)用。主體部分執(zhí)行后创肥,類定義中出現(xiàn)的字段名與方法名以及相應(yīng)的值都將被環(huán)境記錄达舒。

在執(zhí)行過(guò)程中,如果需要為首次出現(xiàn)的變量賦值叹侄,解釋器將像環(huán)境添加有該變量的名稱與值組成的名值對(duì)巩搏。

file

9.5 運(yùn)行包含類的程序

至此,Stone語(yǔ)言已經(jīng)可以支持類與對(duì)象的使用趾代。與之前一樣贯底,最后將要介紹的是解釋器主體程序與相應(yīng)的啟動(dòng)程序。參見代碼清單9.9與代碼清單9.10

代碼清單9.9 ClassInterperter.java

package chap9;
import Stone.ClassParser;
import Stone.ParseException;
import chap6.BasicInterpreter;
import chap7.NestedEnv;
import chap8.Natives;

public class ClassInterpreter extends BasicInterpreter {
    public static void main(String[] args) throws ParseException {
        run(new ClassParser(), new Natives().environment(new NestedEnv()));
    }
}

代碼清單9.10 ClassRunner.java

package chap9;
import chap7.ClosureEvaluator;
import chap8.NativeEvaluator;
import javassist.gluonj.util.Loader;

public class ClassRunner {
    public static void main(String[] args) throws Throwable {
        Loader.run(ClassInterpreter.class, args, ClassEvaluator.class,NativeEvaluator.class,ClosureEvaluator.class);
    }
}
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末撒强,一起剝皮案震驚了整個(gè)濱河市禽捆,隨后出現(xiàn)的幾起案子笙什,更是在濱河造成了極大的恐慌,老刑警劉巖胚想,帶你破解...
    沈念sama閱讀 218,546評(píng)論 6 507
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件琐凭,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡顿仇,警方通過(guò)查閱死者的電腦和手機(jī)淘正,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,224評(píng)論 3 395
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)臼闻,“玉大人,你說(shuō)我怎么就攤上這事囤采∈瞿牛” “怎么了?”我有些...
    開封第一講書人閱讀 164,911評(píng)論 0 354
  • 文/不壞的土叔 我叫張陵蕉毯,是天一觀的道長(zhǎng)乓搬。 經(jīng)常有香客問(wèn)我,道長(zhǎng)代虾,這世上最難降的妖魔是什么进肯? 我笑而不...
    開封第一講書人閱讀 58,737評(píng)論 1 294
  • 正文 為了忘掉前任,我火速辦了婚禮棉磨,結(jié)果婚禮上江掩,老公的妹妹穿的比我還像新娘。我一直安慰自己乘瓤,他們只是感情好环形,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,753評(píng)論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著衙傀,像睡著了一般抬吟。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上统抬,一...
    開封第一講書人閱讀 51,598評(píng)論 1 305
  • 那天火本,我揣著相機(jī)與錄音,去河邊找鬼聪建。 笑死钙畔,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的妆偏。 我是一名探鬼主播刃鳄,決...
    沈念sama閱讀 40,338評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼钱骂!你這毒婦竟也來(lái)了叔锐?” 一聲冷哼從身側(cè)響起挪鹏,我...
    開封第一講書人閱讀 39,249評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎愉烙,沒(méi)想到半個(gè)月后讨盒,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,696評(píng)論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡步责,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,888評(píng)論 3 336
  • 正文 我和宋清朗相戀三年返顺,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片蔓肯。...
    茶點(diǎn)故事閱讀 40,013評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡遂鹊,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出蔗包,到底是詐尸還是另有隱情秉扑,我是刑警寧澤,帶...
    沈念sama閱讀 35,731評(píng)論 5 346
  • 正文 年R本政府宣布调限,位于F島的核電站舟陆,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏耻矮。R本人自食惡果不足惜秦躯,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,348評(píng)論 3 330
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望裆装。 院中可真熱鬧踱承,春花似錦、人聲如沸米母。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,929評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)铁瞒。三九已至妙色,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間慧耍,已是汗流浹背身辨。 一陣腳步聲響...
    開封第一講書人閱讀 33,048評(píng)論 1 270
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留芍碧,地道東北人煌珊。 一個(gè)月前我還...
    沈念sama閱讀 48,203評(píng)論 3 370
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像泌豆,于是被迫代替她去往敵國(guó)和親定庵。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,960評(píng)論 2 355

推薦閱讀更多精彩內(nèi)容