Skip to content

Latest commit

 

History

History
182 lines (157 loc) · 6.74 KB

File metadata and controls

182 lines (157 loc) · 6.74 KB

3.7return语句

现在,您在标准计算器上找不到一些东西: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中止!中止!有错误!,或:错误处理