第二次Blog作业(作业集04-06数字电路模拟程序)
前言
本阶段的三次作业以“数字电路模拟程序”为主线,整体是典型的迭代式开发:在保持“输入解析—连接建模—信号求值—按规则输出”这一主流程不变的前提下,不断提高模型表达能力与规则复杂度。对我而言,三次作业的核心收获并不只是把功能做出来,而是逐步意识到:当需求演进时,代码需要从“能跑”过渡到“可扩展、可验证、可维护”。
作业集04更像一次“把最小可用版本跑通”的练习:元件类型少,但输入格式细节多,主难点在解析与传播闭环的正确性。作业集05则把难点转移到“器件规则密集 + 引脚结构变化”:控制端、多输出、译码/选择/分配等逻辑使得单个器件实现本身就变得复杂。作业集06并未扩展器件种类,而是引入“子电路 + 异常输入检测”,本质上要求在程序中处理作用域、命名冲突、异常优先级与递归求值链路,这对整体架构的分层要求更高。
作为第二次写课程博客的人,这次我刻意把写作时间留到了“基本稳定通过样例与自测”之后:先把工程收敛到一个相对可解释的结构,再整理类图与度量数据,最后把踩坑过程按“现象—根因—修复—验证”补成证据链。相比第一次写博客只会复述功能点,这次更关注“为什么会错、错在哪里、如何让代码下次不再靠运气”。
从主观难度看,我认为三次作业的挑战不在同一维度:
04 最容易出现“看起来小但会致命”的问题(输入解析细节、命名冲突、非法引脚导致的静默失败);
05 的压力主要来自器件规则密度与引脚编号语义,多输出与高阻态一旦处理不严谨,组合电路会把错误放大;
06 则更像“规则系统工程”:子电路带来的作用域/命名隔离、异常优先级、token 词法分类与求值链路要一起成立,否则就会出现“异常对了但正常求值挂、正常求值对了但异常挂”的贴皮现象。
设计与分析:作业集04(基础逻辑门电路)
需求要点与实现目标
作业集04包含与门、或门、非门、异或门、同或门五类基础逻辑门。输入中给出外部输入信号与连接关系,程序需要完成电平传播并按“与/或/非/异或/同或 + 编号”排序输出。若某元件存在输入引脚未接到有效信号,则输出中忽略该元件。
我对“有效输入/可计算”的判定采用了偏保守的策略:门的每个预期输入引脚都必须接到可解析且可追溯的信号源,才允许计算输出;一旦存在悬空输入(null)、非法引脚编号或无法解析的来源,则该门输出视为“不可计算”,并在最终输出中被过滤掉。这样做的好处是语义清晰:不拿默认值糊弄求值,也避免“半连线电路”产生看似合理但不可解释的输出。
架构设计与类职责
作业集04的类结构相对简单,主要是一个门电路的继承体系加上主流程控制。我的实现中以 Gate 作为统一接口,BaseGate 作为公共抽象父类,五种逻辑门作为子类分别实现 compute() 规则。
与此同时,Main 仍承担了输入解析、连线存储、传播迭代与最终输出排序等多项职责,这也是后续复杂度分析中主函数复杂度偏高的原因。
插图(作业集04 UML 类图)

关键流程与设计取舍
我将作业集04的核心流程拆成四段进行理解与实现:
输入解析:把 INPUT: 行拆分成外部输入信号表,并解析连接块 [...] 得到“输出端→多个输入端”的连线关系。
建立连线模型:用合适的数据结构保存邻接关系,以支持后续从已知信号传播到下游引脚。
信号传播与求值:在传播过程中为各门电路收集输入;当门电路输入完整时计算输出并继续传播,直到稳定。
输出排序与过滤:按题目要求排序;对无法计算的元件进行过滤输出。
在 04 中我用 Map<String, List
非法/缺失输入要让门保持不可计算,而不是补默认值;
组合环或震荡会让 changed 永远为 true,因此我加入了最大迭代次数保护,超过阈值就判定电路不可收敛并终止,避免卡死。
设计与分析:作业集05(器件扩展:控制端、多输出、组合器件)
需求变化
作业集05在作业集04基础上新增三态门、译码器、数据选择器、数据分配器等器件。关键变化包括:
引脚类型从“输入/输出”扩展为“控制/输入/输出”的组合;
部分器件具备多输出;输出格式也随器件不同而变化;
程序更强调扩展性:新增器件不应导致主流程到处堆 if-else。
架构分层与可扩展性
相比作业集04,我在作业集05中将职责拆分得更清晰:引入 InputParser、Circuit(电路容器)、ComponentFactory(创建器件对象)、Component 抽象类与各器件子类,最终用 OutputFormatter 集中处理不同器件的输出格式。这样做的直接好处是:新增器件时扩展点更集中(工厂 + 新子类),主流程不再成为唯一复杂点。
插图(作业集05 UML 类图)

复杂点聚焦:译码器的规则密度
从 SourceMonitor 的结果看,作业集05最复杂的方法是 Decoder.computeOutputs()。这与译码器本身的规则密度一致:它需要处理控制端有效性判断、输入编码解析、以及多输出引脚的批量生成与赋值。
我在 05 中遇到的边界条件大多与“真实引脚编号”和“无效态传播”有关:
译码器 M / 分配器 F如果只把结果当作“可打印信息”而没有按真实输出引脚号写回信号表,下游组合电路会断信号;
多输出器件的引脚区间必须严格按“控制—输入—输出”偏移推导,否则样例可能过但组合测试必错;
无效/高阻态必须覆盖旧值并清空缓存,否则会出现“残留值”导致随机 WA;
选择器 Z只需要控制端有效且被选中的一路数据有效即可,不应要求所有数据输入都有效。
定位上我不再靠“盯着代码猜”,而是用最小组合电路把器件接到下游门上,用“下游读不到信号/读到旧值”来反推是哪一类错误:是输出没写回、引脚编号错位,还是无效态没有清理。修复策略也随之变得更明确:先把 token→节点→引脚编号的映射推导清楚,再决定输出结构与缓存清理逻辑。
设计与分析:作业集06(子电路 + 异常输入检测)
子电路的抽象与命名冲突问题
引入子电路后,一个直接的问题是:子电路内部元件与主电路元件可能存在同名/同编号情况;同时子电路端口需要在主电路连接中被引用。为了解决作用域带来的命名冲突,我在实现中采用了“扁平化命名”的策略:把子电路内部的元件、端口映射为全局唯一的节点名,从而让连接校验与求值过程仍可以在统一的“节点—边”模型上进行。
插图(作业集06 UML 类图)

异常输入检测:优先级与单次输出原则
作业集06对异常输入给出了明确输出要求,并强调“优先级”与“只输出最前异常”。我的实现把异常检测集中到校验模块中,先完成连接合法性判断与冲突检测,再进入求值阶段,这样可以避免“带着脏数据求值”导致的二次复杂度。

复杂点聚焦:Component.recompute() 的集中式规则实现
从 SourceMonitor 的结果看,作业集06最复杂的方法是 Component.recompute()。这与实现方式有关:不同逻辑门的运算规则通过 switch(type) 集中在同一方法中实现,导致该方法成为维护热点。它的优点是实现集中、便于快速完成;缺点是随着规则增加复杂度会继续上升,且更难做细粒度单元测试。
我在 06 中最典型的一个问题,其实不是“算法写错”,而是 token 分类的语义假设不一致:我一开始更依赖声明式语义(例如只有出现在 INPUT: 的才算源、只有声明过的端口才算端口),但隐藏点更接近“词法层规则”(例如纯大写裸 token 直接当源)。最终我把 isDriverPin() 的判定逻辑收敛为一致且可解释的一套规则,并配合 flattenPort/flattenComponentName 解决子电路作用域冲突,使得异常检测与求值链路可以复用同一套节点模型。
量化证据:SourceMonitor 指标对比
作业集04:

作业集05:


作业集06:



数据解释
OOP4 的复杂度峰值集中在 Main.main()(Maximum Complexity=37),说明当时我把解析、传播、排序等流程高度集中,属于“先把功能做通”的实现风格。
OOP5 的复杂度峰值转移到 Decoder.computeOutputs()(Maximum Complexity=12),说明我在 05 中完成了主流程分层后,复杂度主要落在“规则密集器件”的内部实现上。
OOP6 的复杂度峰值为 Component.recompute()(Maximum Complexity=16),说明我在 06 中采用集中式 switch(type) 来实现多种门的规则,带来了维护热点;但最大嵌套深度下降(9+ → 6)也反映出整体模块化的改善。
把这组数据与实际开发过程对起来,我能比较明确地看到“复杂度迁移”的轨迹:04 阶段我把所有流程塞进一个主函数,结果复杂度峰值出现在入口处;05 阶段做了分层以后,入口变薄,复杂度自然转移到规则最密集的译码器;06 阶段为了快速保证一致性,我把 A/O/N/X/Y 的门逻辑集中进一个 recompute(),于是它成为新的维护热点。另一方面,最大嵌套深度从 9+ 降到 6,也说明我后期更倾向把规则拆成独立模块(解析/校验/注册/求值/输出),靠模块边界而不是靠“在一个大循环里打补丁”来组织复杂性。
采坑心得
采坑心得:作业集04
作业集04整体器件类型不多,但“坑”主要集中在解析—建模—传播—输出这条链路的细节一致性。一开始我更关注“能把样例跑出来”,对输入的命名约束、非法引脚、组合环等情况缺少约束,导致一些问题不会在简单样例暴露,却会在更复杂连接或隐藏点中集中爆发。
1)多输入异或门“凭空消失”
我起初以为所有门都可以像与门/或门一样写成 X(3) 表示三输入异或。在解析连线如 [...] X1-1 X1-2 X1-3 时,创建函数被调用去创建 X1(3)。但当时的创建逻辑只在“带括号”的分支里处理了 A/O,对 X/Y 没写,直接返回 null。结果是:门对象根本没进入 gates,后续连到它引脚的信号被静默忽略,仿真输出就像“缺了一大块电路”。
修复方式是:补全对 X/Y 多输入形式的解析与创建,并把异或/同或的计算逻辑扩展为可处理多个输入(而不是固定 2 输入)。
最小复现:
Plain Text
1
2
3
4
5
6
INPUT: A-1 B-0 C-1
[A X(3)1-1]
[B X(3)1-2]
[C X(3)1-3]
[X(3)1-0 OUT]
end
修复前的典型表现是 X(3)1 根本不会被创建,最终输出为空或缺失;修复后应能输出 X(3)1-0:0/1(按异或规则计算)。
2)组合环导致死循环
测试时我刻意把一个门的输出接回自身输入,尝试验证仿真器对组合环的处理。程序直接卡死,原因在于我采用“只要信号变化就继续迭代”的主循环策略,而环路会导致信号在“可计算/不可计算”之间震荡:门先因输入不全被移除输出,输出消失又会改变环上其他门输入,随后又重新满足计算条件,changed 永远为 true。
最终我加入“最大迭代次数”保护(例如 1000 次),超过阈值则认为存在组合环或无法收敛的结构,直接终止并输出提示,从而避免死循环。
最小复现:
Plain Text
1
2
3
4
INPUT: A-1
[A N1-1]
[N1-0 N1-1]
end
修复前程序可能因 changed 永远为 true 而卡死;修复后会在达到最大迭代次数后退出并给出提示(避免死循环)。
3)悬空输入与非法引脚导致“门被写死”
在作业04里,门的可计算性由 canCompute() 判断:只要存在 null 输入就不可计算。因此当 A(3) 只接了 2 个输入时,该门输出永远不会出现。
更隐蔽的问题来自“引脚索引越界”:我当时的 setIn() 在索引超出当前列表大小时会自动扩容并填 null。这会导致一个离谱现象:把信号错接到 N1-2(非门第二个引脚),Not 门本来只有一个输入,却被扩充成两个输入,第二个永远 null,门就永久不可计算。
我的修复策略是:
在创建门对象时固定 inputs 大小;
setIn() 对越界引脚直接忽略或报错,不再“自动扩容”;
对悬空输入保持“不可计算→忽略输出”的语义一致性。
4)输出排序:组件编号提取错误
作业04要求按门类型排序,同类型再按编号排序。我用 getCompNum() 从名字末尾往前提取数字来作为编号,但当门名出现非预期格式(例如 A1b2)时,提取结果会变成 2 而非预期的 12,排序立刻混乱。更隐蔽的是:若门名完全不带数字,返回 0,多个 0 的门挤在一起,排序就变得不稳定。
最终我通过“输入命名规则收紧”来解决:门名必须满足既定格式(字母开头 + 纯数字编号 + 可选括号参数),否则直接报错/忽略,避免排序建立在不可靠的编号提取上。
5)wire 源端覆盖:输入名与门输出名冲突
外部输入信号可能写成 INPUT: A-1,而门输出默认是 name-0。如果外部输入名称碰巧与某个门输出 token 相同(例如 INPUT: O1-0),初始 signals 会把门输出位覆盖掉,导致门的真实输出被“强制固定”为输入值。这个问题很隐蔽,因为看起来像门逻辑错了,实际上是命名空间冲突。
我的修复方式是增加命名约束:外部输入名称不允许使用“门输出格式”的 token(例如以 -0 结尾的元件引脚),从源头避免覆盖冲突。
采坑心得:作业集05
作业集05的难点不只是“新增了器件”,更关键的是:这些器件会被放进组合电路里参与信号传播,因此任何“仅为输出打印做的临时表示”都会在组合场景下暴露问题。回头看,作业05我踩的坑大体可以归为三类:多输出器件的引脚编号语义、无效/高阻态的传播与清理、求值策略与输出规则的细节。
1)把“能打印”当成“能连线传播”
最开始实现译码器 M、数据分配器 F 时,我一度把它们的结果只当成“用于输出显示”的信息(例如用特殊键、逻辑序号、临时结构保存),这样在单独输出时看起来是对的。但当 M/F 作为上游器件参与组合电路时,下游元件取信号依赖的是“真实输出引脚号”。如果没有把结果按真实引脚号写回电路信号表,就会出现断信号:M/F 输出看起来有值,但下游始终读不到,隐藏点很容易挂。
最小复现(用于说明):让 M 输出驱动下游门,观察“能打印但下游读不到”的现象。建议你用题面译码器样例稍作改动:让 M(...) 的某个输出引脚 Yk 接到 N1-1 或 A(2)1-1,再让下游输出连到 OUT,即可稳定复现“未按真实引脚号写回信号表”导致的断信号问题(修复后下游应恢复可计算)。
2)多输出器件的“真实引脚编号”处理不完整
M 的输出引脚区间、F 的输出引脚区间必须严格按题面“控制-输入-输出”的编号规则映射。这个坑的典型特征是:题目样例可能过,但一旦出现组合电路、或者输出端接入更深层的门,编号错位就会导致全链路错。
我当时更容易出错的点,是“偏移量的推导”:控制端数量、输入端数量与输出端起始编号之间的关系一旦少加/多加 1,就会出现“输出落在不存在的引脚编号”或“把控制端当输出端”的错位。最终的修复方式是回到题面定义,从“控制—输入—输出”的顺序把每一段的编号区间写成公式,再用样例逐段验证(控制端是否落在 0..k-1、输入端是否紧随其后、输出端区间是否连续)。
3)无效状态/高阻态传播与“旧值残留”
三态门断开、控制端无效、输入不全等情况本质是“输出无效”。如果没有把自身输出、以及下游被驱动输入及时清空,就会出现“上一次有效值残留”。在复杂电路中,这类残留会被放大为随机 WA:看似同样输入,输出却受历史状态影响。
我在修复时的关键思路是:
把“无效”当成一种明确状态,而不是“默认 0/默认 1”;
当器件从有效→无效时,要显式清空对应的输出缓存;
求值更新时要保证无效结果会覆盖旧值,而不是被旧值短路。
最小复现(用于说明):让三态门控制端从 1 切到 0,观察输出是否被正确“清空”为无效。关键验证点是:控制端失效后,下游不应继续读取到上一次的有效值(否则就是旧值残留)。
4)数据选择器 Z 的有效输入判定
我曾按“所有数据输入都必须有效”来判断 Z 是否可计算,但更贴近题意/真实 MUX 的做法是:只需要“控制端有效 + 被选中的那一路数据有效”即可。这个差异很适合卡 1~2 个隐藏点:在未被选中的数据端缺失输入时,程序如果过度严格会把整元件判为无效,导致链路断开。
最小复现(用于说明):控制端固定选中 Dk,只给 Dk 提供输入,其他 D 留空。正确行为是输出仍可计算并传播;错误实现会因为“要求所有数据输入有效”而把整元件判无效。
5)输出规则边界:格式看似小,语义其实大
作业05的输出规则在不同器件之间差异明显:
哪些元件“整元件无效就忽略不输出”;
哪些元件“无效用 - 填充”(例如分配器输出串);
译码器输出的是“输出为 0 的引脚编号”,而不是电平串。
这些看起来像纯格式问题,但本质上是语义差异:你必须先在模型层区分“无效/缺失/高阻态”与“有效 0/有效 1”,才能在输出层做正确表现。
我最初最容易误解的是“无效状态的表现形式”:有的器件是“整元件无效就忽略不输出”,有的器件是“无效用 - 填充输出串”,译码器又是“输出为 0 的引脚编号”。当时如果把它们都当成统一的“输出电平”,就会在输出层产生看似格式问题、实则语义错误的 WA。后续我把“无效/高阻态”作为模型层的一等概念处理后,输出规则才变得可控。
6)求值策略稳定性:迭代传播 vs 递归取值
在作业05中我尝试过“迭代传播 + 缓存”的实现路线。如果没有正确处理失效覆盖,或者隐含依赖 HashMap 的遍历顺序,复杂链路下会出现不稳定。后来我更倾向“按连接关系取值/递归求值 + 缓存”的思路:数据依赖关系更明确,行为也更可控。
在迭代传播方案里,我遇到过两类不稳定:一是失效覆盖不彻底导致的“残留值”,二是对 HashMap 遍历顺序的隐式依赖导致的“传播顺序不稳定”。后来我更倾向在求值层明确“信号从哪里来”:通过连接关系递归追溯来源,并对已求得的节点做缓存,同时用访问栈/标记避免环路递归,这样行为更确定、也更容易写回归用例。
7)工程结构:中途混入后续迭代内容导致失控
这不是判题逻辑本身,但会直接影响最终交付质量:中途如果把“子电路等后续迭代内容”混入作业05实现,结构会越来越难控。最终我把代码回收成“单文件多类、职责拆分清晰”的 OOP 版本,反而更符合题目要求,也更容易维护与定位问题。
结构失控最直观的表现是:同一个规则在多个地方各写一份(解析、求值、输出各自“猜”一次编号与语义),导致改一处必漏一处;同时大量临时 if-else 把真实规则淹没,修一个 bug 引出另一个 bug。后续我把职责收敛成“解析层只管读入、模型层只管语义、求值层只管传播、输出层只管格式”,并把器件规则尽量下沉到各自的实现中,结构才稳定下来。
采坑心得:作业集06
这一部分的“坑”主要集中在三条链路上:解析层的输入归一化、子电路建模与命名隔离、异常检测的优先级与token分类。回头看,这些问题不是“算法不会”,而是对题面中“输入格式细节”和“规则优先级”的理解不够严格;一旦把这些细节写进代码,就会集中爆发在 CircuitParser、TextUtil、ConnectionValidator、ComponentRegistry 这几类模块里。
解析层:输入归一化相关
end 截断
题目要求 end 后内容忽略。如果继续读取 end 后的输入,容易把“垃圾行”误当成连接信息,导致解析阶段出现莫名其妙的组件/端口。我的处理策略是:读取输入时遇到 end 立即停止,并忽略后续所有内容。
BOM(Byte Order Mark)问题
输入首行可能带 BOM,肉眼看不出来,但会导致像 C1:、INPUT: 这种头部识别失败。解决方法是读入每行后先做一次 BOM 清理(例如替换 \uFEFF),保证后续字符串判断不被隐藏字符干扰。
全角冒号问题
OUT:、INPUT: 这种全角冒号非常隐蔽。若不做统一归一化,子电路头部与端口行会识别不到,最终表现为“子电路解析缺失/端口列表为空”。我最终在 TextUtil.normalizeColon() 里把多种冒号统一替换为英文冒号 :,让解析逻辑只面对一种格式。
空白分割问题
固定 split(" ") 会被多个空格、Tab、前后空白打爆。最终我统一采用 split("\s+"),并对 token 做 trim() 与空串过滤,保证解析行为稳定。
“按第一个 - 切”还是“按最后一个 - 切”
像 A-1 这种普通输入没问题,但更复杂名字中若包含多个 -,按第一个 - 切容易把“输入信号名”切错。我的经验是:解析“信号名—数值”时更稳妥的方式是按最后一个 - 切分,避免误伤更复杂的 token。
子电路建模:模板/实例、作用域与命名隔离
子电路“模板”和“实例引用”混淆
子电路定义本质是模板,主电路里的 C1-A 才是对端口的引用。一开始我容易把“模板内部节点”和“主电路引用节点”混在一起,导致连接关系错误。解决的关键是:在内部统一引入“扁平化命名”,将不同作用域的节点映射为全局唯一名称。
子电路 ID 处理不统一
如果把 C1: 存成 1,但主电路引用是 C1-A,连接时就根本匹配不到子电路。我的修复是:子电路 ID 始终以 C1 这种完整形式保存与匹配,避免在不同模块出现不一致约定。
子电路输入端口与输出端口没有隔离
同一个端口名若既参与输入又参与输出,在内部节点映射上很容易混掉。我的做法是采用 IN / OUT 进行区分(例如 C1__IN__A 与 C1__OUT__A),保证端口语义明确、不会互相覆盖。
子电路内部裸线与端口名混用
如果子电路内部存在未声明裸线,同时又有同名端口,很容易把两者当成同一节点。处理思路是:对“裸线、端口、元件引脚”做严格 token 分类,并在 flatten 时把端口与内部裸线映射到不同命名空间。
多子电路同名元件隔离
C1 和 C2 里都可能出现 N1。如果不做扁平化(例如 C1-N1、C2-N1),输出与求值会串线。我的修复策略是:在注册组件时根据所属子电路前缀生成 flatName,确保全局唯一。
异常检测:优先级、token 词法分类与冲突规则
异常优先级没严格按题面
题面给了固定优先级:
include more than one input → include none input → include none output → input and output sequence error → input signal conflict。
如果按直觉“先发现什么报什么”,很容易先报后面的异常,导致与评测不一致。我的做法是把校验过程按题面顺序组织成一条确定的判定链,严格按优先级输出。
把“门输入脚”误判成源
例如只看 N1 存在,就把 N1-1 当成合法输出源;但实际上只有 -0 才是门输出。这个错误会让 isDriverPin() 对 token 的词法分类失真,从而引发连锁错误(源/负载数量统计、冲突检测、顺序检测都可能错)。
把子电路异常与主电路异常写成两套逻辑
一旦主电路和子电路的异常判定条件不一致,就会出现同样非法连接在不同作用域“报错类型不一致/漏报”的问题。我的经验是:异常校验尽量复用同一套 token 分类与判定流程,只在 flatten/作用域映射上做差异处理。
单元素连接 "[A]" 的判错
[A] 这种如果 A 是合法源,应当是 include none output,而不是 include none input。这类案例本质在考“源/负载数量统计”的准确性:不能因为 tokens 只有 1 个就直接归到“无输入”。
冲突检测理解偏差
在“同一输入引脚是否能接受多个输出”这个点上,一度对“同源重复算不算冲突”摇摆。最终以题面约束为准:一个输入引脚不能连接多个输出引脚,冲突检测必须统一到“输入引脚→唯一驱动源”这条规则。
首 token 判定与整行判定标准不一致
如果首 token 用一套更保守规则、后续 token 用另一套更宽规则,会导致同一个 token 在不同位置身份不同,从而让“input and output sequence error”等判断出现随机性。最终应保证 token 分类函数对位置不敏感,或明确地只在“首 token 必须为源”这条规则上使用位置信息。
对子电路引用端口判定过宽/过窄
C1-A、C1-1 之类 token 在不同题设中可能是合法源/合法负载/非法 token。只要在 isDriverPin() 或 isLoadPin() 的判定上略有不一致,就会被隐藏点卡住。因此我最终把“引用子电路端口”的规则显式化:当 head 匹配到子电路 ID 时,tail 必须属于该子电路的输入端口或输出端口之一,否则按非法 token 处理。
case42:误把问题想复杂(复盘)
这一类问题给我的教训是:不要一上来就把问题扩大到“内部总线、全局归一化、复杂冲突模型”。在一些隐藏点里,命中的其实只是 token 的词法分类规则。我曾一度过度依赖“声明式语义”(例如只有出现在 INPUT: 的才算源、只有声明过的才算端口),但题目隐藏点更接近“纯大写裸 token 直接当源”的判定方式。最终命中的关键是:isDriverPin() 对“裸大写 token”的识别要与题面一致。
求值层:未知输出不能补默认值
在求值层我踩过一个非常致命的坑:当子电路输出未知时错误补 0。题面要求是“无法计算就忽略输出”,而不是“默认输出 0”。这类错误会直接打坏普通正确性点。
此外,传播轮数/递归求值若没有缓存与访问保护,会导致重复计算、性能差,甚至在存在环路时出问题。因此在求值实现中需要明确:
未知则返回“不可计算”;
缓存已计算结果;
用访问栈/标记防止递归死循环。
输出层:顺序与表现细节
输出层我踩的坑主要是“顺序”和“表现细节”两类:
多子电路输出顺序:在“全局混排”与“按电路块顺序逐块输出”之间摇摆;隐藏点往往更偏后者,即按子电路定义顺序逐块输出,块内再按类型与编号排序。
末尾多余换行:某些评测对最后一行后的空行敏感,需严格控制换行行为。
错误输出串的来源:在输出原始 rawLine 与输出规范化后的 displayLine 之间需要统一;否则可能出现 Presentation Error 或单点 WA。
实现策略:一次改太多导致不可控
在作业06的迭代里,一个“非功能性但极致致命”的坑是:一次改太多,同时动异常检测、子电路 flatten、冲突规则、输出顺序、输入解析。这样一旦改坏,很难定位是哪一刀导致的。后续更好的策略应是:每次只改一个模块、配一组回归用例,确保问题可控地收敛。
改进建议
OOP4:拆分 Main.main() 的职责
将解析、建图、求值、输出分别封装成独立方法/类,降低最大复杂度与嵌套深度,减少“一个改动牵动整段流程”的风险。
OOP5:降低 Decoder.computeOutputs() 的分支密度
把控制端有效性判断、编码计算、多输出写入拆成独立函数;并为译码器补充“控制端无效/输入缺失/极端编码”测试用例。
OOP6:分解 Component.recompute() 的 switch 维护热点
按门类型拆分为 computeAnd/computeOr/... 私有函数,或引入策略类/子类,让每种门的规则独立可测、独立演进。
通用:补充关键注释与测试记录
SourceMonitor 显示三次作业注释占比极低(最高 0.6%)。建议对“命名扁平化策略、异常优先级规则、输出格式差异”增加说明性注释,同时保留最小复现测试用例作为回归测试资产。
如果只选两条“最值得下次继续做”的改进:
把规则密集的热点方法拆开:例如 06 的 Component.recompute() 用策略/子类拆分,05 的 Decoder.computeOutputs() 做职责拆分。这样不仅降复杂度,更重要的是每一块都能独立写测试。
建立最小复现用例集做回归:把 BOM、全角冒号、优先级冲突、三态门无效残留、MUX 选中一路有效等用例固化下来,每次改动只跑这一套就能快速定位“是哪一刀引起回归”。
如果时间允许,我会进一步把“token 词法分类/命名扁平化/异常规则”抽成独立模块(甚至独立文件),避免在主逻辑里到处散落同一类判断。
总结
回看三次作业,自己的提升并不只在于“把规则写对”,更在于逐步建立了对复杂需求的分层意识:当系统从单一门电路扩展到组合器件、再扩展到子电路与异常校验时,架构需要承载的不再是单次实现,而是可持续演进。
本阶段我最明确的收获主要有五点:
输入不是“字符串”,而是协议:BOM、全角冒号、空白分割、end 截断这些细节决定了解析是否可靠。
命名空间与作用域是工程问题:子电路一引入,扁平化命名与端口隔离就变成“必须解决”的基础设施。
规则要按优先级写进程序结构:异常检测不是“发现什么报什么”,而是严格的判定链;写错顺序就会稳定 WA。
无效态是语义,不是默认值:高阻态/控制端无效/输入缺失都应该明确建模,不能靠补 0/补 1 让程序跑下去。
复杂度会迁移,不能消失:从 04 的 Main.main() 到 05 的 Decoder.computeOutputs() 再到 06 的 Component.recompute(),复杂度峰值不断换位置。真正有效的做法是让复杂度落在“可测试、可替换”的模块里。
后续我还需要继续补齐的短板主要是:
更系统的单元测试与回归测试组织方式(把最小复现用例固化);
更熟练的重构方法(在不破坏行为的前提下拆分热点函数与职责);
更规范的 UML 表达与文档习惯(让设计意图能被他人快速读懂)。
浙公网安备 33010602011771号