用C语言实现SGF格式围棋棋谱解析器

  这是本人(liigo)独立实现的SGF格式围棋棋谱文件解析器,本文介绍其实现细节。网络上肯定可以找到完善的开源的SGF解析器,这是毋庸置疑的,我不直接使用它们,也不参考它们的实现代码,而是自己独立编码实现,是有原因的,因为我想自己重复发明轮子,并且认为这样更有助于提高我的编码能力。(关于我的“一定要学会重复发明轮子”的不成熟的论调,今后我将会专门撰文表述。)

  我(liigo)开发的这个SGF解析器,采用基于事件的简单API,类似于XML解析器中的SAX(Simple API for XML)。这种解析器的核心是:由用户事先提供一系列回调函数,解析器在解析的过程中,依次调用相关的回调函数并传入相应参数,用户程序在回调函数中做出相应的处理。此类解析器属于轻量级的解析器,解析速度快,占用内存少,结构清晰易于实现,只是相对来说不如基于DOM的解析器方便使用。

  SGF格式,Smart Game Format,被设计用来记录多种游戏类棋谱的通用格式,在围棋领域被发扬光大,是用于描述围棋棋谱的最重要也最通用的形式。它是纯文本的、基于树(TREE)的结构,便于识别、存储和传输。其格式简洁实用,也非常易于编程解析。SGF格式官方规范网址为:http://www.red-bean.com/sgf/。(说到围棋棋谱,不得不赞叹一下,它只需用一幅图就可以完整还原一盘棋从始至终的风云变幻;作为对比,象棋一幅图只能描述对弈中某一时刻的场景。)

  SGF的主要结构由树(GameTree)、节点序列(Sequence)、节点(Node)、属性(Property)等组成。其中“属性”为最重要的基本单位,它由属性标识(PropIdent)和属性值(PropValue)组成。由分号“;”分隔的多个属性,称为节点。多个节点顺序排列称为节点序列。由括号“(”“)”括起来的节点序列,称为树,树中可包含子树。SGF的EBNF定义如下(参见http://www.red-bean.com/sgf/sgf4.html#ebnf-def):

Collection = GameTree { GameTree } GameTree = "(" Sequence { GameTree } ")" Sequence = Node { Node } Node = ";" { Property } Property = PropIdent PropValue { PropValue } PropIdent = UcLetter { UcLetter } PropValue = "[" CValueType "]" CValueType = (ValueType | Compose) ValueType = (None | Number | Real | Double | Color | SimpleText | Text | Point | Move | Stone)

  以下是一个简单的有一定代表性的SGF文本,先让大家有一个感性认识:

(;FF[4]GM[1]SZ[19]FG[257:Figure 1]PM[1] PB[Takemiya Masaki]BR[9 dan]PW[Cho Chikun] WR[9 dan]RE[W+Resign]KM[5.5]TM[28800]DT[1996-10-18,19] EV[21st Meijin]RO[2 (final)]SO[Go World #78]US[Arno Hollosi] ;B[pd];W[dp];B[pp];W[dd];B[pj];W[nc];B[oe];W[qc];B[pc];W[qd] (;B[qf];W[rf];B[rg];W[re];B[qg];W[pb];B[ob];W[qb] (;B[mp];W[fq];B[ci];W[cg];B[dl];W[cn];B[qo];W[ec];B[jp];W[jd] ;B[ei];W[eg];B[kk]LB[qq:a][dj:b][ck:c][qp:d]N[Figure 1] ;W[me]FG[257:Figure 2];B[kf];W[ke];B[lf];W[jf];B[jg] (;W[mf];B[if];W[je];B[ig];W[mg];B[mj];W[mq];B[lq];W[nq] (;B[lr];W[qq];B[pq];W[pr];B[rq];W[rr];B[rp];W[oq];B[mr];W[oo];B[mn] (;W[nr];B[qp]LB[kd:a][kh:b]N[Figure 2] ;W[pk]FG[257:Figure 3];B[pm];W[oj];B[ok];W[qr];B[os];W[ol];B[nk];W[qj] ;B[pi];W[pl];B[qm];W[ns];B[sr];W[om];B[op];W[qi];B[oi] (;W[rl];B[qh];W[rm];B[rn];W[ri];B[ql];W[qk];B[sm];W[sk];B[sh];W[og] ;B[oh];W[np];B[no];W[mm];B[nn];W[lp];B[kp];W[lo];B[ln];W[ko];B[mo] ;W[jo];B[km]N[Figure 3]) (;W[ql]VW[ja:ss]FG[257:Dia. 6]MN[1];B[rm];W[ph];B[oh];W[pg];B[og];W[pf] ;B[qh];W[qe];B[sh];W[of];B[sj]TR[oe][pd][pc][ob]LB[pe:a][sg:b][si:c] N[Diagram 6])) (;W[no]VW[jj:ss]FG[257:Dia. 5]MN[1];B[pn]N[Diagram 5])) (;B[pr]FG[257:Dia. 4]MN[1];W[kq];B[lp];W[lr];B[jq];W[jr];B[kp];W[kr];B[ir] ;W[hr]LB[is:a][js:b][or:c]N[Diagram 4])) (;W[if]FG[257:Dia. 3]MN[1];B[mf];W[ig];B[jh]LB[ki:a]N[Diagram 3])) (;W[oc]VW[aa:sk]FG[257:Dia. 2]MN[1];B[md];W[mc];B[ld]N[Diagram 2])) (;B[qe]VW[aa:sj]FG[257:Dia. 1]MN[1];W[re];B[qf];W[rf];B[qg];W[pb];B[ob] ;W[qb]LB[rg:a]N[Diagram 1]))

  熟悉编写文本解析器的程序员朋友应该都清楚,根据EBNF定义,编写对应的解析器,是相当简单和直观的,貌似只是一项翻译性的工作。本人实现SGF解析器,再次印证了这个观点,大部分情况下,我只是按部就班地将EBNF翻译为C语言代码而已,呵呵。

  我首先设计了“SGFParseContext”结构,用于保存解析器工作期间的相关数据:

typedef struct _tagSGFParseContext { void* pUserData; int treeIndex; PFN_ON_TREE pfnOnTree; PFN_ON_TREE_END pfnOnTreeEnd; PFN_ON_NODE pfnOnNode; PFN_ON_NODE_END pfnOnNodeEnd; PFN_ON_PROPERTY pfnOnProperty; char idBuffer[16]; char* valueBuffer; int valueBufferSize; } SGFParseContext;

  相应的还有初始化和清理SGFParseContext结构的函数,initSGFParseContext, cleanupSGFParseContext,皆不是本解析器的关键,略过不提。

  接着我(liigo)设计了五个回调函数的函数原形:

typedef void (*PFN_ON_TREE) (SGFParseContext* pContext, const char* szTreeHeader, int treeIndex); typedef void (*PFN_ON_TREE_END) (SGFParseContext* pContext, int treeIndex); typedef void (*PFN_ON_NODE) (SGFParseContext* pContext, const char* szNodeHeader); typedef void (*PFN_ON_NODE_END) (SGFParseContext* pContext); typedef void (*PFN_ON_PROPERTY) (SGFParseContext* pContext, const char* szID, const char* szValue);

  这五个回调函数,将分别在解析器解析到“树开始”“树结束”“节点开始”“节点结束”“遇到属性”时,由解析器调用。解析器调用每个回调函数时,都会传入必需的参数,供回调函数即时取用。

  下面正式开始解析工作。整个解析器被分为 parseProperty, parseNode, parseNodeSequence, parseGameTree, parseSGF 几大部分顺序解析,属于至底向上的分析实现模式。这几大部分,也分别对应着SGF的EBNF定义中的某一项。所有解析函数都接收参数 const char* szCollection, int fromPos,之前的解析函数将决定后续解析函数的起始解析位置。

  第一步,解析属性(parseProperty)。此处关键的是要定位到属性值(szValue)开始和结束符号“[”和“],两者之间的是属性值,“[”之前的则是属性标识(szID)。由于[和]之间可能存在转义字符“\”,不能简单地搜索字符“]”,必须花相当篇幅的代码处理转义字符(我用局部变量in_escape记录转义状态并进行分别处理)。此外要为提取出的属性标识和属性值分配足够的存储空间,以便传递到用户回调函数,前者不会太长使用静态分配,后者变长则使用动态分配(同时自动预分配存储空间,缓存,避免频繁申请内存)。代码如下:

//Property: id[value] int parseProperty(SGFParseContext* pContext, const char* szCollection, int fromPos) { const char* szFromPos; int lindex; int nIDBufferSize = sizeof(pContext->idBuffer) - 1; assert(szCollection && fromPos >= 0); szFromPos = szCollection + fromPos; lindex = findchar(szFromPos, -1, '['); assert(lindex > 0 && lindex < nIDBufferSize); if(lindex > 0 && lindex < nIDBufferSize) { memcpy(pContext->idBuffer, szFromPos, lindex); pContext->idBuffer[lindex] = '\0'; if(isTextPropertyID(pContext->idBuffer)) { //parse the text or simple-text value, consider the '\' escape character const char* s = szFromPos + lindex + 1; char c; int in_escape = 0; int valuelen = 0; getEnoughBuffer(pContext, 1024); pContext->valueBuffer[0] = '\0'; while(1) { c = *s; assert(c); if(!in_escape) { if(c == '\\') { in_escape = 1; } else if(c == ']') { break; } else { getEnoughBuffer(pContext, valuelen + 1); pContext->valueBuffer[valuelen++] = c; } } else { //ignore the newline after '\' if(c != '\r' && c != '\n') { getEnoughBuffer(pContext, valuelen + 1); pContext->valueBuffer[valuelen++] = c; } else { char nc = *(s+1); if(nc) { if((c=='\r' && nc=='\n') || (c=='\n' && nc=='\r')) s++; } } in_escape = 0; } s++; } getEnoughBuffer(pContext, valuelen + 1); pContext->valueBuffer[valuelen] = '\0'; if(pContext->pfnOnProperty) pContext->pfnOnProperty(pContext, pContext->idBuffer, pContext->valueBuffer); return (s - szCollection + 1); } else { int rindex = findchar(szFromPos, -1, ']'); int nNeedBufferSize = rindex - lindex - 1; assert(rindex >= 0); getEnoughBuffer(pContext, nNeedBufferSize); memcpy(pContext->valueBuffer, szFromPos + lindex + 1, nNeedBufferSize); pContext->valueBuffer[nNeedBufferSize] = '\0'; if(pContext->pfnOnProperty) pContext->pfnOnProperty(pContext, pContext->idBuffer, pContext->valueBuffer); return (fromPos + rindex + 1); } } return -1; }

  第二步,解析节点(parseNode)。分号“;”跟后面N个属性,一个while循环调用parseProperty()逐个解析属性即可:

//Node: ; {property} int parseNode(SGFParseContext* pContext, const char* szCollection, int fromPos) { const char* szFromPos = szCollection + fromPos; assert(fromPos >= 0); //assert(szFromPos[0] == ';'); if(pContext->pfnOnNode) pContext->pfnOnNode(pContext, szFromPos); if(szFromPos[0] == ';') { fromPos++; szFromPos++; } while(1) { fromPos += skipSpaceChars(szFromPos, NULL); if(szCollection[fromPos] == '\0' || findchar(";)(", -1, szCollection[fromPos]) >= 0) break; fromPos = parseProperty(pContext, szCollection, fromPos); szFromPos = szCollection + fromPos; } return fromPos; }

  第三步,解析节点序列(parseNodeSequence)。节点的顺序排列,至少有一个节点,后面可能还有0个或多个节点。仍然是一个while循环搞定:

//NodeSequence: node{node} int parseNodeSequence(SGFParseContext* pContext, const char* szCollection, int fromPos) { const char* szFromPos = szCollection + fromPos; assert(fromPos >= 0); //assert(szFromPos[0] == ';'); while(1) { fromPos = parseNode(pContext, szCollection, fromPos); fromPos += skipSpaceChars(szFromPos, NULL); szFromPos = szCollection + fromPos; if(szFromPos[0] != ';') { if(pContext->pfnOnNodeEnd) pContext->pfnOnNodeEnd(pContext); break; } } return fromPos; }

  第四步,解析树(parseGameTree)。树是一个嵌套结构,最外层是一对括号“(”“)”,里面是N个节点序列或N个嵌套的子树。仍然用一个while循环搞定,遇到“(”则递归调用parseGameTree()解析树或其子树,否则调用parseNodeSequence()解析节点序列。代码如下:

//GameTree: ( {[NodeSequence]|[GameTree]} ) //old GameTree: ( NodeSequence {GameTree} ) int parseGameTree(SGFParseContext* pContext, const char* szCollection, int fromPos) { char c; const char* szFromPos = szCollection + fromPos; assert(fromPos >= 0); assert(szFromPos[0] == '('); pContext->treeIndex++; if(pContext->pfnOnTree) pContext->pfnOnTree(pContext, szFromPos, pContext->treeIndex); fromPos++; szFromPos++; fromPos += skipSpaceChars(szFromPos, NULL); c = szCollection[fromPos]; while(1) { if(c == '(') fromPos = parseGameTree(pContext, szCollection, fromPos); else fromPos = parseNodeSequence(pContext, szCollection, fromPos); szFromPos = szCollection + fromPos; fromPos += skipSpaceChars(szFromPos, NULL); c = szCollection[fromPos]; if(c == ')') { if(pContext->pfnOnTreeEnd) pContext->pfnOnTreeEnd(pContext, pContext->treeIndex); pContext->treeIndex--; break; } } return (fromPos + 1); }

  第五步,最后一步了,解析整个SGF文本内容(parseSGF)。这是对外公开的核心接口。N个树的顺序排列,好办呀,循环调用parseGameTree()顺序解析各个树不就OK了?代码如下:

//SGFCollection: GameTree {GameTree} int parseSGF(SGFParseContext* pContext, const char* szCollection, int fromPos) { const char* szFromPos = szCollection + fromPos; assert(fromPos >= 0); assert(szFromPos[0] == '('); pContext->treeIndex = -1; while(1) { fromPos = parseGameTree(pContext, szCollection, fromPos); fromPos += skipSpaceChars(szFromPos, NULL); szFromPos = szCollection + fromPos; if(szFromPos[0] != '(') break; } return fromPos; }

  测试代码:

int main(int argc, char *argv[]) { char* s; int x; SGFParseContext Context; //initSGFParseContext(&Context, onTree, onTreeEnd, onNode, onNodeEnd, onProperty, NULL); initSGFParseContext(&Context, onTree2, onTreeEnd2, onNode2, onNodeEnd2, onProperty2, NULL); //test parse property: { s = "AB[cdef]X[xyz]"; printf("\ntest parse property: ----- \n"); x = parseProperty(&Context, s, 0); x = parseProperty(&Context, s, 8); s = "C[ab\\]cd]"; x = parseProperty(&Context, s, 0); } //test parse node: { s = ";A[a]BB[bb]C[]"; printf("\ntest parse node: ----- \n"); x = parseNode(&Context, s, 0); s = ";A[a];BB[bb]C[]"; x = parseNode(&Context, s, 0); x = parseNodeSequence(&Context, s, 0); } //test parse tree: { printf("\ntest parse tree: ----- \n"); s = "(;A[a](;C[c](X[x])Z[z]);D[d](;E[e](F[ff])))"; x = parseGameTree(&Context, s, 0); } #if 1 //parse real sgf file: { int len = 0; void* data = NULL; FILE* pfile = fopen("d:\\x.txt", "r"); printf("\n---------- test parse real sgf file: -------- \n"); if(pfile) { fseek(pfile, 0, SEEK_END); len = ftell(pfile); assert(len > 0); fseek(pfile, 0, SEEK_SET); data = malloc(len); assert(data); fread(data, 1, len, pfile); parseSGF(&Context, data, 0); fclose(pfile); pfile = NULL; } } #endif { char c; printf("\n----- any key to exit: ----- \n"); fflush(stdout); scanf("%c", &c); } }

  总结:整个SGF解析器结构比较清晰,只要按照EBNF定义,按部就班地逐步处理即可,不是特别复杂。但由于牵涉到文本、指针、递归,有许多细节需要注意。各位朋友不妨评估一下,自己需要花费多久可以写出类似这样一个SGF解析器?如果时间充裕,也不妨真的动手写一下,看看是否眼高手低呢?所谓的“重复发明轮子”,并非绝对的毫无意义,至少可以锻炼我的动手能力。

  另外,有一个设计上的取舍,不知是较好还是较坏。所有的回调函数,目前都有一个 SGFParseContext* pContext ,而此前相同位置的参数是 void* pUserData。是后来考虑到回调函数可能需要访问SGFParseContext中的相关数据(如在PFN_ON_NODE中读取treeIndex),为了方便用户使用才引入pContext参数(用户也可以通过pUserData自行传入pContext,终究是多了一步)。目前的做法,似乎暴露了解析器内部结构(SGFParseContext),又似乎增强了回调函数的稳定性和扩展性(即使不改变函数原形也能通过pContext提供额外参数)。

  虽然这个SGF解析器已应用到开源软件“M8围棋谱”(http://code.google.com/p/m8weiqipu/)中,并初步达到了实用目的,但并不能保证该解析器已达到工业强度,其实有不少情况尚未测试到,必然会有疏忽错漏之处,诚请各位朋友批评指正。

  另注,考虑到与现有SGF格式文件的兼容性,对SGF规范中的EBNF稍做了一定扩展。

  完整源代码请参见:
http://code.google.com/p/m8weiqipu/source/browse/trunk/sgf.h
http://code.google.com/p/m8weiqipu/source/browse/trunk/sgf.c

转载于:https://www.cnblogs.com/fortest/archive/2009/09/06/2056928.html

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/463299.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

各种***方式说明

使消息保密的技术和科学叫做密码编码学&#xff08;cryptography&#xff09;。密码编码学是密码体制的设计学&#xff0c;即怎样编码&#xff0c;采用什么样的密码体制以保证信息被安全地加密。从事此行业的人员叫做密码编码者&#xff08;cryptographer&#xff09;。 与之相…

C++内存分配方式详解——堆、栈、自由存储区、全局/静态存储区和常量存储区

C内存分配方式详解——堆、栈、自由存储区、全局/静态存储区和常量存储区栈&#xff0c;就是那些由编译器在需要的时候分配&#xff0c;在不需要的时候自动清除的变量的存储区。里面的变量通常是局部变量、函数参数等。在一个进程中&#xff0c;位于用户虚拟地址空间顶部的是用…

Uoj 441 保卫王国

Uoj 441 保卫王国 动态 \(dp\) .今天才来写这个题.设 \(f[u][0/1]\) 表示子树 \(u\) 中不选/选 \(u\) 时的最小权值和,显然有:\(f[u][0]\sum f[v][1] ,f[u][1]w[u]\sum \min(f[v][0],f[v][1])​\) .现在要资瓷修改 \(x\) 的点权 \(w[x]\) ,容易发现修改后只会影响 \(x\) 到根节…

行存和列存的区别

写入&#xff1a; 行存储的写入是一次完成&#xff0c;数据的完整性因此可以确定。 列存储需要把一行记录拆分成单列保存&#xff0c;写入次数明显比行存储多。 行存储在写入上占有很大的优势 数据修改&#xff1a; 行存储是在指定位置写入一次&#xff0c;列存储是将磁盘定位…

javascript中的命名规则和方法

javascript变量名需要遵守两条简单的规则&#xff1a;1、第一个字符必须是字母、下划线&#xff08;_&#xff09; 或美圆符号&#xff08;$&#xff09;。2、余下的字符可以是下划线、美圆符号或任何字母或数字字符。 命名方法&#xff1a;1、Camel标记法——首字母是小写的&a…

jquery插件开发导读

最近发现项目中有些js代码可以重用&#xff0c;但是不知道怎么样组织&#xff0c;在网上调研后&#xff0c;发现jquery插件是一种很好的组织方式&#xff0c;而且项目也采用了jquery框架&#xff0c;所以花了点时间学习jquery插件开发&#xff0c;并且动手将部分项目代码转成jq…

C语言缓冲文件系统和非缓冲文件系统

C 语言所使用的磁盘文件系统有两大类&#xff1a;一类称为缓冲文件系统&#xff0c;又称为标准文件系统&#xff1b;另一类称为非缓冲文件系统。缓冲文件系统的特点是系统自动地在内存区为每一个正在使用的文件开辟一个缓冲区。从磁盘向内存读入数据时&#xff0c;则一次从磁盘…

Swift 里集合类型协议的关系

&#xfffc; &#xfffc; Sequence A type that provides sequential, iterated access to its elements. 是最基础的协议&#xff0c;可以通过迭代来获取它的元素。 有两个关联类型&#xff1a; /// A type representing the sequences elements.associatedtype Element//…

ASP.NET 实现登录界面(生成验证码)

这周末也没干啥&#xff0c;真正开始ASP&#xff0c;做了个学籍管理系统的登录界面&#xff0c;登录界面主要包括用户名、密码、验证码&#xff0c;界面字体用了<font size"5" color"blue" font-family:"华文琥珀";></font>改变字体…

C/C++语言void及void指针深层探索

1.概述  许多初学者对C/C语言中的void及void指针类型不甚理解&#xff0c;因此在使用上出现了一些错误。本文将对void关键字的深刻含义进行解说&#xff0c;并详述void及void指针类型的使用方法与技巧。2.void的含义  void的字面意思是“无类型”&#xff0c;void *则为“无…

多域资源整合之基础准备--DNS配置

由于公司的战略调整,需要整合集团内的资源,当然也也包含IT资源,我们需要评估多家公司的IT架构统一,顺利的合并到总集团的IT架构里,这也就产生一个多域的整合的一个案例,在此分享给大家,希望对大家有所帮助&#xff01;篇幅较长&#xff0c;让我们慢慢细化&#xff01; 在这次的…

python__实参前加*和**的(拆包)功能

print(--------元组打散--------) tup(1,2,3) print(tup) print(*tup) print(--------列表打散--------) list[1,2,3] print(list) print(*list) print(--------字符串打散------) strhello print(str) print(*str) print(--------字典打散--------) def func_dic(name,age):pr…

内存淘汰机制 LRU cache

LRU cache机制的目的是为了减少频繁查找的开销&#xff0c;包括磁盘IO等。 (1)、如果LRU中存在则从LRU缓存中查找&#xff0c;查找到了之后放到容器(list)的最前面 (2)、如果缓存中没有&#xff0c;则从其地方(数据库、磁盘、文件)中读取&#xff0c;读取之后放到容器的最前面…

字符与字符串操作——Windows via C/C++

在最新版的Windows, Windows Vista&#xff0c;它应该支持Unicode 5.0。在编程中对字符与字符串的操作是很普通的&#xff0c;为新的系统写代码&#xff0c;尽可能使用Unicode&#xff0c;它提供了更好的性能&#xff0c;以及可以进行区域化。而且与COM及.Net框架互操作时也有帮…

cmd常用命令总结

CMD命令&#xff1a;开始&#xff0d;>运行&#xff0d;>键入cmd或command&#xff08;在命令行里可以看到系统版本、文件系统版本&#xff09;chcp 修改默认字符集chcp 936默认中文chcp 650011. appwiz.cpl&#xff1a;程序和功能 2. calc&#xff1a;启动计算器 5. ch…

生产者-消费者模式的实现

// 生产者-消费者模式 无锁队列 get()时&#xff0c;如果deque里面没有元素了&#xff0c;则会一直阻塞,还有待改进的空间 template <class T> class BlockingQueue { public:explicit BlockingQueue() : shutdown_(false) {}~BlockingQueue() {}void put(const T&a…

《面向模式的软件体系结构2-用于并发和网络化对象模式》读书笔记(13)--- 线程安全接口和双检查加锁优化...

4.3线程安全接口&#xff08;Thread-Safe Interface&#xff09;1.问题多线程组件通常包括多个可被公共访问的接口方法以及可以改变组件状态的私有方法。为了避免出现竞争条件&#xff0c;可以使用一个组件内部的锁对访问其状态的接口方法调用串行化。尽管当每个方法都自包容的…

C++流的基本概念

在C语言中&#xff0c;数据的输入和输出&#xff08;简写为I/O&#xff09;包括对标准输入设备键盘和标准输出设备显示器、对在外存磁盘上的文件和对内存中指定的字符串存储空间&#xff08;当然可用该空间存储任何信息&#xff09;进行输入输出这三个方面。对标准输入设备和标…

天哪,flash cs4可以使动态文本旋转了

天哪&#xff0c;flash cs4可以使动态文本旋转了. 只要使用rotationZ属性即可。 这可是解决了一个跨世纪的问题&#xff0c;爽爽爽。 啊哈哈哈。 这是测试源代码。 var rTextField:TextFieldnewTextField();var textFormat:TextFormatnewTextFormat();rTextField.text"…

Java编译过程(传送门)

我不是要做一门编程语言&#xff0c;了解这个对我现在的工作也没什么帮助&#xff0c;纯粹好奇而已。 传送门转载于:https://www.cnblogs.com/flying607/p/10481239.html