☰
C#科学型计算器实战:表达式解析与求值从零实现
2026/10/14 18:24:54 网站建设 项目流程

简介:一套基于窗体应用的C#科学型计算器完整工程,面向初学与课程设计人群,解决从图形界面搭建到数学运算、进制转换等核心功能的实现问题。压缩包共46个文件,含13个C#源码、界面资源文件、可执行程序及工程配置等,整包仅209KB,结构精简便于按模块阅读。已有350人学习下载,该项目不仅覆盖加减乘除、百分比,还实现对数、正弦、余弦及二进制、八进制、十六进制转换,并加入异常捕获机制,可应对无效输入与特殊运算边界。代码按窗体拆分,普通计算与科学计算界面分离,同时附带课程设计实施方案文档,便于对照设计思路、理解事件绑定与数学函数库调用方法。无论是作为课程设计参考,还是自己动手扩展按钮与面板,这套小体积资源都能提供完整可运行的起点。

1. C#科学型计算器:表达式解析才是这个项目的真正门槛

很多学C#的朋友第一次做项目都会挑“计算器”下手,但做完才发现自己写的是个只能算“1+1”的玩具——界面摆了十几二十个按钮,遇到括号、负号、三角函数就全线崩溃。真正能支撑起“科学型”三个字的,是表达式解析与求值这条技术线。这套C#科学型计算器实现,不只是把按钮映射到数字,而是完整实现了中缀表达式转后缀、科学函数分派、异常输入兜底三条核心链路。适合刚掌握C#语法、想通过一个完整WinForms项目把集合、委托、正则、异常处理串起来的人,也适合需要一个能扩展自定义函数求值器的开发者。

2. 中缀转后缀:表达式解析的核心算法与实现

2.1 为什么不用DataTable.Compute当捷径

很多初学者发现DataTable.Compute("(1+2)*3", null)能直接算出结果,就以为找到了捷径。把这个方案放进科学型计算器场景里,问题立刻暴露:第一,DataTable.Compute不认sin、cos、log这些函数名,输入"sin(30)"直接抛异常,你得先预处理替换成Math.Sin;第二,运算符优先级在某些带负号的场景下解析结果和数学约定不一致,表现很“玄学”;第三,它抛出的错误信息是组件内部的语法错误,在UI层基本不可读,用户看到“Syntax error”完全不知道错在哪里。

所以在正规的求值器实现里,自己用栈解析表达式是绕不开的基础功。看起来多写了百来行代码,但控制权完全在自己手里,后续加函数、加运算符、加自定义变量都是在一张表里加条目的事。解析链条拆成三步:词法分析把字符串切成Token、中缀转后缀理顺运算顺序、后缀求值真正算出结果。每一层职责单一,出了问题也容易定位。

2.2 词法分析:先把字符串拆成Token

表达式解析的第一步不是计算,而是切分。把"12.5+sin(30)*2"切成数字12.5、加号、函数名sin、左括号、数字30、右括号、乘号、数字2。这一步不做好,后面优先级判断和括号匹配全是空中楼阁。实现方式是逐字符扫描,配合正则或字符类型判断把连续的数字、字母拼成一个Token。

// 定义Token类型和结构 public enum TokenType { Number, Operator, Function, LeftParen, RightParen } public struct Token { public TokenType Type; public string Value; } // 词法分析核心逻辑 private List<Token> Tokenize(string expr) { List<Token> tokens = new List<Token>(); int i = 0; while (i < expr.Length) { char c = expr[i]; if (char.IsWhiteSpace(c)) { i++; continue; } if (char.IsDigit(c) || c == '.') { int start = i; while (i < expr.Length && (char.IsDigit(expr[i]) || expr[i] == '.')) i++; tokens.Add(new Token { Type = TokenType.Number, Value = expr.Substring(start, i - start) }); continue; } if (char.IsLetter(c)) { int start = i; while (i < expr.Length && char.IsLetter(expr[i])) i++; tokens.Add(new Token { Type = TokenType.Function, Value = expr.Substring(start, i - start) }); continue; } if (c == '(') { tokens.Add(new Token { Type = TokenType.LeftParen, Value = "(" }); i++; continue; } if (c == ')') { tokens.Add(new Token { Type = TokenType.RightParen, Value = ")" }); i++; continue; } if ("+-*/^".IndexOf(c) >= 0) { tokens.Add(new Token { Type = TokenType.Operator, Value = c.ToString() }); i++; continue; } throw new FormatException($"无法识别的字符: {c}"); } return tokens; }

逻辑说明:数字部分用char.IsDigit和'.'连续扫描,支持带小数的输入;函数名部分用char.IsLetter把连续字母拼成一个完整的函数名,比如sin、cos、sqrt;运算符限定为加减乘除和乘方五个符号。空格直接跳过,非法字符立刻抛异常,异常信息里带上原始字符,方便UI层定位。

参数说明:这里有一个容易被忽略的点——数字前缀的负号和运算符减号在词法层没有区分,都走Operator分支。这意味着"-3+2"会被切成一个Operator("-")加一个Number("3")。严格来说,这个负号应该作为一元运算符处理。直接把代码跑起来你会发现"(-3)+2"算得对,但"-3+2"就会报“表达式不完整”。这个坑具体怎么解,第4章专门讲。

2.3 优先级表与中缀转后缀

计算机直接算中缀表达式要反复回溯处理优先级和括号,效率低还容易出错。常见做法是先把中缀转成后缀表达式(逆波兰式),转换期间用一个运算符栈保存还没落位的运算符,数字和函数名直接按顺序输出,最后输出序列就是后缀式。比如"(1+2)*3"转成"1 2 + 3 *",求值顺序一目了然。

// 运算符优先级表,数字越大优先级越高 private static readonly Dictionary<string, int> Precedence = new Dictionary<string, int> { { "+", 1 }, { "-", 1 }, { "*", 2 }, { "/", 2 }, { "^", 3 } }; // 中缀转后缀核心方法 private List<Token> ToRpn(List<Token> tokens) { Stack<Token> opStack = new Stack<Token>(); List<Token> output = new List<Token>(); foreach (Token tok in tokens) { if (tok.Type == TokenType.Number || tok.Type == TokenType.Function) { output.Add(tok); } else if (tok.Type == TokenType.Operator) { while (opStack.Count > 0 && opStack.Peek().Type == TokenType.Operator && Precedence[opStack.Peek().Value] >= Precedence[tok.Value]) { output.Add(opStack.Pop()); } opStack.Push(tok); } else if (tok.Type == TokenType.LeftParen) { opStack.Push(tok); } else if (tok.Type == TokenType.RightParen) { while (opStack.Count > 0 && opStack.Peek().Type != TokenType.LeftParen) { output.Add(opStack.Pop()); } if (opStack.Count > 0 && opStack.Peek().Type == TokenType.LeftParen) opStack.Pop(); else throw new FormatException("括号不匹配:存在多余的右括号"); } } while (opStack.Count > 0) { if (opStack.Peek().Type == TokenType.LeftParen) throw new FormatException("括号不匹配:存在多余的左括号"); output.Add(opStack.Pop()); } return output; }

逻辑说明:operator分支里的while循环是这个算法的关键。当新运算符到来时,栈顶只要有优先级不低于它的运算符,就先弹出进输出序列。这个“不低于”保证了同优先级运算从左到右执行,比如"8/4/2"会被转换成"8 4 / 2 /",而不是"8 4 2 / /"。括号处理是另一个容易出错的地方:左括号直接压栈,右括号出现时把栈里直到左括号为止的运算符全部弹出。如果弹出过程中栈已经空了还没见到左括号,说明右括号多余;清空栈阶段如果栈顶还有左括号没被配对,说明左括号多余,两种情况都要抛异常。

参数说明:乘方运算理论上应该是右结合,即"2^3^2"等于2^(3^2)而不是(2^3)^2。如果要求严格,需要给乘方单独设置“栈顶优先级严格大于当前才弹出”的规则。上面代码为了方便统一用>=处理,日常计算够用,但你要做数学严谨的求值器,这里得单独加判断。

提示:字符串里的空格在Tokenize阶段已经被跳过了,但用户从外部粘贴表达式时可能带入全角空格或制表符,建议在进入Tokenize前先做一次Trim和全角替换,详见第5章的NormalizeInput方法。

2.4 后缀表达式求值:栈操作的最后一公里

后缀式子出来之后,求值就简单了:从左往右扫描,遇到数字压栈,遇到运算符弹出两个数计算,把结果压回去。这里有一个细节:弹出两个数时,先弹出的是右侧操作数,后弹出的是左侧操作数,减法除法尤其要注意顺序,写成right-left就是3,写成left-right就是-3,方向错了整个计算结果都是错的。

private double EvaluateRpn(List<Token> rpn) { Stack<double> stack = new Stack<double>(); foreach (Token tok in rpn) { if (tok.Type == TokenType.Number) { if (!double.TryParse(tok.Value, out double val)) throw new FormatException($"数字格式错误: {tok.Value}"); stack.Push(val); } else if (tok.Type == TokenType.Operator) { if (stack.Count < 2) throw new FormatException("运算符缺少操作数"); double right = stack.Pop(); double left = stack.Pop(); switch (tok.Value) { case "+": stack.Push(left + right); break; case "-": stack.Push(left - right); break; case "*": stack.Push(left * right); break; case "/": if (right == 0) throw new DivideByZeroException("除数为零"); stack.Push(left / right); break; case "^": stack.Push(Math.Pow(left, right)); break; } } else if (tok.Type == TokenType.Function) { if (stack.Count < 1) throw new FormatException("函数缺少参数"); double arg = stack.Pop(); switch (tok.Value.ToLower()) { case "sin": stack.Push(Math.Sin(arg)); break; case "cos": stack.Push(Math.Cos(arg)); break; case "tan": stack.Push(Math.Tan(arg)); break; case "sqrt": if (arg < 0) throw new ArgumentOutOfRangeException("sqrt的参数不能为负数"); stack.Push(Math.Sqrt(arg)); break; case "log": stack.Push(Math.Log10(arg)); break; case "ln": stack.Push(Math.Log(arg)); break; default: throw new FormatException($"未知函数: {tok.Value}"); } } } if (stack.Count != 1) throw new FormatException("表达式不完整"); return stack.Pop(); }

逻辑说明:求值器全程用double精度计算。除法前先检查右操作数是否为零,避免把无穷大算出来再显示成“∞”这种在桌面计算器里很突兀的结果。sqrt和log的参数合法性也在函数调用前拦截,把异常信息写清楚,UI层可以直接弹给用户。函数分支用switch做分派,后续要加新函数只需要加一个case,不需要改动解析流程。

参数说明:角度制和弧度制的争议在这里。Math.Sin默认接收弧度,而大多数人习惯输入度数。如果你希望sin(30)按30度算,需要在求值前统一转弧度,比如Math.Sin(arg * Math.PI / 180)。是做一个“度/弧度切换”按钮还是固定用弧度制,建议在UI层直接标明,避免用户拿着计算器按sin(30)发现结果不对来投诉。这套源码默认用弧度制,但保留了一个内部转换方法,改起来很方便。

3. 科学函数与UI交互:让计算器真正“科学”起来

3.1 函数映射表:按钮和解析器之间的一层桥

计算器面板上有一堆按钮,每个按钮往输入框里塞的文本不一样:数字按钮塞"1"、"2",运算符按钮塞"+"、"-",“Sin”按钮塞"sin(",这样用户看着是点按钮,实际是在构建表达式字符串。实现上不需要给每个按钮单独写Click事件,用WinForms的Tag属性存命令文本,统一用一个事件处理器就够。

// 按钮统一事件:把对应文本追加到输入框 private void BtnInput_Click(object sender, EventArgs e) { if (sender is Button btn && btn.Tag is string text) { txtExpression.Text += text; txtExpression.Focus(); txtExpression.SelectionStart = txtExpression.Text.Length; } } // 按钮Tag设置示例(Form构造函数或设计器里配置) // btnSin.Tag = "sin("; // btnSqrt.Tag = "sqrt("; // btnPi.Tag = "3.14159265358979";

逻辑说明:所有输入型按钮共用一个Click处理器,用sender的Tag属性拿到要插入的文本,追加到表达式文本框末尾。这样加新按钮不需要新增事件处理方法,在设计器里拖个按钮、填一个Tag属性,功能就通了。把光标塞到末尾这步很重要,否则用户连续点按钮时新文本会插到光标所在位置而不是末尾。

参数说明:Tag里存的是表达式片段,比如sin按钮存的是"sin(",其中左括号直接带上了,能避免用户插入函数后还要手动补括号。π按钮在这里没有走Tokenize的数字分支,因为字符'π'不在识别范围。常见处理是Tag里存"3.14159265358979"而不是"π"。源码采用前者,简单直接,代价是显示的时候用户看到一长串小数而不是π符号,这个问题不大,习惯就好。

3.2 文本框过滤与实时校验

表达式文本框有两种设计路线:一种是等用户点“=”再统一解析,一种是在输入过程中实时校验并高亮错误。前者实现简单,后者体验好但要在TextChanged事件里频繁触发Tokenize。考虑到解析本身很快,可以做成输入失焦时校验一次,点“=”时再完整校验一次的双重策略。

// 输入框失焦时给出即时提示 private void TxtExpression_Leave(object sender, EventArgs e) { string expr = txtExpression.Text.Trim(); if (string.IsNullOrEmpty(expr)) return; try { List<Token> tokens = Tokenize(expr); List<Token> rpn = ToRpn(tokens); errorProvider1.Clear(); } catch (Exception ex) { errorProvider1.SetError(txtExpression, "检查输入:" + ex.Message); } } // 点击等号执行完整计算 private void BtnEquals_Click(object sender, EventArgs e) { string expr = txtExpression.Text.Trim(); if (string.IsNullOrEmpty(expr)) return; try { List<Token> tokens = Tokenize(expr); List<Token> rpn = ToRpn(tokens); double result = EvaluateRpn(rpn); txtResult.Text = FormatResult(result); lstHistory.Items.Insert(0, $"{expr} = {FormatResult(result)}"); } catch (Exception ex) { MessageBox.Show("无法计算:" + ex.Message, "表达式错误"); } }

逻辑说明:失焦校验只做解析不做求值,因为求值可能有除零这类运行期错误,失焦时就弹窗会烦人。点等号时才真正求值并把结果写进另一个文本标签,同时把表达式和结果拼成一条历史记录插到ListBox最前面。用ErrorProvider控件在输入框旁显示红色小图标,比MessageBox温和得多。

参数说明:ErrorProvider的SetError方法第一个参数是控件,第二个是错误文本,清除时传空字符串。这套逻辑把“语法检查”和“运行计算”分开,语法问题用图标提示,运行错误用弹窗说明,是桌面计算器比较稳妥的交互模式。初学者容易把两类错误混在一起处理,结果就是用户输错一个括号被弹窗打断五次,体验很差。

3.3 显示格式:double的15位有效数字怎么展示

double类型默认ToString()会输出类似"0.30000000000000004"这样的长串,科学型计算器需要自己控制显示位数。常见的做法是保留12位有效数字并去掉尾部多余的零。下面这个方法可以直接复用。

// 结果格式化:最多12位有效数字,去掉末尾多余的0 private string FormatResult(double value) { if (double.IsNaN(value)) return "非数字结果"; if (double.IsInfinity(value)) return "无穷大"; if (Math.Abs(value) < 1e-15) return "0"; return value.ToString("G12"); }

逻辑说明:"G12"是C#内置的格式符,表示保留最多12位有效数字,整数部分过长时自动切换成科学计数法。末尾多余的零由格式化器自动清理,不需要手写正则。判断NaN和Infinity放在最前面,避免把异常数值显示成奇怪字符串。“绝对值小于1e-15按0处理”这一步是因为浮点误差的存在——0.1+0.2算出来可能是0.30000000000000004,直接显示会吓到用户。

参数说明:G后面跟的数字可以根据精度需求调整。G15是double能保证的极限精度,但日常计算显示15位大部分时候都嫌长,12位在视觉和精度之间算一个平衡点。如果你希望结果永远是定点十进制而不是科学计数法,改成"F10"再TrimEnd('0')也可以,但大数时会显示一长串零,两个方案各有取舍。

4. 避坑与常见问题排查:表达式计算最容易翻车的四个位置

4.1 负号还是减号:一元运算符怎么处理

现象:输入"-3+2"点击计算,报“表达式不完整”或“运算符缺少操作数”,但输入"0-3+2"或"(-3)+2"都正常。

原因:词法分析阶段把"-"一律当成二元运算符,放在表达式开头时没有左操作数,后缀求值时栈里只有一个数却要弹出两个。double类型本身能表达负数,但求值器无法区分它面对的是“减一个正数”还是“一个负数参与运算”。

解决:处理Token序列时,先判断这个"-"是二元减号还是一元负号。简单规则是:如果减号出现在表达式开头、左括号之后、或者另一个运算符之后,那它就是一元负号;否则是二元减号。一元负号可以转成一个特殊的函数名"neg",这样它就走函数分支,不占操作数位置。

// 在Tokenize之后、ToRpn之前插入一元负号修正 private List<Token> NormalizeUnaryMinus(List<Token> tokens) { List<Token> result = new List<Token>(); bool expectOperand = true; // 期望操作数时,减号视为一元 for (int i = 0; i < tokens.Count; i++) { Token tok = tokens[i]; if (tok.Type == TokenType.Operator && tok.Value == "-" && expectOperand) { result.Add(new Token { Type = TokenType.Function, Value = "neg" }); } else { result.Add(tok); } if (tok.Type == TokenType.Number || tok.Type == TokenType.Function || tok.Type == TokenType.RightParen) { expectOperand = false; } else { expectOperand = true; } } return result; }

逻辑说明:expectOperand标志位记录下一个出现的减号是不是一元。表达式开头、左括号后、运算符后都处于“期望操作数”状态,这时减号是负号,替换成neg函数;数字、函数、右括号后面出现的减号是二元运算符。替换之后,"3-2"里的减号保持原样," -3+2"变成了"neg(3)+2",正常走函数求值流程。

参数说明:EvaluateRpn里需要加一个分支,让neg函数执行取负操作:

case "neg": stack.Push(-arg); break;

这样"2*-3"也能正确处理,因为乘号后面出现的"-"同样被判定为一元负号,转了neg函数。注意这步转换必须在ToRpn之前做,否则"neg"作为函数名会在Tokenize阶段被误判。

4.2 括号匹配的边界:空括号和连续表达式

现象:输入"sin()"或"(1+2))"时,解析器有时候不报错,但结果明显不对;输入"(1+2"直接报括号不匹配。

原因:括号匹配逻辑只检查了左右括号数量相等,没检查括号内是否存在合法内容。空括号在数学上无定义,但数量上左右各一个,能通过匹配检查,进到求值阶段才会因为“函数缺少参数”而报错。

解决:在ToRpn里加一个检查,检测到左括号压栈后,如果下一个Token是右括号,立刻抛异常。把ToRpn里的foreach改成for循环,拿索引判断:

// ToRpn循环体内,左括号入栈前检查下一Token for (int i = 0; i < tokens.Count; i++) { Token tok = tokens[i]; // ... 原有TokenType分支判断逻辑不变 if (tok.Type == TokenType.LeftParen) { if (i + 1 < tokens.Count && tokens[i + 1].Type == TokenType.RightParen) throw new FormatException("括号内没有内容"); opStack.Push(tok); continue; } }

逻辑说明:连续括号比如"((1+2))"不需要特殊处理,因为左括号压栈后下一个Token不是右括号就正常入栈。函数参数丢失的情况类似——sin后面紧跟右括号,Tokenize阶段函数名已入列,但求值时栈里没有参数,会触发“函数缺少参数”,这条链已经兜住了。

参数说明:像"(1)(2)"这种两个独立表达式并列的输入,在当前架构下会求值成两个数都进栈,最后报“表达式不完整”。如果你想支持隐式乘法(即允许"(1)(2)"表示乘),需要在中缀转后缀阶段,当两个连续操作数之间没有运算符时插入一个乘法Token,这算一个扩展功能,日常计算器不强制。

4.3 除零与非法参数:异常时机和用户提示

现象:输入"1/0"或"sqrt(-4)"没有崩溃,但结果要么是“无穷大”要么是“非数字结果”,用户完全看不懂发生了什么。

原因:double类型本身支持无穷大和NaN,Math.Sqrt(-4)在C#里返回NaN而不是抛异常,Math.Log(-1)同理。很多人以为Math类方法会像int除法那样抛异常,其实double的运算机制让异常在很久之后才以NaN形式浮出来。

解决:在函数求值器里主动做参数检查,而不是依赖Math类的方法抛异常。上面代码里sqrt和log已经写了if检查,除法判断右操作数等于0.0时抛DivideByZeroException。注意浮点数运算中"1e-300"这种极小数除以它也会产生Infinity,但用户输入"1/0.0000000001"在形式上合法,不应该报错,所以除零检查只拦精确等于0的情况。

// 在按钮事件里捕获这几种特殊情况并给出明确提示 catch (DivideByZeroException) { MessageBox.Show("除数不能为0", "计算错误"); } catch (ArgumentOutOfRangeException ex) { MessageBox.Show(ex.Message, "参数错误"); } catch (FormatException ex) { MessageBox.Show(ex.Message, "表达式格式错误"); }

逻辑说明:异常类型选对了,用户才能看到人话。三种异常分别弹窗,而不是统一catch(Exception)。统一捕获当然省事,但用户面对一条“对象引用未设置到实例”的报错只会一头雾水。

参数说明:catch的顺序不要调换,因为DivideByZeroException和FormatException之间没有继承关系,但ArgumentOutOfRangeException是ArgumentException的子类,如果你把ArgumentException写在前面会拦截后者。按具体到一般的顺序排列是最稳的。

4.4 浮点误差与预期不符:0.3减0.2的显示问题

现象:计算0.3-0.2,结果显示0.09999999999999998而不是0.1。

原因:二进制浮点无法精确表示0.3和0.2,所有语言都一样,不是C#的问题。double用53位尾数逼近十进制小数,差距在乘以2的幂次后会放大,最终反映在第16位数字上。

解决:显示层用FormatResult里的G12格式符截断到12位有效数字,0.09999999999999998会被显示成0.1。但如果用户再拿这个显示值参与后续运算,误差就保留下来了,这是double的固有属性。要做到完全精确的十进制运算,需要把运算层换成decimal,但decimal无法直接算三角函数,科学计算器一般不做这个取舍。

// 历史记录里同时保存原始表达式和显示结果 lstHistory.Items.Insert(0, $"{expr} = {FormatResult(result)}"); // 如果后续要做二次运算,建议保留result的原始值 // 显示只做格式化,不改变参与计算的double本身

逻辑说明:历史记录里存的是格式化后的文本,展示给用户看是干净的0.1,但内部变量result仍然是带误差的double。这样设计是有意的——用户看到的是适合阅读的结果,编程层面知道这个值有误差,避免下一次计算时把显示文本再转为double,那会二次引入误差。

参数说明:如果实在要显示成十进制并保持精确,可以给结果乘一个10的幂次后用Math.Round再补零,比如Math.Round(value, 10).ToString("F10")。但科学型计算器里三角函数一进入,decimal就不够用了,所以这条只能算“后悔药”,不能作为主方案。

5. 把计算器做到顺手:键盘绑定、历史记录与表达式预检

5.1 键盘输入支持

桌面计算器不支持键盘输入基本等于残废。WinForms里给TextBox监听KeyDown事件,拦截数字、运算符、回车和Esc即可:

private void TxtExpression_KeyDown(object sender, KeyEventArgs e) { if (e.KeyCode == Keys.Enter) { e.SuppressKeyPress = true; // 防止TextBox响铃 BtnEquals_Click(sender, e); } else if (e.KeyCode == Keys.Escape) { txtExpression.Clear(); } }

键盘输入的数字和运算符默认会进文本框,不用额外处理。回车触发等号、Esc清空,这两个是桌面用户肌肉记忆里的操作。Enter要设置SuppressKeyPress,否则WinForms的TextBox对回车默认输出一个铃声字符,还会把按钮触发变成两次。

5.2 历史记录与二次编辑

ListBox显示历史记录时,双击某一条可以把它重新加载回输入框,这就是“后悔药”功能:算错了、想调整某个数字重算一遍,直接双击历史就能改。实现上双击事件拿到ListBox的Item文本,把等号分隔的表达式部分取出来填回输入框:

private void LstHistory_DoubleClick(object sender, EventArgs e) { if (lstHistory.SelectedItem is string line && line.Contains(" = ")) { txtExpression.Text = line.Substring(0, line.IndexOf(" = ")); txtExpression.Focus(); } }

逻辑说明:历史记录本身就是“表达式 = 结果”的字符串,双击时截取等号左边部分,等于把这个表达式原样带回输入框。用户可以直接改一个数字再按回车,比重新输入一整条表达式快得多。焦点放回输入框是为了让用户立刻打字或按回车触发重算。

5.3 表达式预检:粘贴进来的全角符号

最后一个建议:在解析流程的入口做一个字符串预检,把中文括号、全角数字、肉眼难分辨的Unicode空白统一替换成半角。否则用户从聊天软件里复制表达式过来大概率翻车:

private string NormalizeInput(string raw) { return raw.Replace('(', '(').Replace(')', ')') .Replace('+', '+').Replace('-', '-') .Replace('*', '*').Replace('/', '/') .Replace('。', '.').Trim(); }

逻辑说明:全角字符在词法分析阶段会被当成“无法识别的字符”抛异常,而用户看到报错往往会愣一下,因为他觉得括号长得差不多。在入口统一替换,能让异常率明显下降。

从那以后,每次写完一个计算器项目,我都会强制自己走一遍全流程:表达式合法性、异常分支、边界输入、键盘操作、历史回显,五条路径逐项验。这五个点任何一项没覆盖,上线就会被用户一记“怎么算错了”打回来。完整源码已经整理好,下载打开解决方案就能跑起来,关键模块和测试用例都在,改起来也方便。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询