本文还有配套的精品资源,点击获取 menu-r.4af5f7ec.gif

简介:本项目基于C#编程语言设计并实现一个支持基本加减乘除运算的简易计算器,适用于编程初学者及高校课程设计实践。通过面向对象编程思想,构建包含加、减、乘、除方法的Calculator类,并结合Windows Forms或WPF实现图形用户界面。项目涵盖用户输入处理、事件响应、异常捕获与错误提示等核心环节,帮助学习者掌握C#基础语法、类与对象的应用以及简单GUI开发流程。经过编译调试后可运行完整计算功能,为进一步学习科学计算器和复杂功能扩展打下坚实基础。

C# 与面向对象设计:从零构建一个健壮的计算器应用 🚀

你有没有想过,为什么我们每天用的计算器 App 看似简单,背后却藏着这么多工程智慧?🤔 其实啊,它不只是“按个按钮出结果”这么直白。今天,咱们就一起 手搓一个功能完整、结构清晰、可扩展性强的计算器程序 ,用 C# 和 Windows Forms 把理论和实践串起来。

这不是那种“Hello World”级别的玩具项目哦~我们要玩真的:封装、继承、异常处理、GUI 设计、事件驱动编程……通通安排上!✨
准备好了吗?Let’s go → 💻🔥


一上来先别急着拖控件!我们得搞懂 C# 的底子 🧱

很多人学编程都喜欢跳过基础直接写界面,结果写着写着就懵了:“哎我这个值怎么变了?”、“为啥这里报错?”——归根结底,是没吃透语言本身的机制。

C# 是一门现代、类型安全、内存安全的语言,支持垃圾回收(GC)、自动内存管理,还有超多语法糖让开发效率飞起。比如:

  • LINQ 查询表达式让你像写 SQL 一样操作数据;
  • async/await 让异步代码看起来跟同步一样清爽;
  • 属性(Properties)代替 getter/setter,干净又安全;
  • 模式匹配( switch 表达式)一行搞定复杂判断;

来看看一个小例子:一个控制台版的简易计算器入口 👇

using System;

class Program {
    static void Main() {
        Console.WriteLine("请输入两个数字和运算符:");
        double a = Convert.ToDouble(Console.ReadLine());
        double b = Convert.ToDouble(Console.ReadLine());
        char op = Console.ReadKey().KeyChar;

        double result = op switch {
            '+' => a + b,
            '-' => a - b,
            '*' => a * b,
            '/' => b != 0 ? a / b : throw new DivideByZeroException(),
            _ => throw new InvalidOperationException("不支持的运算符")
        };

        Console.WriteLine($"结果:{result}");
    }
}

是不是挺简洁?但问题来了:这段代码如果要加新功能(比如支持百分比、开方),或者想做成图形界面,你会发现——耦合太高了!所有逻辑挤在一起,改一处可能崩一片。

所以啊,真正的工业级代码讲究 分层设计 + 面向对象建模 。接下来我们就一步步拆解,看看怎么把“能跑”的代码变成“好维护”的系统。

💡 小贴士:推荐使用 Visual Studio Community VS Code + .NET SDK 开发。创建项目只需一条命令:

bash dotnet new console -n MyCalculator

编译运行也是一行:

bash dotnet run

.csproj 文件记录依赖和配置, Program.cs 是入口点,CLR 加载后从 Main() 方法开始执行。


面向对象不是玄学,它是你组织复杂世界的武器 💣

我们常说“万物皆对象”,这话听着抽象,其实特别实用。想象一下你在做一个智能家电控制系统,里面有一堆设备:灯、空调、窗帘……如果你不用类来抽象它们,那代码会变成什么样?

// 不用 OOP 的后果:一堆变量满天飞 😵‍💫
string lightStatus = "on";
int lightBrightness = 75;
bool acRunning = true;
double acTemperature = 24.5;
// ……几十个变量混在一起,谁还记得哪个属于哪个?

而用了类之后呢?

public class Light {
    public bool IsOn { get; set; }
    public int Brightness { get; set; }
}

public class AirConditioner {
    public bool IsRunning { get; set; }
    public double Temperature { get; set; }
}

瞬间清爽有木有!这就是 OOP 的核心价值:把相关的数据和行为打包成独立单元

在我们的计算器项目中,同样适用这套思维模式。我们需要一个 Calculator 类,它不是一堆函数的集合,而是一个“具有状态和行为的实体”。

🔐 封装:藏得好才是真高手

封装(Encapsulation)是 OOP 的第一道防线。它的本质很简单: 对外暴露接口,对内隐藏实现细节

举个例子,假设你的计算器允许外部直接访问内部操作数字段:

public class Calculator {
    public double Operand1; // ❌ 危险!可以直接被篡改
    public double Operand2;
    public string Operation;
}

这时候别人可以随便改:

calc.Operand1 = -999999; 
calc.Operation = "abc"; // 合法吗?谁知道!

这就像把汽车引擎盖打开,让人随便拧螺丝一样危险 😬

正确做法是: 私有字段 + 公共属性 ,并在设置时做校验:

public class Calculator
{
    private double _operand1;
    private double _operand2;
    private string _operation;

    public double Operand1
    {
        get => _operand1;
        set => _operand1 = value;
    }

    public double Operand2
    {
        get => _operand2;
        set => _operand2 = value;
    }

    public string Operation
    {
        get => _operation;
        set
        {
            if (IsValidOperation(value))
                _operation = value;
            else
                throw new ArgumentException("Invalid operation. Use +, -, *, /");
        }
    }

    private bool IsValidOperation(string op)
    {
        return op is "+" or "-" or "*" or "/";
    }
}

看到没? Operation 在赋值前会先走一遍验证逻辑,非法输入直接抛异常,根本进不来!

特性 未封装(公有字段) 已封装(私有字段 + 属性)
数据安全性 低,可随意修改 高,可通过验证逻辑控制
可调试性 差,无法追踪赋值来源 好,可在 getter/setter 中设断点
扩展性 弱,增加逻辑需改动调用方 强,可在属性内自由扩展(如日志)
维护成本

而且以后你要加功能也很方便。比如哪天产品经理说:“咱们要记录每次运算的历史。”那你只需要在 set 里加一句日志就行:

set
{
    if (IsValidOperation(value))
    {
        _operation = value;
        Log($"Operation changed to: {value}"); // 新增日志
    }
    else
        throw new ArgumentException("...");
}

调用方完全不用改!这才是真正的松耦合 ✅

下面是这个类的 Mermaid 类图,一眼看清结构:

classDiagram
    class Calculator {
        -double _operand1
        -double _operand2
        -string _operation
        +double Operand1 {get; set;}
        +double Operand2 {get; set;}
        +string Operation {get; set;}
        -bool IsValidOperation(string op)
    }

箭头指向清晰:私有的藏起来,公共的亮出来,层次分明!


🧩 实例化到底意味着什么?

很多人以为“new 一个对象”只是分配内存那么简单,其实远不止!

当你写下这句代码:

Calculator calc1 = new Calculator();

你其实在做三件事:

  1. 申请内存空间 :CLR 在堆上为这个实例分配一块专属区域;
  2. 初始化字段 :所有字段按默认值填充( double=0 , string=null );
  3. 建立引用关系 calc1 变量指向这块内存地址;

这意味着什么呢?意味着你可以创建多个互不影响的实例:

var calc1 = new Calculator();
calc1.Operand1 = 10;
calc1.Operand2 = 5;
Console.WriteLine(calc1.Calculate()); // 15

var calc2 = new Calculator();
calc2.Operand1 = 8;
calc2.Operand2 = 0;
try {
    Console.WriteLine(calc2.Calculate());
} catch (DivideByZeroException ex) {
    Console.WriteLine($"Error: {ex.Message}"); // 安全捕获
}

看到没? calc1 calc2 各自拥有独立的状态,互不干扰。这种“状态隔离”特性在 GUI 应用中尤为重要——比如用户打开了两个计算器窗口,每个窗口背后都是一个独立对象,谁也不会影响谁。

更妙的是,这种设计天然支持 依赖注入(DI) 。你看:

public partial class CalculatorForm : Form
{
    private readonly Calculator _calculator;

    public CalculatorForm(Calculator calculator)
    {
        InitializeComponent();
        _calculator = calculator ?? throw new ArgumentNullException(nameof(calculator));
    }
}

我们在窗体构造函数里传入 Calculator 实例,而不是在里面 new 一个。这样做的好处是什么?

  • 单元测试时可以用 mock 替代真实计算逻辑;
  • 切换算法时只需传不同实现,无需修改窗体代码;
  • 解耦!高内聚低耦合的典范 👏

属性 vs 方法:怎么设计才算优雅?🎨

好的 API 设计就像好的 UI 设计:直观、一致、不易误用。

Calculator 类中,我们要决定哪些东西暴露为属性,哪些作为方法提供服务。

📦 公共接口 vs 私有实现:划清界限很重要

想象你是这个类的使用者,你希望怎么调用它?

var calc = new Calculator();
calc.Operand1 = 10;
calc.Operand2 = 5;
calc.Operation = "+";
var result = calc.Calculate(); // ✅ 直观自然

而不是:

calc._operand1 = 10; // ❌ 字段名带下划线,一看就不该碰
calc.perform_calculation(); // ❌ 命名混乱,不符合 C# 规范

所以我们定个规则:

类型 是否公开 职责 示例
公共方法 对外服务接口 Calculate()
私有方法 内部逻辑拆分 PerformAddition()
受保护方法 可选 支持继承扩展 (暂无)

来看具体实现:

public class Calculator
{
    public double Operand1 { get; set; }
    public double Operand2 { get; set; }
    public string Operation { get; set; }

    public double Calculate()
    {
        if (!IsValidOperation(Operation))
            throw new InvalidOperationException("Invalid operation specified.");

        return Operation switch
        {
            "+" => PerformAddition(),
            "-" => PerformSubtraction(),
            "*" => PerformMultiplication(),
            "/" => PerformDivision(),
            _ => throw new NotSupportedException($"Operation '{Operation}' not supported.")
        };
    }

    private double PerformAddition() => Operand1 + Operand2;
    private double PerformSubtraction() => Operand1 - Operand2;
    private double PerformMultiplication() => Operand1 * Operand2;
    private double PerformDivision()
    {
        if (Math.Abs(Operand2) < double.Epsilon)
            throw new DivideByZeroException("Cannot divide by zero.");
        return Operand1 / Operand2;
    }

    private bool IsValidOperation(string op) => op is "+" or "-" or "*" or "/";
}

注意几个关键点:

  • Calculate() 是统一入口,符合“单一出口”原则;
  • 每种运算都拆成独立私有方法,便于调试和测试;
  • 错误检查前置,避免进入无效分支;
  • 使用 double.Epsilon 判断浮点是否接近零,比 == 0 更安全;

整个流程可以用 Mermaid 流程图表示:

flowchart TD
    A[Start Calculate] --> B{Valid Operation?}
    B -- Yes --> C[Switch on Operation]
    B -- No --> D[Throw Exception]
    C --> E[Call PerformAddition]
    C --> F[Call PerformSubtraction]
    C --> G[Call PerformMultiplication]
    C --> H[Call PerformDivision]
    E --> I[Return Result]
    F --> I
    G --> I
    H --> I

是不是感觉整个控制流特别清晰?这就是良好设计的魅力所在 💫


⌨️ 方法签名的选择:别小看参数列表

方法签名(Method Signature)包括方法名、参数列表和返回类型,直接影响 API 的可用性和性能。

对于 Calculate() 方法,我们面临选择:

方案一:无参实例方法(推荐 ✅)
public double Calculate()

优点:
- 符合面向对象语义:“让这个计算器去算”
- 支持链式操作或状态保持(如连续计算)
- 易于扩展为支持历史记录、撤销等功能

方案二:静态工具方法(适合无状态场景)
public static double Calculate(double a, double b, string op)

优点:
- 无需创建实例,节省资源;
- 线程安全,适合高频调用;
- 类似 Math.Max(a,b) 的风格,简洁明了;

方案三:带参实例方法(折中方案)
public double Calculate(double a, double b, string op)

优点:
- 不依赖实例状态,灵活性高;
- 可用于临时计算而不污染当前对象;

三种方式对比:

设计模式 是否依赖实例状态 适用场景 并发安全性
实例方法(无参) 多步操作、状态保持 低(状态共享)
实例方法(带参) 一次性计算
静态方法 完全无状态 工具类、函数式调用 最高

✅ 推荐做法: 优先使用无参实例方法 ,因为它最能体现对象的行为特征。如果需要高性能无状态计算,可额外提供静态重载。

另外提一句参数传递机制:C# 支持 ref out 、值传递等。特别是 TryParse 模式中的 out 参数非常高效:

if (double.TryParse(textBox1.Text, out double num1))
{
    calculator.Operand1 = num1;
}
else
{
    MessageBox.Show("请输入有效数字!");
}

out 允许方法同时返回布尔结果和解析后的数值,避免异常开销,是 .NET 中的标准做法 👍


构造函数:对象诞生的第一课 🐣

构造函数是类实例化时自动调用的特殊方法,负责把“蓝图”变成“活生生的对象”。

🏗️ 默认构造函数的作用

如果你不写任何构造函数,C# 编译器会偷偷帮你生成一个默认的:

public Calculator()
{
    // 自动将字段初始化为默认值
    // double -> 0, string -> null, bool -> false
}

但这可能会带来隐患:

var calc = new Calculator();
Console.WriteLine(calc.Calculate()); // 可能抛 NullReferenceException!

因为 Operation null switch 匹配失败!

所以稳妥的做法是显式定义构造函数,设置合理的初始状态:

public Calculator()
{
    Operand1 = 0;
    Operand2 = 0;
    Operation = "+"; // 设置默认运算
}

这样哪怕用户啥也不设,也能安全地执行一次 0 + 0

🔧 初始化策略大全:灵活组合才够强

除了默认构造函数,我们还可以定义带参数的版本,快速构建预配置对象:

public Calculator(double operand1, double operand2, string operation = "+")
{
    Operand1 = operand1;
    Operand2 = operand2;
    Operation = operation;
}

支持多种调用方式:

var calc1 = new Calculator();                    // 用无参
var calc2 = new Calculator(10, 5);               // 部分参数
var calc3 = new Calculator(10, 5, "*");          // 完整参数

甚至还能结合对象初始化器语法:

var calc4 = new Calculator
{
    Operand1 = 12,
    Operand2 = 3,
    Operation = "/"
};

这在绑定 UI 数据时特别有用,写起来像 JSON 一样清爽!

此外,字段也可以直接初始化:

private double _operand1 = 0;
private string _operation = "+";

这类初始化会在构造函数执行前完成,属于编译期常量初始化,适合设置不变的默认值。

总结一句话: 构造函数 + 初始化器 + 字段默认值 = 三位一体的对象诞生仪式 🎉


动真格了:动手实现核心计算类 🔧

前面说了那么多理论,现在我们正式开始编码 Calculator 类!

目标很明确:打造一个既能满足当前需求,又能轻松扩展未来的计算引擎。

🧱 成员变量定义:要不要存状态?

这是个哲学问题: Calculator 应该是有状态还是无状态?

  • 有状态 :保存上次结果、操作数、运算符,适合连续计算;
  • 无状态 :每次调用独立传参,更适合多线程环境;

我们折中处理:保留有限状态,比如最后结果:

public class Calculator
{
    private double _lastResult;

    public double LastResult => _lastResult;

    public Calculator() => _lastResult = 0.0;
}

LastResult 是只读属性,外部只能读不能改,防止状态被破坏。

未来如果要做表达式求值器,还可以扩展为栈结构:

private Stack<double> _operandStack = new();
private Stack<char> _operatorStack = new();

但现在先专注四则运算。

➕➖✖️➗ 四大金刚登场:Add/Subtract/Multiply/Divide

每个方法职责单一,命名清晰:

public double Add(double x, double y) => x + y;

public double Subtract(double x, double y) => x - y;

public double Multiply(double x, double y)
{
    if ((x > 0 && y > 0 && x > double.MaxValue / y) ||
        (x < 0 && y < 0 && x < double.MaxValue / y) ||
        (x > 0 && y < 0 && y < double.MinValue / x) ||
        (x < 0 && y > 0 && x < double.MinValue / y))
    {
        throw new OverflowException("乘法溢出!");
    }
    return x * y;
}

public double Divide(double x, double y)
{
    if (Math.Abs(y) < double.Epsilon)
        throw new DivideByZeroException("除数不能为零!");

    return Math.Round(x / y, 10); // 控制精度
}

等等……为什么用 double ?难道不怕精度丢失吗?

⚠️ 注意: 0.1 + 0.2 != 0.3 是 IEEE 754 浮点的经典坑!

解决方案?上 decimal !它有 28–29 位有效数字,专为金融和高精度计算设计:

类型 精度 适用场景
float ~7位 图形、游戏
double ~15–16位 一般科学计算
decimal 28–29位 金融、高精度需求

果断升级:

public decimal Add(decimal x, decimal y) => x + y;
public decimal Divide(decimal x, decimal y, int scale = 4)
{
    if (y == 0) throw new DivideByZeroException();
    return Math.Round(x / y, scale, MidpointRounding.AwayFromZero);
}

虽然慢一点,但稳得多!💪


异常处理:程序稳定的最后一道防线 🛡️

别指望用户永远输入正确的数据。现实世界充满恶意字符串、空输入、NaN……

我们必须提前设防!

✅ 输入合法性验证:TryParse 是好朋友

永远不要用 Convert.ToDouble() 这种会直接抛异常的方法处理用户输入!

if (decimal.TryParse(txtInput.Text, out decimal value))
{
    calculator.Operand1 = value;
}
else
{
    MessageBox.Show("请输入有效数字!");
}

TryParse 返回布尔值,成功才赋值,安全又高效。

🚨 主动抛异常:宁可早死,不可苟活

Divide 方法里主动检测除零:

if (y == 0) throw new DivideByZeroException("除数不能为零。");

调用方就可以安心 try-catch:

try {
    var result = calc.Divide(10, 0);
} catch (DivideByZeroException ex) {
    MessageBox.Show(ex.Message);
}

记住: 异常不是错误,而是可控的流程分支


GUI 搭建:让程序“看得见摸得着” 🖼️

终于到了激动人心的时刻——做界面!

我们用 Windows Forms ,虽然老派,但胜在简单高效,适合教学和原型开发。

🛠️ 创建项目 & 布局设计

dotnet new winforms -n CalculatorUI

主窗体叫 Form1.cs ,我们可以用设计器拖控件,也可以代码生成。

推荐使用 TableLayoutPanel 来排布按钮,自动适应窗口缩放:

private TableLayoutPanel CreateKeypadLayout()
{
    var panel = new TableLayoutPanel
    {
        ColumnCount = 4,
        RowCount = 5,
        Dock = DockStyle.Fill,
        Padding = new Padding(5)
    };

    for (int i = 0; i < 4; i++)
        panel.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 25F));

    string[] keys = {
        "7", "8", "9", "/",
        "4", "5", "6", "*",
        "1", "2", "3", "-",
        "0", "C", "=", "+"
    };

    int index = 0;
    for (int row = 0; row < 4; row++)
    {
        panel.RowStyles.Add(new RowStyle(SizeType.Percent, 20F));
        for (int col = 0; col < 4; col++)
        {
            Button btn = new Button
            {
                Text = keys[index],
                Dock = DockStyle.Fill,
                Font = new Font("Arial", 12, FontStyle.Bold)
            };
            btn.Click += Button_Click;
            panel.Controls.Add(btn, col, row);
            index++;
        }
    }

    return panel;
}

配上文本框显示结果:

txtDisplay = new TextBox
{
    Dock = DockStyle.Top,
    Height = 50,
    Font = new Font("Consolas", 16),
    TextAlign = HorizontalAlignment.Right,
    ReadOnly = true,
    Text = "0"
};

事件驱动:点击按钮背后的秘密 🔔

Windows Forms 是典型的事件驱动框架。你点一个按钮,系统就会触发 Click 事件,回调你的处理函数。

private void Button_Click(object sender, EventArgs e)
{
    Button btn = (Button)sender;
    string value = btn.Text;

    switch (value)
    {
        case "C": ResetCalculator(); break;
        case "=": PerformCalculation(); break;
        default: AppendToInput(value); break;
    }
}

所有的业务逻辑都在这里调度!

PerformCalculation() 会调用我们之前写的 Calculator 类:

private void PerformCalculation()
{
    if (!operand1.HasValue || string.IsNullOrEmpty(pendingOperator)) return;

    decimal operand2 = decimal.Parse(txtDisplay.Text);
    decimal result = 0;

    try
    {
        switch (pendingOperator)
        {
            case "+": result = calc.Add(operand1.Value, operand2); break;
            case "-": result = calc.Subtract(operand1.Value, operand2); break;
            case "*": result = calc.Multiply(operand1.Value, operand2); break;
            case "/": result = calc.Divide(operand1.Value, operand2); break;
        }
        txtDisplay.Text = result.ToString("G");
        operand1 = null;
    }
    catch (Exception ex)
    {
        MessageBox.Show(ex.Message, "错误", MessageBoxButtons.OK, MessageBoxIcon.Error);
        ResetCalculator();
    }
}

最后一步:构建、调试、部署 🚀

  • Release 模式编译 → 输出 .exe
  • 断点调试 → 查看局部变量
  • 异常捕捉 → 打开“异常设置”面板
  • 扩展功能建议:
  • 添加历史记录(ListBox)
  • 科学计算模式(TabControl)
  • 深色主题切换(动态改颜色)

总结一下:我们到底学会了啥?🧠

这一路走来,我们不仅仅做了一个计算器,更是完成了一次完整的软件工程训练:

  • ✅ 掌握了 C# 的核心语法和 OOP 三大特性;
  • ✅ 学会了如何设计高内聚、低耦合的类;
  • ✅ 实践了异常处理、输入验证、单元测试;
  • ✅ 搭建了 GUI 界面并实现事件驱动交互;
  • ✅ 理解了构造函数、属性、方法的设计权衡;

最重要的是—— 你已经具备了从零构建一个小型桌面应用的能力

下一步你可以尝试:

  • 把计算器改成表达式求值器(支持括号、优先级)
  • 接入数据库记录计算历史
  • 改造成 Web API + 前端页面
  • 用 WPF 或 MAUI 做更炫的界面

技术的世界没有终点,只有不断前行 🚶‍♂️💨

Keep coding, keep growing! ❤️

本文还有配套的精品资源,点击获取 menu-r.4af5f7ec.gif

简介:本项目基于C#编程语言设计并实现一个支持基本加减乘除运算的简易计算器,适用于编程初学者及高校课程设计实践。通过面向对象编程思想,构建包含加、减、乘、除方法的Calculator类,并结合Windows Forms或WPF实现图形用户界面。项目涵盖用户输入处理、事件响应、异常捕获与错误提示等核心环节,帮助学习者掌握C#基础语法、类与对象的应用以及简单GUI开发流程。经过编译调试后可运行完整计算功能,为进一步学习科学计算器和复杂功能扩展打下坚实基础。


本文还有配套的精品资源,点击获取
menu-r.4af5f7ec.gif

Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐