【问题标题】:Batch Limitation - Maximum Recursion while browsing menus批处理限制 - 浏览菜单时的最大递归
【发布时间】:2012-08-08 15:36:45
【问题描述】:

我在测试我的代码时遇到了一个大问题。而这个问题就是烦人的消息……“超出最大递归深度”。看看它是如何工作的:

@echo off

REM ---------- MAIN MENU ----------
:Main

echo 1.Supermarket
echo 2.Exit Batch

setlocal
for /f %%A in ('"prompt $H & echo on & for %%B in (1) do rem"') do set "BS=%%A"
set /p Menu=%BS% Type in your option {1,2} followed by ENTER:
if not '%Menu%'=='' set Menu=%Menu:~0,1%
if %Menu% EQU 1 ENDLOCAL & goto Super
if %Menu% EQU 2 goto EOF

REM ---------- SECONDARY MENU ----------
:Super

echo 1.Supermarket
echo   a.Apple
echo   b.Banana

setlocal
for /f %%A in ('"prompt $H & echo on & for %%B in (1) do rem"') do set "BS=%%A"
set /p Menu=%BS% Type in your option {1,a,b} followed by ENTER:
if not '%Menu%'=='' set Menu=%Menu:~0,1%
if %Menu% EQU 1 ENDLOCAL & goto Main
if %Menu% EQU a ENDLOCAL & goto App
if %Menu% EQU b ENDLOCAL & goto Ban

:App

 setlocal enabledelayedexpansion
 set /a cnt=0
 for /f "tokens=* delims= " %%a in (*.apple) do (set /a cnt+=1)
 if %cnt% EQU 0 (
 echo There are %cnt% apples & pause>nul & ENDLOCAL & goto Main
 ) else (
 echo There are %cnt% apples

 [... do many things and after 200 lines]

 echo The total number of apples was %cnt% & pause>nul & ENDLOCAL & goto Main

:Ban
echo This is Banana & pause>nul & goto Main

:EOF

假设用户连续遵循这个序列:

1 --> a (第一次) --> 1 --> a (第二次) --> 1 --> a (第三次) etc etc etc 1 --> a (第八次) --> 1 --> a (第九次),此时,您将收到 "Maximum recursion depth exceeded". 消息。如果您一直忽略此消息,您的代码的内部计算将开始表现出奇怪的行为,甚至“显然”似乎已经产生了良好的结果

其实顺序无关紧要。如果您在字母“a”和“b”之间不断变化,或者“a”多于“b”,也会发生同样的事情:

1 --> b (第一次) --> 1 --> a (第二次) --> 1 --> a (第三次) ... 1 --> a (第八次) --> 1 --> b (第九次)。再次出现“超出最大递归深度”。 消息

此外,如果用户不断地输入选项 1(总是停留在 MAIN MENU),则在第 12 次试用后会弹出错误消息。

如何保持与上面相同的代码结构但避免出现此错误?我在这里简化,但我说的是一批几百行代码和几个二级、三级、四级等子菜单。

谢谢,
马莱克

【问题讨论】:

  • 我有很多 setlocal 打开但没有关闭。通过在代码的几个区域发出命令 ENDLOCAL,问题几乎消失了。 ENDLOCAL 是否太多?我不想错过任何变量(如 %cnt%)的结果。根据 ENDLOCAL 的位置,%cnt% 不显示任何值。在数百行代码中,究竟应该将 ENDLOCAL 放在哪里才能获得性能并且不会丢失数据?

标签: batch-processing batch-file


【解决方案1】:

我在您发布的代码中没有看到任何递归,毫不奇怪,我无法让代码失败。

您必须在未显示的导致问题的代码中进行递归。

我知道批处理文件有两种类型的致命递归错误:

1) SETLOCAL 最多限制为 32 个级别。尝试在不发出 ENDLOCAL 的情况下第 33 次使用 SETLOCAL 将导致以下致命错误消息:

Maximum setlocal recursion level reached.

我怀疑这是您达到的递归限制。

更新: 实际上,每个 CALL 级别的限制是 32 个 SETLOCAL。欲了解更多信息,请阅读http://ss64.org/viewtopic.php?id=1778

上的整个帖子

2) 如果在其中任何一个返回之前发出太多 CALL,CALL 递归可能会失败。每当到达脚本、EXIT /B 或 GOTO :EOF 的末尾时,批处理都会从 CALL 中返回。递归调用的最大数量取决于机器。 (可能与代码有关?)错误消息如下所示:

******  B A T C H   R E C U R S I O N  exceeds STACK limits ******
Recursion Count=600, Stack Usage=90 percent
******       B A T C H   PROCESSING IS   A B O R T E D      ******

可能有一些系统配置可能会增加您可以进行的递归 CALL 的数量,但总会有一些上限。


如果您达到了递归限制,那么除了重构您的代码之外,您真的别无选择。但是,除非您展示您的递归代码,否则我们很难(就我而言几乎不可能)为您提供解决问题的方法。


针对更新的问题和评论进行编辑

这是您需要遵循的一般规则:如果您有一个代码循环,那么每个 SETLOCAL 都必须与一个 ENDLOCAL 配对。循环可以由 GOTO 创建,也可以是 FOR 循环。

您正在大量使用 GOTO 循环。您成功地确定在发出 SETLOCAL 之后,您必须在执行循环返回的 GOTO 之前发出 ENDLOCAL。在我看来,你解决了这个问题。


最后一个 EDIT - 使用 CALL 简化逻辑

当您使用 GOTO 循环时,要准确跟踪必须发出 ENDLOCAL 的位置和次数是一件很痛苦的事情。如果你使用 CALL 代替,逻辑会简单得多,而且代码也更有条理。

在执行 CALL 子例程期间发出的所有 SETLOCAL 语句都会在例程结束后隐式结束。使用 CALL 时无需显式使用 ENDLOCAL。

下面我展示了如何重构您的代码以使用 CALL。研究所有代码 - 我做了一些我认为简化和/或改进逻辑的其他方面的细微更改。

@echo off
setlocal
:: Define the BS variable just once at the beginning
for /f %%A in ('"prompt $H & echo on & for %%B in (1) do rem"') do set "BS=%%A"

:Main  ---------- MAIN MENU ----------
:: ECHO( echoes a blank line
echo(
echo Main Menu
echo   1.Supermarket
echo   2.Exit Batch
:: If user hits ENTER without entering anything then current value remains.
:: So clear the value to make sure we don't accidently take action based
:: on prior value.
set "Menu="
set /p Menu=%BS% Type in your option {1,2} followed by ENTER:
if defined Menu set "Menu=%Menu:~0,1%"
if "%Menu%" EQU "1" call :Super
if "%Menu%" EQU "2" exit /b
goto :Main

:Super ---------- SECONDARY MENU ----------
setlocal
echo(
echo Supermarket
echo   a.Apple
echo   b.Banana
echo   1.Retutn to Main
set "Menu="
set /p Menu=%BS% Type in your option {1,a,b} followed by ENTER:
if defined Menu set "Menu=%Menu:~0,1%"
:: Use IF /I option so case does not matter
if /i "%Menu%" EQU "1" exit /b
:: Note that :App and :Ban are still logially part of this subroutine
:: because I used GOTO instead of CALL. When either :App or :Ban ends, then
:: control will return to the Main menu
if /i "%Menu%" EQU "a" goto :App
if /i "%Menu%" EQU "b" goto :Ban
goto :Super

:App
setlocal enableDelayedExpansion
set /a cnt=0
for /f "tokens=* delims= " %%a in (*.apple) do (set /a cnt+=1)
echo There are %cnt% apples
if %cnt% GTR 0 (
 REM ... do many things and after 200 lines
 echo The total number of apples was %cnt%
)
pause>nul
exit /b
:: The EXIT /B above returns to the Main menu (ends the CALL to :Super)
:: Note that the SETLOCAL at the start of :Super and the one at the
:: start of :App are implicitly ended. No need to explictly issue
:: ENDLOCAL.

:Ban
echo This is Banana
pause>nul
exit /b
:: The EXIT /B above returns to the Main menu (ends the CALL to :Super)
:: Note that the SETLOCAL at the start of :Super is implicitly ended.
:: No need to explictly issue ENDLOCAL.

【讨论】:

  • @LuizMaleck - 看起来你解决了你的问题。请参阅我编辑的答案。
  • @dbnham : 再次感谢亲爱的朋友!亲切的问候。
  • @LuizMaleck - 看看我编辑的答案中的简化逻辑。
  • dbenham,我从你的回答中学到了很多不错的技巧!我非常感谢这个详细的解释以及您测试和发布代码的时间。我想买这本书“终极批处理文件书(1995 年) - 罗尼·理查森(Ronny Richardson)”。你怎么看?有没有一本你或任何读过这篇文章的人都会毫不犹豫地推荐的 Batch 编程书?
  • 我从来没有读过任何关于批处理的书,所以我不能说。但是,如果您通过查看一些好的网站来了解同样的信息,我不会感到惊讶:dostips.comjudago.webs.comrobvanderwoude.com/batchfiles.phpss64.com/nt/syntax.html
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2015-02-03
  • 1970-01-01
  • 2014-08-25
相关资源
最近更新 更多