第7章

通过特定应用程序代码解耦语法

到目前为止,我们已经知道了怎么用ANTLR的语法来定义语言了,接下来我们要给我们的语法注入一些新的元素了。就语法本身而言,其用处并不大,因为它只能告诉我们一个用户输入的句子是否符合语言程序的语法规范。要建立一个完整的语言程序,我们就需要语法解析器在遇到特定的输入的时候能够产生对应的动作。“语法->动作”的映射对集合就是连接我们的语言程序(或者,至少是语言接口)的语法到大型实际相关应用之间的桥梁。

在这一章中,我们将要学习怎样使用语法分析树的监听器(listener)和访问器(visitor)来建立语言应用程序。当一个语法分析树遍历器发现或结束识别一个节点的时候,监听器就会响应对应的规则进入事件和规则退出事件(分析识别事件)。某些程序可能需要控制一棵语法树的遍历方式,为了满足这种需求,ANTLR生成的分析树同样也支持非常有名的访问者模式(visitor pattern)。

监听器和访问器之间最大的差别就是,监听器的方法不能通过调用特定的方法来访问子节点。而访问器则需要明确调用子节点,从而才能保证遍历过程继续下去(就如我们在第2.5节中看到的那样)。这是因为这些明确的调用过程,访问器才能够控制遍历的顺序已经遍历节点的数量。为了方便起见,我会用事件方法这个词来代指监听器回调方法或访问器方法。

这一章中,我们的目标是了解ANTLR为我们生成树访问工具的作用以及原理。首先,我们监听器的运行机制开始,了解我们怎样利用监听器和访问器嵌入我们的特定应用程序代码。然后,我们将学习怎样让ANTLR给规则的每个选项生成一个特定的方法。当我们对ANTLR的树遍历过程有了较深的了解之后,我们将看看三个计算器的实现例子,分别代表了三种不同的传递子表达式结果的方法。最后,我们将讨论下这三种传递方式的优缺点。完成了这些之后,我们就已经为下一章中的实例做好充足的准备了。

7.1 从动作代码嵌入到监听器的演化

如果你用过之前版本的ANTLR或者是其它编译器生成器,你一定会很惊讶,我们可以不使用嵌入动作代码就实现语言程序。监听器和访问器机制使得程序代码和语法分离开来(也就是所谓的解耦),这会产生很多令人兴奋的好处。这种解耦可以很好地将程序独立出来,而不是将其打散了分散到语法的各个部分中去。出去嵌入动作之后,我们可以实现语法重用,也就是几乎不需要重新生成语法分析器就可以将相同的语法应用到不同的程序中去。

没有了嵌入动作之后,ANTLR也可以实现用相同的语法使用不同的编程语言生成语法分析器。(我预期在4.0发布之后会开始支持不同的目标语言。)此外,语法错误的修改和语法更新也会变得更加容易,因为我们不用再担心会因为嵌入动作导致合并冲突。

在这一节,我们要探讨下从嵌入动作到语法完全独立的这个过程。下面展示了一个属性文件的语法规则,其中使用<<…>>的方式嵌入代码。像“<<start_file>>”这样的动作标识都表示这里有一段合适的Java代码。

grammar PropertyFile;

file : {«start file»} prop+ {«finish file»} ;

prop : ID '=' STRING '\n' {«process property»} ;

ID : [a-z]+ ;

STRING: '"' .*? '"';

这样的结合写法就将语法和一个特定的应用程序绑定到一起了。更好的做法是建立一个PropertyFileParser(由ANTLR生成的一个类)的子类,然后将嵌入代码转换成其成员函数。这样的做法可以在语法中仅保留对这些新建的函数的调用就可以了。然后,通过创建不同的子类,我们可以实现在不修改语法的前提下实现不同的程序。下面是一个这种实现的例子:

grammar PropertyFile;

@members {

void startFile(){ } // blank implementations

void finishFile(){ }

voiddefineProperty(Token name, Token value) { }

}

file : {startFile();} prop+ {finishFile();} ;

prop : ID '=' STRING'\n'{defineProperty($ID, $STRING)} ;

ID : [a-z]+ ;

STRING : '"' .*? '"';

这样可以实现语法的重用,但是这样的语法仍然和Java关联起来了,因为语法中使用的是Java语言的函数调用方式。我们将在稍后处理这个问题。

为了证明重构代码的可重用性,我们来建立两个简单的应用程序,第一个应用程序仅仅是简单地输出它识别到的属性。实现的过程也就是简单地扩展ANTLR自动生成的语法分析类,并重载几个语法中定义的方法。

class PropertyFilePrinterextendsPropertyFileParser {

void defineProperty(Token name, Token value) {

System.out.println(name.getText()+"="+value.getText());

}

}

这个类中,我们并没有重载startFile()和finishFile()这两个方法,因为在父类PropertyFileParser中已经由ANTLR自动生成了其默认实现了。在这里,我们并不想去改它。

要执行我们的代码,我们需要创建我们自己写的PropertyFileParser的子类的实例。

PropertyFileLexer lexer = newPropertyFileLexer(input);

CommonTokenStream tokens = newCommonTokenStream(lexer);

PropertyFilePrinter parser = newPropertyFilePrinter(tokens);

parser.file(); // launch our special version of the parser

第二个程序,我们将识别到的属性保存到一个map对象中,而不是直接输出来。我们需要做的所有改动仅仅是创建一个新的子类,并在defineProperty()函数中写入不同的代码。

class PropertyFileLoaderextendsPropertyFileParser {

Map<String,String> props =new OrderedHashMap<String,String>();

void defineProperty(Token name, Token value) {

props.put(name.getText(), value.getText());

}

}

语法分析程序执行完之后,props字段中就会填充上识别到的键值对了。

这个语法中还有一个问题存在,那就是嵌入动作限制了我们只能生成Java语言的程序。若是想要提高语法重用性并做到与目标语言无关,我们需要彻底不使用嵌入动作。在下面的两节中我们将展示如何使用监听器和访问器实现这一点。

7.2 使用分析树监听器实现语言程序

要建立一个独立于应用和语法的语言程序,关键在于用语法分析器创建一棵语法树,然后使用特定程序代码遍历这课语法树。我们可以自己手动编写代码来遍历这棵树,当然,我们也可以使用ANTLR生成的树遍历工具来实现。在这一节中,我们使用ANTLR内建的ParseTreeWalker来建立一个基于监听器版本的属性文件程序。

首先,我们看一下没有任何嵌入代码的属性文件语法。

listeners/PropertyFile.g4

file : prop+ ;

prop : ID '=' STRING'\n';

下面是一个样例的属性文件:

listeners/t.properties

user="parrt"

machine="maniac"

ANTLR会根据语法自动生成PropertyFileParser类,这个类会自动建立下面这样的语法分析树:

一旦我们建立了语法树之后,我们就可以使用ParseTreeWalker来遍历所有节点,并触发对应的进入事件和退出事件。

我们先来看看ANTLR根据PropertyFile语法自动生成的PropertyFileListener这个监听器接口。ParseTreeWalker在发现和离开节点的时候,会分别为每一个规则子树引发进入和退出事件。因为我们的PropertyFile语法中只有两个语法规则,所以接口中只有4个方法。

listeners/PropertyFileListener.java

import org.antlr.v4.runtime.tree.*;

import org.antlr.v4.runtime.Token;

public interface PropertyFileListenerextendsParseTreeListener {

void enterFile(PropertyFileParser.FileContext ctx);

void exitFile(PropertyFileParser.FileContext ctx);

void enterProp(PropertyFileParser.PropContext ctx);

void exitProp(PropertyFileParser.PropContext ctx);

}

FileContext和PropContext分别是语法分析树节点具体的实现对象。它们包含了很多有用的方法。

为了方便起见,ANTLR也生成了一个PropertyFileBaseListener类来提供接口函数的默认实现,就像我们在上一节中在@members区域中写的一样,这个类提供了所有函数的空实现方法。

public class PropertyFileBaseVisitor<T>extendsAbstractParseTreeVisitor<T>

implements PropertyFileVisitor<T>

{

@Override public T visitFile(PropertyFileParser.FileContextctx) { }

@Override public T visitProp(PropertyFileParser.PropContextctx) { }

}

(译者注:上面的代码出现得好像和文本描述的内容有点不相符,但是不影响理解。)

对接口的默认实现可以允许我们只对我们感兴趣的方法进行重载和实现。例如,下面的代码就是对属性文件加载器的再实现,它使用了监听器机制,却只有一个方法:

listeners/TestPropertyFile.java

public static class PropertyFileLoaderextends PropertyFileBaseListener {

Map<String,String> props = new OrderedHashMap<StringString>();

public void exitProp(PropertyFileParser.PropContext ctx){

String id = ctx.ID().getText(); // prop : ID '=' STRING '\n' ;

String value = ctx.STRING().getText();

props.put(id,value);

}

}

这个版本代码的主要特征就是其扩展了BaseListener,这个监听器的方法会在解析器完成后调用。

在这一步我们会接触到很多接口和类,下面,我们先看一看主要类和接口之间的继承关系是怎样的(接口用斜体表示)。

ParseTreeListener接口位于ANTLR运行时库当中,所有的监听器都继承自它,它决定了所有监听器都要响应visitTerminal(),enterEveryRule(),exitEveryRule()以及(依据语法错误)visitErrorNode()方法。ANTLR会根据PropertyFile语法生成PropertyFileListener接口,并生成了一个默认实现所有接口方法的类:PropertyFileBaseListener。唯一一个我们自己建立的类就是PropertyFileLoader,它从PropertyFileBaseListener哪里继承了所有的空实现的函数。

exitProp()方法访问了一个规则上下文对象:PropContext。这个对象是和prop规则紧密联系在一起的。也就是说,对于prop规则中定义的每一个元素(ID和STRING),上下文对象中都会对应有一个方法。由于这些元素正好都是语法中的词法节点(符号元素),所以这些对应的方法会返回语法树节点中的TerminalNode。我们可以直接通过getText()方法获得这些符号对应的文本内容,也可以通过getSymbol()方法获得这些符号的Token。

现在,我们可以做一些令人兴奋的事情了。让我们来遍历这棵树,然后通过新的PropertyFileLoader来进行监听。

listeners/TestPropertyFile.java

// create a standard ANTLR parse treewalker

ParseTreeWalker walker = new ParseTreeWalker();

// create listener then feed to walker

PropertyFileLoader loader = new PropertyFileLoader();

walker.walk(loader, tree); // walk parse tree

System.out.println(loader.props);// print results

下面,我们复习下怎样运行一个ANTLR语法,首先将生成的代码进行编译,然后启动一个测试程序来处理输入文件:

$ antlr4 PropertyFile.g4

$ ls PropertyFile*.java

PropertyFileBaseListener.java PropertyFileListener.java

PropertyFileLexer.java PropertyFileParser.java

$ javac TestPropertyFile.java PropertyFile*.java

$ cat t.properties

user="parrt"

machine="maniac"

$ java TestPropertyFile t.properties

{user="parrt",machine="maniac"}

我们的测试程序成功地将属性赋值文本转换成内存中的map数据结构了。

基于监听器的方法是一个很不错的方法,因为所有的树节点遍历以及相应的方法都是自动进行的。然而,在某些场合下,当我们需要控制遍历的过程的时候,就不能用自动遍历这个方法了。例如,我们需要遍历一个C程序的语法树,希望跳过函数体语法子树,即忽略函数的函数体部分。此外,监听器无法使用函数返回值来传递数据。要解决以上问题,我们需要使用到访问者模式。下面,让我们建立一个基于访问者模式的属性加载器,进而对比这两种方式的优缺点。

7.3 使用访问器来实现语言程序

要使用访问器来取代监听器,我们需要让ANTLR生成访问器接口,实现这个接口,然后再创建一个测试程序调用visit()方法来访问语法树。这一节中,我们并不需要修改语法的任何信息。

在命令行中使用-visitor参数,ANTLR就会生成PropertyFileVisitor接口和一个默认实现了所有方法的PropertyFileBaseVisitor类:

public class PropertyFileBaseVisitor<T>extends AbstractParseTreeVisitor<T>

implements PropertyFileVisitor<T>

{

@Overridepublic T visitFile(PropertyFileParser.FileContextctx) { ... }

@Overridepublic T visitProp(PropertyFileParser.PropContextctx) { ... }

}

我们可以将监听器中的exitProp()函数体拷贝到visitor中关于prop规则的方法中。

listeners/TestPropertyFileVisitor.java

public static class PropertyFileVisitorextends

PropertyFileBaseVisitor<Void>

{

Map<String,String> props = new OrderedHashMap<StringString>();

public Void visitProp(PropertyFileParser.PropContext ctx){

String id = ctx.ID().getText(); // prop : ID '=' STRING '\n' ;

String value = ctx.STRING().getText();

props.put(id,value);

return null; // Java says must return something evenwhen Void

}

}

(译者注:这段代码所属的java文件名可能不对,我觉得文件名应该是PropertyFileVisitor.java)。

为了更好地和上一节所讲的监听器版本做比较,下面列出了访问器接口和类的继承关系:

访问器通过明确调用ParseTreeVisitor接口中子节点的visit()方法来遍历语法树。visit方法是从AbstractParseTreeVisitor中实现的。在这个例子中,prop节点的调用中并没有包含任何子节点,所以visitProp()方法并不需要调用visit()方法。我们将在后面看到访问器的泛型类型参数。

监听器和访问器的测试代码中,最大的差距在于访问器不需要一个ParseTreeWalker。它们直接使用访问器自身来遍历解析器创建的树。

listeners/TestPropertyFileVisitor.java

PropertyFileVisitor loader = new PropertyFileVisitor();

loader.visit(tree);

System.out.println(loader.props);// print results

一切都准备就绪之后,下面是建立和测试的结果:

$ antlr4 -visitor PropertyFile.g4 # create visitor as well this time

$ ls PropertyFile*.java

PropertyFileBaseListener.java PropertyFileListener.java

PropertyFileBaseVisitor.java PropertyFileParser.java

PropertyFileLexer.java PropertyFileVisitor.java

$ javac TestPropertyFileVisitor.java

$ cat t.properties

user="parrt"

machine="maniac"

$ java TestPropertyFileVisitor t.properties

{user="parrt", machine="maniac"}

使用访问器和监听器,我们几乎能完成所有事情了。当我们进入Java的空间的时候,就已经和ANTLR没有半毛钱关系了。我们需要知道的仅仅是语法间的关系,生成的语法树,以及访问器和监听器的事件方法。除了这些,剩下的就是你的代码编写能力了。为了响应我们识别到的输入短语,我们可以生成输出,并收集信息(就像我们之前做的那样),并通过特定方法激活短语,或进行计算。

这个读取属性文件的例子非常的简单,所以我们不用考虑到带有选项分支的规则。默认情况下,不管解析器匹配到一条规则的哪一个选项,ANTLR都只是会给一条规则只生成单一的一个事件方法。这样是十分不方便的,因为绝大多数情况下,访问器或监听器都必须知道解析器匹配的是哪条规则。在下一节,我们将要更细致地讨论下事件方法。

7.4 通过标记规则选项来指定事件方法

为了更好地说明问题,让我们试着用监听器去根据下面的表达式语法来实现一个简单的计算器程序:

listeners/Expr.g4

grammar Expr;

s : e ;

e : e op=MULT e // MULT is '*'

| e op=ADD e // ADD is '+'

| INT

;

显然,e规则会产生一个相当无用的监听器,因为所有e规则的选项只会在树遍历器中引发同样的方法enterE()和exitE()。

public interface ExprListenerextends ParseTreeListener {

void enterE(ExprParser.EContext ctx);

void exitE(ExprParser.EContext ctx);

...

}

为了知道监听器方法中匹配的是e子树中的哪条规则,我们不得借助ctx对象中的op标签来进行判断。

listeners/TestEvaluator.java

public void exitE(ExprParser.EContextctx) {

if ( ctx.getChildCount()==3 ) { // operations have 3 children

int left = values.get(ctx.e(0));

int right = values.get(ctx.e(1));

if ( ctx.op.getType()==ExprParser.MULT ) {

values.put(ctx,left * right);

}

else {

values.put(ctx,left + right);

}

}

else {

values.put(ctx,values.get(ctx.getChild(0))); // an INT

}

}

exitE()方法中出现的MULT字段是由ANTLR在ExprParser中自动生成的:

public class ExprParserextends Parser {

public static final int MULT=1, ADD=2, INT=3, WS=4;

...

}

观察下ExprParser类中的EContext子类,我们可以发现ANTLR将e规则中三个选项中的所有元素都收集起来放到同一个上下文对象中。

public static class EContextextends ParserRuleContext {

    public Token op; // derived fromlabel op

    public List<EContext> e() { ... } // get all e subtrees

    public EContext e(int i) {... } // get ith e subtree

    public TerminalNode INT() { ... } // get INT node if alt 3 of e

    ...

}

在ANTLR中,我们可以使用#运算符来给一个规则最外层的选项命名,从而生成监听器中更多的事件方法。我们从Expr引申出新语法LExpr,并用这种方法给e的选项命名。下面是修改后的e规则:

listeners/LExpr.g4

e : e MULT e # Mult

| e ADD e # Add

| INT # Int

;

现在,ANTLR会根据e的不同的选项生成不同的监听器方法。这样之后,我们就不再需要op这个标签了。对于每个选项标签X,ANTLR都会生成enterX()和exit()两个方法。

public interface LExprListenerextends ParseTreeListener {

void enterMult(LExprParser.MultContext ctx);

void exitMult(LExprParser.MultContext ctx);

void enterAdd(LExprParser.AddContext ctx);

void exitAdd(LExprParser.AddContext ctx);

void enterInt(LExprParser.IntContext ctx);

void exitInt(LExprParser.IntContext ctx);

...

}

需要注意的是,ANTLR同时也会根据每个选项的标签名生成特定的上下文对象(EContext的子类)。这些特定的上下文对象只能访问其对应语法选项中特定的元素。比如,IntContext仅仅只有一个INT()方法。我们能够在enterInt()中调用ctx.INT(),但是在enterAdd()中却无法调用这个方法。

监听器和访问器是十分不错的工具,借助它们,我们可以让我们的语法有着很高的可重用性和可重定向性,甚至实现封装语言的应用,而我们要做的仅仅是更新下事件方法的代码。ANTLR同时也会给我们生成框架代码。事实上,到目前为止,我们建立的应用程序都还没有遇到需要共同实现的问题,也即,事件方法有时候需要彼此之间传递一些部分结果或其他信息。

7.5 在事件方法之间共享信息

不管是收集信息还是计算数值,通过参数和返回值进行信息共享,对比于通过全局变量的形式而言,即方便,又能体现出良好的编程习惯。现在问题来了,ANTLR自动生成的函数签名是不包含参数和返回值类型的。同样,ANTLR生成的访问器方法也是不带特定参数的。

在这一节中,我们要继续在不修改事件方法签名的前提下在事件方法之间传递参数的方案。基于上一节的LExpr语法,我们要建立三种不同的实现方法。第一种实现方法使用了访问器方法的返回值,第二种方法定义了一个共享事件方法的字段,第三种方法通过标记语法树节点来贮存感兴趣的值。

利用访问器遍历语法树

要建立一个基于访问器的计算器,最简单的方法就是通过返回子表达式的值来联系expr下的各个规则元素。例如,visitAdd()应该返回两个子表达式的和。visitInt()应该返回整数的值。传统的访问器并不指定它们访问方法的返回值。给访问器添加返回值类型并不难,只需要扩展LExprBaseVisitor<T>,并把T参数指定为我们需要的Integer就可以了。下面是我们的访问器的代码:

listeners/TestLEvalVisitor.java

public static class EvalVisitorextends LExprBaseVisitor<Integer> {

public Integer visitMult(LExprParser.MultContext ctx) {

return visit(ctx.e(0)) * visit(ctx.e(1));

}

public Integer visitAdd(LExprParser.AddContext ctx) {

return visit(ctx.e(0)) + visit(ctx.e(1));

}

public Integer visitInt(LExprParser.IntContext ctx) {

return Integer.valueOf(ctx.INT().getText());

}

}

EvalVisitor继承了ANTLR中AbstractParseTreeVisitor类的visit()方法,我们在访问器中将会使用这个方法来访问子树。

你可能注意到了EvalVisitor中并没有关于规则s的访问器方法。在LExprBaseVisitor中默认实现的visitS()方法会调用预定义的ParseTreeVisitor.visitChildren()方法。visitChildren()方法会返回最后一个访问的子节点的返回值。在我们的例子当中,visitS()会返回它惟一一个子节点(e节点)所计算的表达式的返回值。就此而言,我们可以直接使用其默认实现。

在测试文件TestLEvalVisitor.java中,我们使用常规代码来启动LExprParser,并输出语法树。然后,我们需要写代码来启动EvalVisitor,并在访问树的时候将计算到的表达式的值输出。

listeners/TestLEvalVisitor.java

EvalVisitor evalVisitor = new EvalVisitor();

int result= evalVisitor.visit(tree);

System.out.println("visitor result = "+result);

建立我们的计算器的时候,我们通知ANTLR生成访问器,我们可以像在属性文件解析时那样使用-visitor选项。(如果我们希望不要生成监听器的话,可以使用-no-listener选项。)下面是完整的建立和测试命令:

$ antlr4 -visitor LExpr.g4

$ javac LExpr*.java TestLEvalVisitor.java

$ java TestLEvalVisitor

1+2*3

➾EOF

<(s (e (e 1) + (e (e 2) * (e 3))))

visitorresult = 7

如果我们使用Java内建的返回值机制来传递信息,我们的访问器工作得非常好。如果我们希望不去手动访问子节点的话,我们可以选择监听器机制。不幸的是,这也同时意味着我们不能使用Java方法的返回值。

用栈来模拟返回值

ANTLR生成的监听器事件方法是没有返回值的(返回值类型是void)。我们要想在监听器的节点方法上实现访问值的话,我们可以将部分结果存在监听器的一个字段中。这个时候很容易就想到了栈结构,就类似于Java运行时使用CPU栈来临时保存方法的返回值。我们的想法就是,将子表达式的值压入栈中,而子表达式上层的方法从栈顶取结果。下面是完整的Evaluator计算器的监听器代码(位于文件TestLEvaluator.java中):

listeners/TestLEvaluator.java

public static class Evaluatorextends LExprBaseListener {

Stack<Integer>stack = new Stack<Integer>();

 

public void exitMult(LExprParser.MultContext ctx) {

int right= stack.pop();

int left =stack.pop();

stack.push( left * right );

}

public void exitAdd(LExprParser.AddContext ctx) {

int right= stack.pop();

int left =stack.pop();

stack.push(left + right);

}

public void exitInt(LExprParser.IntContext ctx) {

stack.push( Integer.valueOf(ctx.INT().getText()) );

}

}

要测试这段代码,我们可以在TestLEvaluator测试代码中使用ParseTreeWalker,就像我们在TestPropertyFile中那样做。

$ antlr4 LExpr.g4

$ javac LExpr*.java TestLEvaluator.java

$ java TestLEvaluator

1+2*3

➾EOF

<(s (e (e 1) + (e (e 2) * (e 3))))

stackresult = 7

虽然使用栈有点不方便,但是也能很好工作。我们在监听器方法中就必须小心压栈和入栈的操作是否都是正确的。使用访问器可以避免使用栈,但是却需要我们手动访问树的节点。解决这个问题的第三个方法就是捕获并存储部分结果到树节点中。

ANTLR4权威指南 - 第7章 通过特定应用程序代码解耦语法的更多相关文章

  1. ANTLR4权威指南 - 第6章 尝试一些实际中的语法

    第6章 尝试一些实际中的语法 在前一章,我们学习了通用词法结构和语法结构,并学习了如何用ANTLR的语法来表述这些结构.现在,是时候把我们学到的这些用来构建一些现实世界中的语法了.我们的主要目标是,怎 ...

  2. ANTLR4权威指南 - 第5章 设计语法

    在第I部分,我们熟悉了ANTLR,并在一个比较高的层次上了解了语法以及语言程序.现在,我们将要放慢速度来学习下实现更实用任务的一些细节上的技巧,例如建立内部数据结构,提取信息,生成输入对应的翻译内容等 ...

  3. javascript权威指南第14章 表单脚本示例代码

    HTML部分 <!DOCTYPE html> <html> <head> <title></title> </head> < ...

  4. HTTP权威指南:第二章

    URL概览 前面提到,URL资源是HTTP协议所使用的寻找资源位置的定位符.分为三个部分,主要的结构是: 方案://服务器/路径 这种结构使得网络上的每一个资源都只有唯一的命名方法,从而使得浏览器可以 ...

  5. PADSPCB权威指南-第三章 预处理(部分)(原创)

    PADSPCB权威指南-第三章(部分)豆丁地址:http://www.docin.com/p-707132066.html

  6. 【笔记】javascript权威指南-第六章-对象

    对象 //本书是指:javascript权威指南    //以下内容摘记时间为:2013.7.28 对象的定义: 1.对象是一种复合值:将很多值(原始值或者对象)聚合在一起,可以通过名字访问这些值. ...

  7. javascript权威指南第11章 DOM扩展

    //javascript 权威指南 第三版 第11章 DOM扩展 //取得body元素 var body = document.querySelector("body"); //取 ...

  8. 读书笔记系列之java性能优化权威指南 一 第一章

    主题:java性能优化权威指南 pdf 版本:英文版 Java Performance Tuning 忽略:(0~24页)Performance+Acknowledge 1.Strategies, A ...

  9. 【笔记】javascript权威指南-第三章-类型,值和变量

    javascript中的原始类型和对象类型(基本类型和引用类型) //本书是指:javascript权威指南    //以下内容摘记时间为:2013.7.27   计算机程序运行时需要对值(value ...

随机推荐

  1. Android-系统解析AndroidManifest

    在上一篇博客,Android-XML格式描述,介绍来XML在Android中的格式: Android-系统解析AndroidManifest,是分析Android系统是如何去解析AndroidMani ...

  2. Tomcat监听443端口的方法

    当我们需要更安全的访问网站的时候就会选择使用https协议,而https协议默认的端口号为443端口,这就是我们为什么向让Tomcat监听在443端口的原因,因为监控在非80端口和443端口的web服 ...

  3. [LeetCode 题解] Search in Rotated Sorted Array

    前言 [LeetCode 题解]系列传送门: http://www.cnblogs.com/double-win/category/573499.html 题目描述 Suppose an array ...

  4. spring mvc配置ObjectMapper忽略无法识别的字段,以及一些博客推荐

    通过Spring的MethodInvokingFactoryBean类实现的调用configure方法,此方法返回调用该方法的本身实例. 配置完毕后,可以在spring mvc的消息处理器中使用,为了 ...

  5. Winform打包安装程序覆盖安装的实现

    1.修改项目程序集版本号. 2.设置Version,使当前版本号大于前一个版本号. 3.RemovePreviousVersions属性设置为true. 以上三步后,生成安装程序即可实现覆盖安装. P ...

  6. hdu 4325 Flowers(区间离散化)

    http://acm.hdu.edu.cn/showproblem.php?pid=4325 Flowers Time Limit: 4000/2000 MS (Java/Others)    Mem ...

  7. 编程哲学之C#篇:01——创世纪

    我们能否像神一样地创建一个世界? 对于创建世界而言,程序员的创作能力最接近于神--相对于导演,作家,漫画家而言,他们创建的世界(作品)一旦完成,就再也不会变化,创建的角色再也不会成长.而程序员创建的世 ...

  8. LINQ to SQL语句大全

    LINQ to SQL语句大全     LINQ to SQL语句(1)之Where 适用场景:实现过滤,查询等功能. 说明:与SQL命令中的Where作用相似,都是起到范围限定也就是过滤作用的,而判 ...

  9. JSOI2010 满汉全席

    题目链接:戳我 一个2-SAT的模板题. (什么是2-SAT呢?就是解决一个情况两种决策的问题,我们根据"选了其中一个点A就必须选一个点B的原则,从A向B连边.最后判断如果在一个强连通分量里 ...

  10. 队列的实现——c++

    一.介绍 队列(Queue),是一种线性存储结构.它有以下几个特点:(01) 队列中数据是按照"先进先出(FIFO, First-In-First-Out)"方式进出队列的.(02 ...