2

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

@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 1 --> a (第八次) --> 1 - -> a(第九次),此时,您将获得“超出最大递归深度”。信息。如果您一直忽略此消息,则代码的内部计算将开始表现出奇怪的行为,甚至“显然”似乎已经产生了良好的结果

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

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

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

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

谢谢,
马莱克

4

1 回答 1

12

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

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

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

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      ******

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


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


编辑以响应更新的问题和评论

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

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


最后一个编辑 - 使用 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.
于 2012-08-11T21:26:47.220 回答