处理异常时如何避免部分执行代码?
How to avoid partial execution of code when handling exceptions?
我正在做文字冒险。以下代码是展示我的问题的玩具模型。
public class Player:
private Location location;
private Inventory inventory;
public take(Item item) throws ActionException {
location.remove(item); // throws ActionException if item isn't in location
inventory.add(item); // throws ActionException if item can't be picked up
}
}
我的问题是:如果物品 可以 从该位置移除,但 不能 添加到玩家的存货?目前,代码会将物品从该位置移除,但随后无法将其添加到玩家的物品栏中。
基本上,我希望两者都发生,或者两者都不发生。
知道我该怎么做吗?任何帮助表示赞赏。
您通常要寻找的是交易的概念。
这很重要。通常的策略是使用本身支持它的数据库。
如果你不想去那里,你可以从DB中获取灵感。
他们使用版本控制方案。想想像 git 这样的区块链或版本控制系统:你实际上从来没有在任何地方添加或删除任何东西,相反,你制造了克隆。这样,对某些游戏状态的引用就永远不会改变,这很好,因为仔细想想:
即使删除有效并且添加也有效,如果涉及其他线程或在这两个操作之间有任何代码,它们可以 'witness' 项目刚刚消失的情况。它已从 location
中删除,但尚未添加到 inventory
。
想象一个文件系统。假设您有一个包含 2 个文件的目录。您想要从第一个文件中删除第一行并将该行添加到第二个文件中。如果您实际上只是这样做(编辑两个文件),那么总会有一个时刻,观察该目录的任何其他程序都可以观察到无效状态(它要么在两个文件中都看不到该行,要么看到它在两者中)。
因此,您可以这样做:创建一个新目录,复制两个文件,调整两个文件,然后在一个原子操作中将新创建的目录重命名为其旧名称。任何程序,假设它们 'grab a handle to the directory'(这就是它在 linux 上的工作方式),不可能观察到无效状态。他们要么获得旧状态(行在文件 1 中而不是文件 2 中),要么获得新状态(行在文件 2 中而不是文件 1 中)。
您可以对代码使用相同的方法,其中所有状态都是不可变的,所有修改都是通过构建器(可变变体)或一次一步完成的,中间有不可变树,一旦完成你所要做的就是获取一个 GameState
类型的字段并更新它以引用新状态 - java 保证如果你写: someObj = someExpr
,其他线程要么看到旧状态或新状态,他们看不到指针的一半或其他类似的废话。 (您仍然需要 volatile
或 synchronized
或其他东西来确保所有线程及时获得更新)。
如果线程无关紧要,还有另一种选择:
GameState 动作。
除了调用 location.remove
,您还可以使用游戏状态操作。这样的操作既知道如何完成工作(从该位置删除项目),也知道如何撤消工作。
然后您可以编写一个小框架,在其中列出游戏状态操作(此处:可以执行或撤消的操作 'remove from location',以及可以执行或撤消的操作 'add this to inventory') .然后,您将操作列表交给框架。然后,该框架将执行每个操作,一次一个,捕获异常。如果它抓住了一个,它将以相反的顺序进行并在每个游戏状态操作上调用撤消。
这样,您就有了一个可管理的策略,您可以按顺序 运行 一系列操作,知道如果其中一个失败,到目前为止所做的一切都将被正确撤消。
如果没有全局状态锁定,此策略在多线程环境中完全不可能正常工作,因此请确保您将来不需要它.
这一切都非常复杂。结果......大多数人只会使用数据库引擎来做这些事情;他们有交易支持。作为奖励,保存您的游戏现在变得微不足道(数据库一直在为您保存)。
请注意 h2 is a free, open source, all-java (no servers needed, just one jar that needs to be there when your program is run), file-based (As in, all DBs are a single file) DB engine that supports transactions and a decent chunk of SQL syntax. That'd be one way to go. For convenient access, combine it with a nice abstraction over java's core DB access layer, such as JDBI 并且您的系统:
- 可以轻松保存文件。
- 让您可以快速 运行 复杂查询,例如 'find all game rooms with a bleeding monster on it'。
- 完全支持交易。
您只需 运行 这些命令:
START TRANSACTION;
DELETE FROM itemsAtLocation WHERE loc = 18 AND item = 356;
INSERT INTO inventory (itemId) VALUES (356);
COMMIT;
要么两者都发生,要么都没有发生。只要你能用DB约束来表达规则,DB就会帮你检查,如果你违反了规则,就拒绝commit。例如,您可以简单地声明任何给定的 itemId 只能在库存中出现一次。
最后一个也许是最简单但最不灵活的选择是将其编码:您所有的 'game-state-modifying-code' 应该首先检查它是否 100% 确定它可以以非破坏性的方式执行序列中的每个任务时尚。只有当它知道这是可能的时候,所有的工作才会完成。如果其中之一在中途失败,只是硬崩溃,您的游戏现在处于未知的不稳定状态。抛出异常的要点现在降级为 bug 检测:异常现在仅仅意味着你搞砸了,你的检查代码没有涵盖所有的基础。假设您的游戏没有错误,则永远不会发生异常。当然,这个也不能以多线程方式工作。真的,只有数据库才是可靠的答案,如果这就是您想要的,或者手动处理数据库所做的大部分工作。
理想情况下,您会想要一个 inventory.canAddItem(item) 函数,无论它叫什么,returns 一个布尔值,您可以在从该位置删除之前调用它。正如评论者指出的那样,将异常用于控制流并不是一个好主意。
如果添加回该位置不是问题,则如下:
public take(Item item) throws ActionException {
location.remove(item);
try{
inventory.add(item);
}
catch(ActionException e){
location.add(item);
}
}
可能适合你。
我正在做文字冒险。以下代码是展示我的问题的玩具模型。
public class Player:
private Location location;
private Inventory inventory;
public take(Item item) throws ActionException {
location.remove(item); // throws ActionException if item isn't in location
inventory.add(item); // throws ActionException if item can't be picked up
}
}
我的问题是:如果物品 可以 从该位置移除,但 不能 添加到玩家的存货?目前,代码会将物品从该位置移除,但随后无法将其添加到玩家的物品栏中。
基本上,我希望两者都发生,或者两者都不发生。
知道我该怎么做吗?任何帮助表示赞赏。
您通常要寻找的是交易的概念。
这很重要。通常的策略是使用本身支持它的数据库。
如果你不想去那里,你可以从DB中获取灵感。
他们使用版本控制方案。想想像 git 这样的区块链或版本控制系统:你实际上从来没有在任何地方添加或删除任何东西,相反,你制造了克隆。这样,对某些游戏状态的引用就永远不会改变,这很好,因为仔细想想:
即使删除有效并且添加也有效,如果涉及其他线程或在这两个操作之间有任何代码,它们可以 'witness' 项目刚刚消失的情况。它已从 location
中删除,但尚未添加到 inventory
。
想象一个文件系统。假设您有一个包含 2 个文件的目录。您想要从第一个文件中删除第一行并将该行添加到第二个文件中。如果您实际上只是这样做(编辑两个文件),那么总会有一个时刻,观察该目录的任何其他程序都可以观察到无效状态(它要么在两个文件中都看不到该行,要么看到它在两者中)。
因此,您可以这样做:创建一个新目录,复制两个文件,调整两个文件,然后在一个原子操作中将新创建的目录重命名为其旧名称。任何程序,假设它们 'grab a handle to the directory'(这就是它在 linux 上的工作方式),不可能观察到无效状态。他们要么获得旧状态(行在文件 1 中而不是文件 2 中),要么获得新状态(行在文件 2 中而不是文件 1 中)。
您可以对代码使用相同的方法,其中所有状态都是不可变的,所有修改都是通过构建器(可变变体)或一次一步完成的,中间有不可变树,一旦完成你所要做的就是获取一个 GameState
类型的字段并更新它以引用新状态 - java 保证如果你写: someObj = someExpr
,其他线程要么看到旧状态或新状态,他们看不到指针的一半或其他类似的废话。 (您仍然需要 volatile
或 synchronized
或其他东西来确保所有线程及时获得更新)。
如果线程无关紧要,还有另一种选择:
GameState 动作。
除了调用 location.remove
,您还可以使用游戏状态操作。这样的操作既知道如何完成工作(从该位置删除项目),也知道如何撤消工作。
然后您可以编写一个小框架,在其中列出游戏状态操作(此处:可以执行或撤消的操作 'remove from location',以及可以执行或撤消的操作 'add this to inventory') .然后,您将操作列表交给框架。然后,该框架将执行每个操作,一次一个,捕获异常。如果它抓住了一个,它将以相反的顺序进行并在每个游戏状态操作上调用撤消。
这样,您就有了一个可管理的策略,您可以按顺序 运行 一系列操作,知道如果其中一个失败,到目前为止所做的一切都将被正确撤消。
如果没有全局状态锁定,此策略在多线程环境中完全不可能正常工作,因此请确保您将来不需要它.
这一切都非常复杂。结果......大多数人只会使用数据库引擎来做这些事情;他们有交易支持。作为奖励,保存您的游戏现在变得微不足道(数据库一直在为您保存)。
请注意 h2 is a free, open source, all-java (no servers needed, just one jar that needs to be there when your program is run), file-based (As in, all DBs are a single file) DB engine that supports transactions and a decent chunk of SQL syntax. That'd be one way to go. For convenient access, combine it with a nice abstraction over java's core DB access layer, such as JDBI 并且您的系统:
- 可以轻松保存文件。
- 让您可以快速 运行 复杂查询,例如 'find all game rooms with a bleeding monster on it'。
- 完全支持交易。
您只需 运行 这些命令:
START TRANSACTION;
DELETE FROM itemsAtLocation WHERE loc = 18 AND item = 356;
INSERT INTO inventory (itemId) VALUES (356);
COMMIT;
要么两者都发生,要么都没有发生。只要你能用DB约束来表达规则,DB就会帮你检查,如果你违反了规则,就拒绝commit。例如,您可以简单地声明任何给定的 itemId 只能在库存中出现一次。
最后一个也许是最简单但最不灵活的选择是将其编码:您所有的 'game-state-modifying-code' 应该首先检查它是否 100% 确定它可以以非破坏性的方式执行序列中的每个任务时尚。只有当它知道这是可能的时候,所有的工作才会完成。如果其中之一在中途失败,只是硬崩溃,您的游戏现在处于未知的不稳定状态。抛出异常的要点现在降级为 bug 检测:异常现在仅仅意味着你搞砸了,你的检查代码没有涵盖所有的基础。假设您的游戏没有错误,则永远不会发生异常。当然,这个也不能以多线程方式工作。真的,只有数据库才是可靠的答案,如果这就是您想要的,或者手动处理数据库所做的大部分工作。
理想情况下,您会想要一个 inventory.canAddItem(item) 函数,无论它叫什么,returns 一个布尔值,您可以在从该位置删除之前调用它。正如评论者指出的那样,将异常用于控制流并不是一个好主意。
如果添加回该位置不是问题,则如下:
public take(Item item) throws ActionException {
location.remove(item);
try{
inventory.add(item);
}
catch(ActionException e){
location.add(item);
}
}
可能适合你。