现在,您在标准计算器上找不到一些东西:return 语句。Monkey 有它们,就像许多其他语言一样。 它们可以在函数体中使用,也可以在 Monkey 程序中作为顶级语句使用。 但是它们用在何处并不重要,因为它们的工作方式不会改变:return 语句停止对一系列语句的求值,并留下它们的表达式求值后的值。
这是 Monkey 程序中的顶级 return 语句:
5 * 5 * 5;
return 10;
9 * 9 * 9;
当评估这段程序时应该返回10.如果这里的语句是在函数体内,调用函数应该评估为10.重要的应该是后一行,9 * 9 * 9
表达式,将不会被评估。
有几种不同的方法来实现 return 语句。 在某些宿主语言中,我们可以使用 goto 或异常。 但是在 Go 中,“rescue”或“catch”并不容易,而且我们真的没有选择以干净的方式使用 goto。 这就是为什么,为了支持 return 语句,我们将通过我们的评估器传递一个“返回值”。 每当我们遇到 return 时,我们都会将它应该返回的值包装在一个对象中,以便我们可以跟踪它。 我们需要跟踪它,以便我们以后可以决定是否停止评估。
这是所述对象的实现。 这是 object.ReturnValue:
// object/object.go
const (
// [...]
RETURN_VALUE_OBJ = "RETURN_VALUE"
)
type ReturnValue struct {
Value Object
}
func (rv *ReturnValue) Type() ObjectType { return RETURN_VALUE_OBJ }
func (rv *ReturnValue) Inspect() string { return rv.Value.Inspect() }
由于这只是另一个对象的包装器,因此这里没有什么令人惊讶的。 object.ReturnValue 的有趣之处在于它何时以及如何使用。
以下测试展示了我们在以下上下文中对 return 语句的期望一段Monkey程序:
// evaluator/evaluator_test.go
func TestReturnStatements(t *testing.T) {
tests := []struct {
input string
expected int64
}{
{"return 10;", 10},
{"return 10; 9;", 10},
{"return 2 * 5; 9;", 10},
{"9; return 2 * 5; 9;", 10},
}
for _, tt := range tests {
evaluated := testEval(tt.input)
testIntegerObject(t, evaluated, tt.expected)
}
}
为了让这些测试通过,我们必须更改已有的 evalStatements 函数,并将 *ast.ReturnStatement 的 case 分支添加到 Eval:
// evaluator/evaluator.go
func Eval(node ast.Node) object.Object {
// [...]
case *ast.ReturnStatement:
val := Eval(node.ReturnValue)
return &object.ReturnValue{Value: val}
// [...]
}
func evalStatements(stmts []ast.Statement) object.Object {
var result object.Object
for _, statement := range stmts {
result = Eval(statement)
if returnValue, ok := result.(*object.ReturnValue); ok {
return returnValue.Value
}
}
return result
}
此更改的第一部分是对 *ast.ReturnValue 的评估,我们在其中评估与 return 语句关联的表达式。 然后我们将调用 Eval 的结果包装在我们的新 object.ReturnValue 中,以便我们可以跟踪它。
在 evalStatements 中,evalProgramStatements 和 evalBlockStatements 使用它来评估一系列语句,我们检查最后的评估结果是否是这样的 object.ReturnValue,如果是,我们停止评估并返回解包的值。 这很重要。 我们不返回 object.ReturnValue,而只返回它包装的值,这是用户期望返回的值。
不过有一个问题。 有时我们必须更长时间地跟踪 object.ReturnValues 并且无法在第一次遇到时解开它们的值。 块语句就是这种情况。 看看这个:
if (10 > 1) {
if (10 > 1) {
return 10;
}
return 1;
}
这段程序应该返回10,但随着我们当下的实现,它却是返回1。一小段测试证明了结论:
// evaluator/evaluator_test.go
func TestReturnStatements(t *testing.T) {
tests := []struct {
input string
expected int64
}{
// [...]
{
`
if (10 > 1) {
if (10 > 1) {
return 10;
}
return 1;
}
`,
10,
},
}
// [...]
}
这段测试有明确的失败信息:
$ go test ./evaluator
--- FAIL: TestReturnStatements (0.00s)
evaluator_test.go:159: object has wrong value. got=1, want=10
FAIL
FAIL monkey/evaluator 0.007s
我打赌你已经发现了我们当下这段程序实现的问题所在。但是如果你想让我把它说出来,它来了:如果我们有嵌套的块语句(这在 Monkey 程序中是完全合法的!)我们不能第一眼看到 object.ReturnValue 的值,因为我们需要 进一步跟踪它,以便我们可以在最外面的块语句中停止执行。
非嵌套块语句适用于我们当前的实现。 但是为了让嵌套语句工作,我们必须做的第一件事就是接受我们不能重用我们的 evalStatements 函数来评估块语句。 这就是为什么我们要将其重命名为 evalProgram 并使其不那么通用。
// evaluator/evaluator.go
func Eval(node ast.Node) object.Object {
// [...]
case *ast.Program:
return evalProgram(node)
// [...]
}
func evalProgram(program *ast.Program) object.Object {
var result object.Object
for _, statement := range program.Statements {
result = Eval(statement)
if returnValue, ok := result.(*object.ReturnValue); ok {
return returnValue.Value
}
}
return result
}
为了评估 *ast.BlockStatement,我们引入了一个名为 evalBlockStatement 的新函数:
// evaluator/evaluator.go
func Eval(node ast.Node) object.Object {
// [...]
case *ast.BlockStatement:
return evalBlockStatement(node)
// [...]
}
func evalBlockStatement(block *ast.BlockStatement) object.Object {
var result object.Object
for _, statement := range block.Statements {
result = Eval(statement)
if result != nil && result.Type() == object.RETURN_VALUE_OBJ {
return result
}
}
return result
}
这里我们明确不解包返回值,只检查每个评估结果的 Type()。 如果它是 object.RETURN_VALUE_OBJ,我们只需返回 *object.ReturnValue,而不解包其 .Value,因此它会在可能的外部块语句中停止执行并冒泡到 evalProgram,在那里它最终被解包。 (当我们实现函数调用的评估时,最后一部分会发生变化。)
然后测试通过:
$ go test ./evaluator
ok monkey/evaluator 0.007s
执行返回语句。 现在我们绝对不再构建计算器了。而且由于 evalProgram 和 evalBlockStatement 在我们的脑海中仍然如此新鲜,让我们继续研究它们。
< 3.6条件句 | > 3.8中止!中止!有错误!,或:错误处理 |
---|