C#科学计算器核心:递归下降解析器与表达式求值实践
简介C#科学型计算器完整开发资料面向正在学习WinForms/WPF桌面编程的高校学生及需要完成课程设计或巩固C#事件驱动开发的读者。项目覆盖四则运算、对数、正弦余弦、百分比和二进制/八进制/十六进制转换以“界面控件布局—按钮事件绑定—数学函数调用—异常处理”为主线提供完整工程结构。源码中通过Math.Log、Math.Sin、Convert.ToString等API示例展示了科学计算与进制换算的实际写法同时包含try-catch对负数对数、除零等非法输入的提示处理。资源共46个文件、约209KB以13个.cs核心源码为主附带.resx/.resources界面资源、Form设计器、可运行exe、调试pdb和课程设计实施方案doc方便直接打开编译、运行验证并在此基础上扩展。已有350人学习下载是一份适合自学和课程答辩的实用C#项目样例。1. C#科学型计算器标题背后真正要解决的那道坎很多人看到“C#科学型计算器”这个标题第一反应是“不就是把按钮排一排文本框里显示个结果吗”。真动手做一次你会发现这个项目真正的分水岭根本不在界面上而在表达式解析用户敲进一串sin(30)2^3*(5-2)程序怎么知道先算括号、再算乘方、最后处理正弦这才是科学计算器区别于普通四则运算计算器的核心。用C#来做这件事恰好能把编译原理里“词法分析→语法分析→求值”的经典路径浓缩在一个几千行就能跑通的工程里既适合练手也适合作为后续做公式引擎、报表计算组件、脚本化规则系统的起步模板。这篇笔记就沿着“解析核心→函数库→交互→避坑→进阶复用”这条线展开读者不管是学生做课程设计还是工程师给内部工具加个公式计算模块都能照着落地。2. 先把表达式解析做对从字符串到可计算的树2.1 为什么C#做科学计算器要先处理字符串解析科学计算器和普通计算器的本质区别在于用户输入的是一个“表达式”而不是一串“已经排好的操作序列”。普通计算器按2 3 × 4每一步都是即时计算科学计算器则会等到你按下等号才把整串表达式按数学规则求值。这就要求程序先把字符串拆成有意义的单元——数字、运算符、函数名、括号——这叫词法分析再把单元按优先级组合成一棵树这叫语法分析。如果跳过这一步直接用正则匹配一段一段替换表达式一复杂就会出问题2^3^2到底是(2^3)^2还是2^(3^2)-2^2是(-2)^2还是-(2^2)这些在教科书里有明确约定但用简单字符串替换根本无从谈起。所以我做这个项目时第一步就下了决心写一个小型递归下降解析器。它不需要像编译器那样处理类型系统但要严格按“表达式→项→因子→指数→函数调用”的层级来定义语法这样才能保证每个运算符的优先级和结合性都可控。用递归下降解析还有一个额外好处错误定位清晰。用户输入1*2解析器能在“期望一个数字或左括号的位置却遇到*”这个节点上停下来准确地告诉用户错在哪。这一点对科学计算器很重要——用户不是计算机专业人员他们需要的是“你哪写错了”而不是一个崩溃弹窗。2.2 递归下降解析的最小实现一个能跑的骨架我给出的是一个精简但能完整跑通四则运算、括号、幂运算和单参数函数调用的骨架。完整工程里我会把它拆成Tokenizer、Parser、Evaluator三个类这里为了说明原理先放在一个文件里。using System; using System.Collections.Generic; using System.Linq; public class ScientificCalculator { private string _input; private int _pos; private ListToken _tokens; private enum TokenType { Number, Operator, Function, LeftParen, RightParen, Comma } private class Token { public TokenType Type; public string Text; public double Value; } // 词法分析把 sin(30)*2 切成 [sin, (, 30, ), *, 2] private ListToken Tokenize(string input) { var result new ListToken(); int i 0; while (i input.Length) { if (char.IsWhiteSpace(input[i])) { i; continue; } if (char.IsDigit(input[i])) { int start i; while (i input.Length (char.IsDigit(input[i]) || input[i] .)) i; string numStr input.Substring(start, i - start); result.Add(new Token { Type TokenType.Number, Text numStr, Value double.Parse(numStr) }); continue; } if (char.IsLetter(input[i])) { int start i; while (i input.Length char.IsLetter(input[i])) i; string func input.Substring(start, i - start).ToLower(); result.Add(new Token { Type TokenType.Function, Text func }); continue; } switch (input[i]) { case : result.Add(new Token { Type TokenType.Operator, Text }); break; case -: result.Add(new Token { Type TokenType.Operator, Text - }); break; case *: result.Add(new Token { Type TokenType.Operator, Text * }); break; case /: result.Add(new Token { Type TokenType.Operator, Text / }); break; case ^: result.Add(new Token { Type TokenType.Operator, Text ^ }); break; case (: result.Add(new Token { Type TokenType.LeftParen, Text ( }); break; case ): result.Add(new Token { Type TokenType.RightParen, Text ) }); break; default: throw new FormatException($无法识别的字符: {input[i]}); } i; } return result; } // 语法分析入口表达式 项 {(|-) 项} private double ParseExpression() { double left ParseTerm(); while (_pos _tokens.Count (_tokens[_pos].Text || _tokens[_pos].Text -)) { string op _tokens[_pos].Text; _pos; double right ParseTerm(); left op ? left right : left - right; } return left; } // 项 因子 {(*|/) 因子} private double ParseTerm() { double left ParseFactor(); while (_pos _tokens.Count (_tokens[_pos].Text * || _tokens[_pos].Text /)) { string op _tokens[_pos].Text; _pos; double right ParseFactor(); left op * ? left * right : left / right; } return left; } // 因子 指数 {^ 指数}右结合 private double ParseFactor() { double left ParseUnary(); if (_pos _tokens.Count _tokens[_pos].Text ^) { _pos; double right ParseFactor(); // 递归调用实现右结合 left Math.Pow(left, right); } return left; } // 一元运算符处理负号 private double ParseUnary() { if (_pos _tokens.Count _tokens[_pos].Text -) { _pos; return -ParseUnary(); } if (_pos _tokens.Count _tokens[_pos].Text ) { _pos; return ParseUnary(); } return ParsePrimary(); } // 基础单元数字、括号表达式、函数调用 private double ParsePrimary() { Token t _tokens[_pos]; if (t.Type TokenType.Number) { _pos; return t.Value; } if (t.Type TokenType.LeftParen) { _pos; double val ParseExpression(); if (_pos _tokens.Count || _tokens[_pos].Type ! TokenType.RightParen) throw new FormatException(括号不匹配); _pos; return val; } if (t.Type TokenType.Function) { string func t.Text; _pos; if (_pos _tokens.Count || _tokens[_pos].Type ! TokenType.LeftParen) throw new FormatException($函数 {func} 后缺少左括号); _pos; double arg ParseExpression(); if (_pos _tokens.Count || _tokens[_pos].Type ! TokenType.RightParen) throw new FormatException(函数调用缺少右括号); _pos; switch (func) { case sin: return Math.Sin(arg); case cos: return Math.Cos(arg); case tan: return Math.Tan(arg); case sqrt: return Math.Sqrt(arg); case log: return Math.Log10(arg); case ln: return Math.Log(arg); default: throw new FormatException($不支持的函数: {func}); } } throw new FormatException(表达式在意外位置结束); } public double Calculate(string input) { _input input; _pos 0; _tokens Tokenize(input); if (_tokens.Count 0) throw new FormatException(表达式为空); double result ParseExpression(); if (_pos _tokens.Count) throw new FormatException($表达式在 {_tokens[_pos].Text} 附近有多余内容); return result; } }这段代码的执行顺序是Calculate先调用Tokenize把字符串切成Token列表然后从ParseExpression开始递归下降。ParseExpression处理加减ParseTerm处理乘除ParseFactor处理乘方ParseUnary处理负号ParsePrimary处理数字、括号和函数调用。层级关系决定了优先级越底层的函数优先级越高所以乘方先于乘除乘除先于加减。有两个细节需要重点说明。一是乘方运算符的右结合性2^3^2应该等于2^(3^2)即512而不是(2^3)^2即64。代码里ParseFactor发现^后递归调用自身而不是调用ParseUnary这就保证了右结合。二是负号的优先级-2^2在数学约定里是-(2^2)即-4。因为ParseFactor先调ParseUnary处理负号然后才检查^所以-2^2会先吃掉-2这个一元表达式再对2做乘方不对仔细看ParseFactor的流程是left ParseUnary()此时ParseUnary会吃掉-2返回-2然后检查^所以-2^2会被解析成(-2)^2即4。这其实不符合数学约定。要修正的话应该让乘方优先于一元负号在ParseUnary里处理负号之前先检查后面是不是^。这是这个骨架的第一个“玄学”点后面避坑清单里我专门讲。2.3 运算符优先级与函数的边界哪些写法会翻车上面的骨架能跑通12*3、(12)*3、sin(30)但有几个边界写法一测就会翻车。第一个是-2^2按数学约定是-4但这个骨架给出4。要修正常见做法是把幂运算和一元负号的层级互换先让ParseUnary在遇到^时跳转到幂处理或者把幂处理的代码放到ParseUnary的下层。我自己的工程里是让ParseUnary先判断负号后递归调用ParseUnary但ParseUnary内部不再直接调ParseFactor而是单独处理幂这样-2^2就变成负号先作用于整个幂表达式得到-4。第二个是函数参数里再套函数比如sin(sqrt(4))。骨架里的ParsePrimary在处理函数参数时调用ParseExpression而ParseExpression最终会回到ParsePrimary所以嵌套函数天然支持。但如果你在词法分析时把函数名和变量名混在一起处理就会出问题。我的建议是函数表用字典维护词法分析阶段先查字典命中的归为Function没命中的如果后面紧跟左括号也归为函数否则报“未知标识符”。第三个是空括号()、函数缺参数sin()、多余的小数点1.2.3。这些都属于词法分析没做严格校验导致的。我一般会在Tokenize阶段就拦截连续两个运算符、小数点后没数字、括号不配对都在词法层直接抛异常错误信息写清楚“第几个字符附近出了问题”。解析器做得越严谨后面的UI层就越轻松这个取舍值得投入。3. 把函数库和精度控制装进计算核心3.1 数学函数库的选型思路自研还是借用科学计算器需要支持的函数通常包括三角函数sincostan及反三角函数asinacosatan对数lnlog幂与根sqrtcbrtpow还有阶乘、取整、绝对值、随机数等。在C#里这些功能大部分都能通过System.Math类直接获得不需要自己实现数值算法。Math.SinMath.CosMath.Tan的参数单位是弧度Math.Log是自然对数Math.Log10是常用对数Math.Pow做幂运算这些API的精度和性能都比自己手写泰勒展开可靠得多所以选型结论很直接能用System.Math就不自己造轮子。自己需要补充的主要是两件事。第一是角度与弧度的切换。用户习惯输入sin(30)并期望结果是0.5但Math.Sin(30)计算的是30弧度结果接近-0.988。常见的做法是在函数表里给每个三角函数配置一个“是否启用角度制”的标记计算时先做转换rad deg * Math.PI / 180。第二是用户自定义常量和函数比如输入pi返回3.14159265358979输入ans返回上一次计算结果。这些不属于System.Math范畴需要在解析器里提前把标识符表准备好。我用一个Dictionarystring, Funcdouble, double存一元函数一个Dictionarystring, double存常量这样扩展新函数时只需要往字典里加一条记录。3.2 如何在解析树里挂接三角函数、对数与常数要让sin(30)、ln(e)、2*pi都能正确求值关键是在解析器里建立“函数名→委托”的映射表。下面是我在实际项目中用的做法它比在ParsePrimary里写一长串switch更整洁也更容易扩展。public class FunctionRegistry { // 一元函数表名称 - (参数, 是否角度制) private Dictionarystring, (Funcdouble, double fn, bool useDegrees) _functions; // 常量表 private Dictionarystring, double _constants; public FunctionRegistry() { _functions new Dictionarystring, (Funcdouble, double, bool)(StringComparer.OrdinalIgnoreCase) { { sin, (Math.Sin, true) }, { cos, (Math.Cos, true) }, { tan, (Math.Tan, true) }, { asin, (Math.Asin, true) }, { acos, (Math.Acos, true) }, { atan, (Math.Atan, true) }, { sqrt, (Math.Sqrt, false) }, { cbrt, (Math.Cbrt, false) }, { ln, (Math.Log, false) }, { log, (Math.Log10, false) }, }; _constants new Dictionarystring, double(StringComparer.OrdinalIgnoreCase) { { pi, Math.PI }, { e, Math.E }, { tau, Math.Tau }, }; } public bool TryGetFunction(string name, out Funcdouble, double fn, out bool useDegrees) { if (_functions.TryGetValue(name, out var entry)) { fn entry.fn; useDegrees entry.useDegrees; return true; } fn null; useDegrees false; return false; } public bool TryGetConstant(string name, out double value) { return _constants.TryGetValue(name, out value); } public void AddFunction(string name, Funcdouble, double fn, bool useDegrees) { _functions[name] (fn, useDegrees); } }这段代码的核心价值在于把“函数支持哪些”和“函数怎么调用”解耦。解析器遇到函数名时只需要查表拿委托和角度制标记然后统一执行如果是角度制就arg * Math.PI / 180转成弧度再调用委托。以后想加abs、floor、ceil一行代码的事。想支持双参数函数pow(a,b)就需要额外维护一个Dictionarystring, Funcdouble, double, double并在解析器里增加一个“遇到逗号就继续解析参数”的分支这个我放到后面进阶部分讲。常量表的处理与之同理。词法分析阶段如果识别到字母序列先查函数表再查常量表两者都没命中就报“未知标识符”。常量的好处是让表达式更可读2*pi比2*3.14159265358979好看得多也避免了用户手动输入圆周率时的精度损失。3.3 精度陷阱浮点数显示、四舍五入与decimal取舍C# 的double是双精度浮点数有效数字约15~17位科学计算器这个场景用double完全够。但浮点数的二进制表示天然存在误差最典型的是0.1 0.2不等于0.3而是0.30000000000000004。如果直接把结果展示给用户体验会很差。所以我的习惯是计算内部全用double展示层统一走一个格式化函数。public static string FormatResult(double value) { if (double.IsNaN(value)) return 错误结果不是有效数字; if (double.IsInfinity(value)) return 错误结果超出可表示范围; // 针对接近整数的浮点误差做修正 double rounded Math.Round(value, 12); if (Math.Abs(value - rounded) 1e-10) value rounded; // 位数控制超过 10 位有效数字时使用科学计数法 string text value.ToString(G10); return text; }ToString(G10)表示保留10位有效数字超过自动转科学计数法。比如0.30000000000000004会格式化为0.31234567890123会变成1.23456789E12这个展示方式对科学计算器来说既准确又简洁。关于decimal我的建议是不要用。decimal精度虽高约28~29位有效数字但Math.Sin、Math.Cos、Math.Log这些函数都只接受double你用decimal还得先转double再算、再转回decimal精度优势尽失。而且decimal的运算速度比double慢不少在循环求值比如后面要讲的历史记录批量重算时差距明显。所以这条经验很直接计算核心统一double只有涉及货币或需要精确十进制表示的场景才考虑decimal科学计算器不属于这类场景。还有一个人人都可能踩的坑角度制下的tan(90°)。Math.Tan(Math.PI/2)在理论上应该是无穷大但因为Math.PI/2不是精确的90度计算结果是1.633123935319537E16这个巨大的数。用户看到这个数第一反应是程序坏了。处理方式有两种一是对正切函数在接近直角时做特殊判断当输入角度接近90 k*180时直接报“无定义”二是在展示层看到结果绝对值超过某个阈值时提示“结果可能无定义”。我在实际做的时候选了第一种因为它在计算层就把问题解决了展示层不需要再猜。4. 交互层与界面让计算器真的“科学”起来4.1 WPF还是WinForms这个标题下的选型理由做C#桌面计算器界面框架无非是WinForms和WPF二选一。WinForms胜在简单拖控件、写事件几百行代码就能做出一个能用的界面WPF胜在布局灵活、样式丰富特别是科学计算器这种按钮多、需要分组排列的界面用WPF的WrapPanel或Grid来做比WinForms的绝对定位要顺手得多。另外一个实际考量是高分屏适配WPF是矢量渲染缩放不模糊WinForms在4K屏上字小到看不清需要额外做DPI感知是个麻烦事。所以我的建议很明确新项目直接用WPF。虽然学习曲线陡一点但做科学计算器恰好是练习Grid布局、Command绑定、DataTemplate的好场景。如果你只是交课程设计且时间紧WinForms完全能完成任务如果你想把这个项目当作长期维护的工具或者后续加绘图、加复数计算WPF的潜力大得多。UI布局上我习惯把窗口分成三块顶部是表达式回显区用一个只读文本框显示用户输入中间是结果区用一个大字号只读文本框显示当前求值结果或错误信息底部是按钮区按功能分组排列——数字与小数点一组四则运算一组函数键一组sincostanlnlog功能键一组C清零、←退格、求值、()括号。分组不只为了美观更重要的是让用户形成肌肉记忆用久了不用看键盘就能找到按键。4.2 按键输入与表达式回显的一个可复现方案计算器的交互有两个方向一是点界面按钮二是直接敲键盘。两个方向都必须回显到表达式文本框里然后统一走计算核心。下面是我常用的做法它把输入源和计算逻辑完全分开。private void AppendToExpression(string text) { // 光标定位到末尾实现尾部追加 ExpressionBox.Focus(); ExpressionBox.CaretIndex ExpressionBox.Text.Length; ExpressionBox.SelectedText text; } private void OnDigitClick(object sender, RoutedEventArgs e) { Button btn (Button)sender; AppendToExpression(btn.Tag.ToString()); } private void OnFunctionClick(object sender, RoutedEventArgs e) { Button btn (Button)sender; string func btn.Tag.ToString(); AppendToExpression(${func}(); // 自动补左括号方便直接输入参数 } private void OnEqualsClick(object sender, RoutedEventArgs e) { try { double result _calculator.Calculate(ExpressionBox.Text); ResultBox.Text FormatResult(result); _lastResult result; _history.Add(ExpressionBox.Text ResultBox.Text); } catch (FormatException ex) { ResultBox.Text 输入错误 ex.Message; } catch (Exception ex) { ResultBox.Text 计算失败 ex.Message; } }这里有几个设计习惯值得照抄。第一按钮的Tag属性存要插入的文本而不是在事件里硬编码写死每个按钮的文本这样改按钮布局或增加新的函数键时不需要动事件代码。第二函数键自动补左括号用户点sin得到sin(光标停在括号内输入数字后自动闭合。这个细节让科学计算器的操作效率明显提升用户不需要手动去输入括号。第三所有计算都包在try-catch里解析器的异常信息直接展示到结果区程序永不崩溃。键盘输入的做法是给窗口挂PreviewKeyDown事件数字键、运算符、括号、回车求值、Esc清空分别映射到对应操作。但这里有一个WPF特有的坑如果焦点在某个按钮上按空格会触发按钮的点击事件导致重复输入。我的解决方式是给按钮设置FocusableFalse让焦点始终停留在文本框上键盘事件自然就统一了。这个问题在WinForms里不明显WPF里几乎一定会遇到。4.3 历史记录、内存变量与括号辅助的设计习惯历史记录是科学计算器里一个很加分的功能。我的做法是用一个ObservableCollectionstring存表达式和结果每次求值后添加一条sin(30) 0.5界面用ListBox绑定展示。用户双击历史列表项就把该表达式重新放回表达式框方便修改重算。这个功能的代码量不大但对使用体验的提升很明显尤其是做公式对比验证的时候。内存变量方面我实现了三个键MC清除内存、MR读取内存、MS存入内存外加M累加。实现方式就是维护一个double? _memory字段操作时直接读写。这个功能虽然传统但在科学计算场景里真的有用。比如你先算出圆的面积再去算圆柱体积直接MR调出来乘高就行不用重新输入或手动记数字。还有一个容易被忽略的辅助细节括号匹配提示。当用户在表达式里输入左括号时我建议在界面上实时统计左右括号数量数量不等时用颜色提示比如文本框边框变红或状态栏显示“括号未闭合”。我第一次做的时候没加这个用户输入了(12)*(34忘记闭合点了等号才看到错误信息。加了实时提示后这类问题在输入阶段就被发现了体验质变。实现不复杂在文本框的TextChanged事件里做括号计数就行。5. 科学计算器的避坑清单五个高频翻车点与现场修复5.1 现象输入-2^2结果居然显示4数学上-2^2的约定值是-4但解析器按“负号优先于乘方”处理时会先把-2解析成一个整体再对它做乘方得到(-2)^2 4。原因就是前面2.3节提到的ParseFactor先调ParseUnary导致负号的优先级高于^。解决调整层级顺序。我的做法是单独处理一元负号和幂的交互——ParseUnary遇到负号时先尝试解析一个“幂表达式”再把负号应用到幂结果上。修改后的逻辑是-2^2先解析2^2 4再取负得-4而(-2)^2因为括号的存在括号内先算出-2再平方得4。这个行为才符合数学约定。5.2 现象tan(90)得到一个超大数字而不是错误提示原因角度制下用户输入90度计算前转成弧度是Math.PI/2。但由于浮点精度限制这个值不是精确的直角Math.Tan算出来的结果逼近无穷大显示为一个16位的科学计数法数字。用户觉得“程序是不是疯了”其实程序没错是数学上这个值本就无定义。解决在函数表注册表里给tan增加一个预处理委托。当输入角度接近90 k*180误差在1e-10以内时直接返回NaN并在上层把NaN映射为“数学错误tan(90°) 无定义”。同理asin(2)也会触发Math.Asin返回NaN统一在展示层对NaN做文案适配即可。5.3 现象括号不匹配时解析器崩溃而不是给出提示我早期写的解析器在ParsePrimary里遇到意外的右括号时直接throw new Exception(index out of range)因为_pos已经超出Token列表长度抛出的异常信息完全不可读。原因词法分析阶段没有校验括号配对语法分析阶段对“随时可能出现的右括号”也没有防御。解决在ParsePrimary里遇到右括号时先判断当前Token类型如果是RightParen且当前不是在处理括号表达式就直接抛FormatException(多余的右括号)。同时在Calculate的入口处对_tokens做一次括号数量统计左括号数不等于右括号数时直接报“括号不匹配”提前拦截这一大类错误。这两处加上后所有括号相关的错误都会以清晰的文本出现在结果区。5.4 现象WPF窗口里键盘输入被按钮“吃掉”点界面按钮是正常的但切换成键盘输入时数字按不出来或按一下出现两个字符。原因焦点在某个按钮上时键盘事件被按钮捕获空格键和回车键会触发按钮的点击而不是文本框的输入。解决所有科学计算器的按键都设置FocusableFalse并在窗口层面统一处理PreviewKeyDown。这样焦点永远停留在表达式文本框上键盘输入直接进文本框按钮点击和键盘输入两条路径互不干扰。这是我在这个项目里花时间最多但收益最大的一次修正希望后来者直接照做别自己再趟一遍。5.5 现象连续计算几十次后程序明显变卡原因历史记录列表无限制增长每次求值都往ObservableCollection里加一条而且如果界面做了复杂的ListBox数据模板每次添加新项都会触发完整的UI刷新。这个在数据量小的时候没感觉累积到几百条就能明显感知。解决给历史记录加一个上限比如保留最近100条超过时移除最旧的。同时在历史记录的ItemsSource绑定上尽量用最简单的显示模板不要在ListBoxItem里放复杂的嵌套控件。另外一个隐藏优化如果历史列表只需要在用户点击时才更新可以改成“按需刷新”而不是实时刷新也就是求值后只更新结果区历史列表延时到用户打开历史面板时再一次性同步。6. 进阶把计算核心做成可复用组件用测试给行为上保险这个标题走到最后值得做的一件事是把解析器从窗体代码里彻底分离出来封装成一个独立的计算引擎类库并提供一套单元测试作为“行为契约”。我第一次做的时候解析器和窗体事件混在同一个文件里计算逻辑和UI逻辑互相纠缠想加一个新函数都得担心会不会碰坏界面。后来我把核心抽成了CalculatorEngine类窗体和引擎之间只通过Calculate(string expression)和FunctionRegistry交互整个项目的可维护性立刻不一样了。推荐照着这个结构做一个类库项目放解析器、函数注册表、格式化器一个WPF项目只做界面一个测试项目跑解析器的用例。有了清晰的边界就可以开始维护一个回归测试集。每修一个bug就先写一个对应的测试用例把“哪个表达式应该得出什么结果”固定下来。比如避坑清单里的-2^2应该等于-4、tan(90)应该触发错误提示、0.10.2应该展示为0.3这些都可以写成参数化测试。以后改代码时跑一遍测试就知道有没有把别的功能改坏。这一步是纯赚的测试代码量不大但每次重构都有了安全网。引擎的扩展能力也值得预留。我给自己留的扩展接口有三个方向双参数函数pow(a,b)、log(b,a)、自定义变量赋值x5之后表达式里就能用x、以及表达式缓存对同一个字符串做哈希命中缓存就直接返回上次结果。其中表达式缓存在批量计算场景下性能提升明显——比如你在循环里对一万行数据套同一个模板解析器每行都重新做词法分析和语法分析而缓存能让性能翻倍。不过要注意缓存的键不能只存表达式文本还得包含函数表、常量表的版本号否则用户改了自定义常量后旧缓存会给出错误结果。这个项目做到最后我对“计算器”这件事的认知也变了不少。它不是一个可以拿现成控件库糊弄的小玩意而是一个麻雀虽小五脏俱全的编译原理实践场。每次修掉一个边界条件本质上都是在补全你对“语法如何对应语义”的理解。希望这篇笔记能把你在第一个sin(输入之前该做的决策、该避的坑都提前交代清楚后面走起来更顺。希望帮到你。本文还有配套的精品资源点击获取

相关新闻

OCLP-Mod 后台守护进程机制全解析:自动根补丁监控、更新预缓存与 KDK 管理

OCLP-Mod 后台守护进程机制全解析:自动根补丁监控、更新预缓存与 KDK 管理

桌面应用CLI系统编程 【免费下载链接】OCLP-Mod A mod version for OCLP,with more interesting features. 项目地址: https://gitcode.com/gh_mirrors/oc/OCLP-Mod 点击查看 免费下载 OCLP-Mod 通过一组常驻后台的 launchd 服务(LaunchAgent / LaunchD…

2026/10/14 20:22:41 阅读更多 →
权威测评!2026年最火AI论文工具榜单,免费款也能高效产初稿

权威测评!2026年最火AI论文工具榜单,免费款也能高效产初稿

一、实测方法论:5 大维度量化评估 本次实测覆盖全学科论文场景,重点考察以下维度,结果客观可复现: 表格 评估维度权重测评标准中文适配度25%是否符合 GB/T7714 引用规范,语言逻辑自然度,术语准确性免费功能…

2026/10/14 20:21:39 阅读更多 →
Linux嵌入式SD/MMC驱动调试:信号流向、三层架构与实战排错

Linux嵌入式SD/MMC驱动调试:信号流向、三层架构与实战排错

1. 项目概述:为什么一个SD/MMC驱动分析值得花时间深挖?在嵌入式Linux开发一线干了十多年,我经手过的板子从ARM9、ARM11到Cortex-A7/A53/A72,再到如今的RISC-V SoC,几乎每一块主控芯片的BSP包里,SD/MMC子系统…

2026/10/14 20:21:39 阅读更多 →

最新新闻

AI高薪风口来袭!小白程序员如何抓住机遇,收藏这份学习指南?

AI高薪风口来袭!小白程序员如何抓住机遇,收藏这份学习指南?

文章主要介绍了AI行业的薪资现状,从实习生日薪5000元到年薪上亿,AI人才市场正在经历一场白热化的争夺战。文章指出,AI正在“入侵”每一个行业,人才缺口超过500万,普通人有机会分到一杯羹。文章建议普通人通过系统学习掌…

2026/10/14 21:14:53 阅读更多 →
大厂校招薪资大揭秘:小白程序员必看,收藏这份热门岗位薪资指南!

大厂校招薪资大揭秘:小白程序员必看,收藏这份热门岗位薪资指南!

本文汇总了字节跳动、腾讯、阿里巴巴等热门大厂校招薪资情况,涵盖前后端开发、AI/Agent方向、测开等岗位。数据来源于往届案例及公开信息,详细展示了不同级别薪资待遇及福利。文章提醒求职者在选择Offer时,不仅要关注总包,还要了解…

2026/10/14 21:14:53 阅读更多 →
如何快速上手cad-viewer:10分钟在浏览器中打开你的第一份DWG/DWG图纸

如何快速上手cad-viewer:10分钟在浏览器中打开你的第一份DWG/DWG图纸

前端3D渲染图形学 【免费下载链接】cad-viewer The world’s first fully web-based DXF/DWG viewer and editor that runs entirely in the browser — no backend server required. 项目地址: https://gitcode.com/gh_mirrors/cad/cad-viewer 点击查看 免费下载 …

2026/10/14 21:14:53 阅读更多 →
大模型高薪诱惑:年薪百万?小白必看收藏!

大模型高薪诱惑:年薪百万?小白必看收藏!

一篇顶会一作AIGC方向211本硕,某短视频大厂算法岗月入4.5万,实际年薪或超百万。但需注意,高薪并非保底,且薪资构成复杂,包括签字费、期权等。大厂算法岗普通档薪资为26k-36k,该岗位薪资远超普通水平&#x…

2026/10/14 21:14:53 阅读更多 →
AI工程师两大门派:传统算法与AI大模型应用开发,收藏这份入门指南!

AI工程师两大门派:传统算法与AI大模型应用开发,收藏这份入门指南!

文章介绍了AI领域的两大门派:传统算法工程师和AI大模型应用开发工程师。前者是“基建狂魔”,通过数学和编程解决实际问题,如精准推荐、最优路线规划等;后者是“场景魔术师”,利用现成大模型解决业务落地难题&#xff0…

2026/10/14 21:14:53 阅读更多 →
阿里云短信签名模板审核避坑指南与Python调用实战

阿里云短信签名模板审核避坑指南与Python调用实战

简介:本资源是一份面向Java开发者的企业级短信集成实战示例,聚焦阿里云短信服务(Dysmsapi)的快速接入与稳定调用,适用于用户注册验证、密码重置、订单通知等典型业务场景。压缩包共92个文件,含68个C#源码文…

2026/10/14 21:13:53 阅读更多 →

日新闻

AnyPS5通用适配层设计:兼容性与性能的工程取舍

AnyPS5通用适配层设计:兼容性与性能的工程取舍

1. 从"AnyPS5"这个名字说起:它到底想解决什么问题第一次看到"AnyPS5"这个标题,我脑子里冒出来的第一个念头是:这大概率是一个围绕"跨平台运行"或者"通用适配"做文章的项目。名字里的"Any"…

2026/10/14 0:00:41 阅读更多 →
Flutter跨平台开发实战:二手交易应用与HarmonyOS适配

Flutter跨平台开发实战:二手交易应用与HarmonyOS适配

1. 项目概述:Flutter跨平台二手交易应用开发作为一名长期从事移动应用开发的工程师,我最近完成了一个基于Flutter框架的二手物品交易平台项目。这个项目最吸引我的地方在于它完美结合了Flutter的跨平台优势与HarmonyOS的生态特性,实现了"…

2026/10/14 0:00:41 阅读更多 →
DFS算法解决飞机降落调度问题实战

DFS算法解决飞机降落调度问题实战

1. 项目背景与核心思路飞机降落问题是一个经典的调度优化场景,也是算法竞赛和面试中的高频题目。这个题目要求我们合理安排多架飞机的降落顺序和时间,确保所有飞机都能安全着陆而不发生冲突。在实际航空管制中,这类问题直接关系到飞行安全和机…

2026/10/14 0:00:41 阅读更多 →

周新闻

流感时间序列预测实战:ARIMA/LSTM全流程拆解与避坑指南

流感时间序列预测实战:ARIMA/LSTM全流程拆解与避坑指南

简介:基于 ARIMA、LSTM、Transformer 等模型的流感时间序列预测 Python 源码,面向计算机相关专业课程设计与期末大作业学生,以及项目实战学习者。内容覆盖预处理、平稳性检验、定阶、残差分析、多模型对比预测的完整时序建模流程,…

2026/10/14 3:30:28 阅读更多 →
影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别

影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别

影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别 做影刀RPA自动化,十个新手有八个栽在"往输入框里填东西"这件事上:要么填不进去,要么填了一半,要么直接把原来内容追加在后面。这背后的根因&…

2026/10/14 16:30:29 阅读更多 →
影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容

影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容

影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容 1. 认识影刀:什么场景该用RPA采小说数据 起点中文网的页面结构相对稳定——分类榜单、书籍详情、章节内容三块独立页面,跳转链路清晰。这种场景非常适合影刀自动化&#x…

2026/10/14 3:29:58 阅读更多 →

月新闻

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/13 22:01:27 阅读更多 →
Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/13 14:57:39 阅读更多 →
黑夜航拍船只数据集训练YOLOV5模型全流程解析

黑夜航拍船只数据集训练YOLOV5模型全流程解析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/14 7:53:34 阅读更多 →