如何在Pharo Smalltalk中实现一个开关(switch)

9

我想解析一个命令和一个整数,使"turtle"在棋盘上移动。我有点不知所措,因为它没有抛出异常,我甚至无法弄清如何在没有异常的情况下打开调试器。

我的代码:

"only should begin parsing on new line"
endsWithNewLine:= aTurtleProgram endsWith: String cr.
endsWithNewLine ifTrue:[
    "splits commands based on new lines"
    commands := aTurtleProgram splitOn: String cr.
    commands do:[:com |
        "should split into command and value for command"
        i := com splitOn: ' '.
        "command"
        bo := ci at: 1.
        val := ci at: 2.
        "value for command"
        valInt := val asInteger.
        ^ bo = 'down' "attempted switch"
            ifTrue: [ turtle down: valInt ]
            ifFalse: [
              bo = 'left'
                  ifTrue: [ turtle left: valInt ]
                  ifFalse: [
                    bo = 'right'
                        ifTrue: [ turtle right: valInt ]
                        ifFalse: [
                          bo = 'up'
                              ifTrue: [ turtle up: valInt ]
                              ifFalse: [ self assert: false ] ] ] ] ].
3个回答

10

您可以按照之前的方式进行,通过在代码中插入self halt语句打开调试器。

通常,使用if,特别是类似于case语句的if,都不是很好的做法。因此,您可以将功能分解为像DownMoveLeftMove等类,然后每个类在调用诸如execute:方法时会实现其自己的功能,这将完全按照命令所需执行操作。但在您的情况下,这种方法可能过于繁琐;而且,您的操作非常琐碎。

您可以使用包含定义的字典。因此,想象一下您定义了一个实例变量或者类变量:

moveActions := {
  'down' -> [ :dist | turtle down: dist ] .
  'left' -> [ :dist | turtle left: dist ] .
  ... } asDictionary

然后在你的代码中,你需要这样做:(moveActions at: bo) value: valInt。这段代码会为字符串(键)给出一个块(value),然后你可以用整数来评估这个块。

另一方面,由于动作模式相同,只有消息不同,因此你可以仅在字典中映射消息名称:

moveActions := {
  'down' -> #down: .
  'left' -> #left: .
  ... } asDictionary

那么您可以要求您的海龟执行由字符串动态给出的消息:

`turtle perform: (moveActions at: bo) with: valInt`

另外,如果您希望依赖于阅读的命令与发送给海龟的消息之间的相似性,则可以动态组合消息字符串:

`turtle perform: (bo, ':') asSymbol with: valInt`
请注意,这种方式不建议在您的情况下使用。首先,您正在将用户输入与您的代码耦合在一起,也就是说,如果您决定将用户命令从down更改为moveDown,则必须将方法名称从down:更改为moveDown:。此外,这种方法允许用户向您的系统“注入”恶意代码,因为他可以编写像become 42这样的命令,这会导致代码:
`turtle perform: #become: with: 42`

这将在海龟对象和42之间交换指针。或者你可以考虑更糟糕的情况。但我希望这次元游对你有好处。:)


1
"perform:" 需要一个符号作为参数。因此,您可以执行 (bo, ':') asSymbolbo asSymbol asMutator - Tobias
非常好用!非常感谢! - sumaksion
1
基于多态性的方法为什么会很麻烦?相反,这将是正确的方法。Smalltalk没有switch语句的原因是很容易创建一小组对象层次结构并让它们管理条件行为。这就是GoF所谓的“状态模式”,旨在使代码比许多条件语句更易于维护。(当然,字典方法仍然比使用switch语句更好。) - Amos M. Carpenter
据我所知,不存在“完美的软件设计”,这就是为什么这本书叫做“面向对象设计启发式”的原因。当然,拥有不同的类以各自的方式处理相同的消息是有意义的,但在当前情况下,EstebanLM的解决方案简短而简单,而为每个可能的命令维护一个类可能会非常困难。另一方面,如果您想添加新的命令并以易于管理的方式进行管理,则必须使用类来完成。 - Uko
@Uko:说得好。如果有一种完美的方法,每个人都会用那种方法。:-) 在除了Smalltalk之外的其他语言中,我可能会同意对于这些东西许多小类层次结构可能会很麻烦(例如,在Java中,使用枚举的解决方案可能更容易)。 - Amos M. Carpenter
显示剩余2条评论

7

在Smalltalk中,您不使用switch语句。相反,您使用“case方法”(我认为这个术语是由Ken Beck引入的,但我不确定)。

在您的情况下,可能会像这样:

method1
    "only should begin parsing on new line"
    endsWithNewLine:= aTurtleProgram endsWith: String cr.
    endsWithNewLine ifTrue:[
    "splits commands based on new lines"
    commands := aTurtleProgram splitOn: String cr.
    commands do:[ :eachCommand | 
        | tuple direction value |

        tuple := eachCommand splitOn: ' '.
        direction := tuple first.
        value := tuple second asInteger.
        self moveTurtleDirection: direction value: value ].

moveTurtleDirection: direction value: value
    direction = 'down'  ifTrue: [ ^turtle down: value ].
    direction = 'left'  ifTrue: [ ^turtle left: value ].
    direction = 'right' ifTrue: [ ^turtle right: value ].
    direction = 'up'    ifTrue: [ ^turtle up: value ].

    self error: 'Invalid direction'.

如您所见,这种设计更为清晰,不需要应用“闲聊魔法”就能高效实现。此外,它还具有清晰、执行速度快和易于编译器以及JIT优化的优点 :)


3

只是举另一个可能性的例子:与其自己编写解析器,不如使用Smalltalk中可用的ParserGenerator之一(PetitParser、OMeta、Smacc、Xtreams等)

以下是使用Xtreams的示例https://code.google.com/p/xtreams/wiki/Parsing(链接可能很快失效,但我没有更新的内容...),它可以解析PEG格式(http://en.wikipedia.org/wiki/Parsing_expression_grammar)。

首先在字符串中定义您的语法:

grammar := '
    Number   <- [0-9]+
    Up  <- "up" Number
    Down    <- "down" Number
    Left    <- "left" Number
    Right   <- "right" Number
    Command <- Up / Down / Left / Right
'.

然后你定义一个解释器来移动海龟:

PEGActor subclass: #TurtleInterpreter
    instanceVariableNames: ''
    classVariableNames: ''
    poolDictionaries: ''
    category: 'Test-Parser'.

使用几种方法将解释器连接到Turtle,并通过所谓的pragma(注释)将操作与上面的语法规则连接起来:
turtle: aTurtle
    turtle := aTurtle

Number: digits
    <action: 'Number'>
    ^digits inject: 0 into: [ :total :digit | total * 10 + ('0123456789' indexOf: digit) - 1 ]

Up: aNumber
    <action: 'Up'>
    turtle moveUp: aNumber

Down: aNumber
    <action: 'Down'>
    turtle moveDown: aNumber

Left: aNumber
    <action: 'Left'>
    turtle moveLeft: aNumber

Right: aNumber
    <action: 'Right'>
    turtle moveRight: aNumber

然后,您只需创建一个解析器并将其连接到此解释器:

parser := PEGParser parserPEG
    parse: 'Grammar'
    stream: grammar
    actor: PEGParserParser new.
interpreter := TurtleInterpreter new turtle: Turtle new.    
parser parse: 'Command' stream: 'left24' actor: interpreter.

我让你自己发现如何指定空格、换行或命令序列,但是你可以看到使用这样的框架时代码是多么解耦和易于扩展:一个新命令 = 语法描述中的一行 + 解释器中连接海龟动作的一个方法...

1
这有点好笑,一个人问关于switch语句的问题,得到了一个真正回答他问题的答案,另一个则告诉如何使用元编程和间接消息,还有一个简短的编写解析器指南 :). 我们拥有一个很棒的社区。 - Uko
1
@uko 是的,答案是如何在Smalltalk中不编写switch语句 ;) - aka.nice

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接