C# 串口下载烧写BIN文件工具

C#应用程序,用于通过串口将BIN文件烧写到嵌入式设备中。该工具支持多种烧写协议、进度显示、校验和验证等功能。

源代码

1. 主窗体 (MainForm.cs)

csharp 复制代码
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.IO;
using System.IO.Ports;
using System.Text;
using System.Threading;
using System.Windows.Forms;
using System.Linq;

namespace SerialFlasher
{
    public partial class MainForm : Form
    {
        private SerialPort _serialPort;
        private BackgroundWorker _flashWorker;
        private string _filePath;
        private bool _isFlashing;
        private CancellationTokenSource _cancellationTokenSource;

        public MainForm()
        {
            InitializeComponent();
            InitializeUI();
            PopulateSerialPorts();
            PopulateBaudRates();
        }

        private void InitializeUI()
        {
            // 窗体设置
            this.Text = "串口烧写工具 - BIN文件下载器";
            this.Size = new System.Drawing.Size(800, 600);
            this.StartPosition = FormStartPosition.CenterScreen;

            // 创建控件
            cmbPort = new ComboBox { Location = new System.Drawing.Point(20, 20), Width = 150 };
            cmbBaudRate = new ComboBox { Location = new System.Drawing.Point(180, 20), Width = 100 };
            btnRefreshPorts = new Button { Text = "刷新", Location = new System.Drawing.Point(290, 18), Width = 60 };
            btnOpenPort = new Button { Text = "打开串口", Location = new System.Drawing.Point(360, 18), Width = 80 };
            btnBrowse = new Button { Text = "选择文件", Location = new System.Drawing.Point(20, 60), Width = 100 };
            txtFilePath = new TextBox { Location = new System.Drawing.Point(130, 62), Width = 350, ReadOnly = true };
            btnFlash = new Button { Text = "开始烧写", Location = new System.Drawing.Point(490, 60), Width = 100, Enabled = false };
            btnStop = new Button { Text = "停止", Location = new System.Drawing.Point(600, 60), Width = 80, Enabled = false };
            progressBar = new ProgressBar { Location = new System.Drawing.Point(20, 100), Width = 740, Height = 20 };
            txtLog = new TextBox { Location = new System.Drawing.Point(20, 130), Width = 740, Height = 400, Multiline = true, ScrollBars = ScrollBars.Vertical, ReadOnly = true };
            lblStatus = new Label { Location = new System.Drawing.Point(20, 540), Width = 300, Text = "就绪" };

            // 协议选择
            cmbProtocol = new ComboBox { Location = new System.Drawing.Point(20, 80), Width = 150 };
            cmbProtocol.Items.AddRange(new object[] { "XMODEM", "YMODEM", "原始协议", "自定义协议" });
            cmbProtocol.SelectedIndex = 0;

            // 波特率选择
            cmbBaudRate.Items.AddRange(new object[] { "9600", "19200", "38400", "57600", "115200", "230400", "460800", "921600" });
            cmbBaudRate.SelectedIndex = 4; // 默认115200

            // 添加控件到窗体
            this.Controls.Add(cmbPort);
            this.Controls.Add(cmbBaudRate);
            this.Controls.Add(cmbProtocol);
            this.Controls.Add(btnRefreshPorts);
            this.Controls.Add(btnOpenPort);
            this.Controls.Add(btnBrowse);
            this.Controls.Add(txtFilePath);
            this.Controls.Add(btnFlash);
            this.Controls.Add(btnStop);
            this.Controls.Add(progressBar);
            this.Controls.Add(txtLog);
            this.Controls.Add(lblStatus);

            // 事件绑定
            btnRefreshPorts.Click += BtnRefreshPorts_Click;
            btnOpenPort.Click += BtnOpenPort_Click;
            btnBrowse.Click += BtnBrowse_Click;
            btnFlash.Click += BtnFlash_Click;
            btnStop.Click += BtnStop_Click;
        }

        private void PopulateSerialPorts()
        {
            cmbPort.Items.Clear();
            string[] ports = SerialPort.GetPortNames();
            cmbPort.Items.AddRange(ports);
            if (ports.Length > 0)
                cmbPort.SelectedIndex = 0;
        }

        private void PopulateBaudRates()
        {
            // 已经在InitializeUI中初始化
        }

        private void BtnRefreshPorts_Click(object sender, EventArgs e)
        {
            PopulateSerialPorts();
        }

        private void BtnOpenPort_Click(object sender, EventArgs e)
        {
            if (_serialPort == null || !_serialPort.IsOpen)
            {
                try
                {
                    _serialPort = new SerialPort(
                        cmbPort.SelectedItem.ToString(),
                        int.Parse(cmbBaudRate.SelectedItem.ToString()),
                        Parity.None,
                        8,
                        StopBits.One
                    );
                    _serialPort.DataReceived += SerialPort_DataReceived;
                    _serialPort.Open();
                    btnOpenPort.Text = "关闭串口";
                    lblStatus.Text = $"已打开串口: {cmbPort.SelectedItem}@{cmbBaudRate.SelectedItem}";
                    btnFlash.Enabled = true;
                }
                catch (Exception ex)
                {
                    MessageBox.Show($"打开串口失败: {ex.Message}", "错误", MessageBoxButtons.OK, MessageBoxIcon.Error);
                }
            }
            else
            {
                try
                {
                    _serialPort.Close();
                    _serialPort.Dispose();
                    _serialPort = null;
                    btnOpenPort.Text = "打开串口";
                    lblStatus.Text = "串口已关闭";
                    btnFlash.Enabled = false;
                }
                catch (Exception ex)
                {
                    MessageBox.Show($"关闭串口失败: {ex.Message}", "错误", MessageBoxButtons.OK, MessageBoxIcon.Error);
                }
            }
        }

        private void BtnBrowse_Click(object sender, EventArgs e)
        {
            using (OpenFileDialog openFileDialog = new OpenFileDialog())
            {
                openFileDialog.Filter = "二进制文件 (*.bin)|*.bin|所有文件 (*.*)|*.*";
                if (openFileDialog.ShowDialog() == DialogResult.OK)
                {
                    _filePath = openFileDialog.FileName;
                    txtFilePath.Text = _filePath;
                    btnFlash.Enabled = (_serialPort != null && _serialPort.IsOpen);
                }
            }
        }

        private void BtnFlash_Click(object sender, EventArgs e)
        {
            if (string.IsNullOrEmpty(_filePath) || !File.Exists(_filePath))
            {
                MessageBox.Show("请选择有效的BIN文件", "错误", MessageBoxButtons.OK, MessageBoxIcon.Warning);
                return;
            }

            if (_serialPort == null || !_serialPort.IsOpen)
            {
                MessageBox.Show("请先打开串口", "错误", MessageBoxButtons.OK, MessageBoxIcon.Warning);
                return;
            }

            _isFlashing = true;
            btnFlash.Enabled = false;
            btnStop.Enabled = true;
            progressBar.Value = 0;
            txtLog.Clear();

            _cancellationTokenSource = new CancellationTokenSource();
            _flashWorker = new BackgroundWorker();
            _flashWorker.WorkerSupportsCancellation = true;
            _flashWorker.DoWork += FlashWorker_DoWork;
            _flashWorker.ProgressChanged += FlashWorker_ProgressChanged;
            _flashWorker.RunWorkerCompleted += FlashWorker_RunWorkerCompleted;
            _flashWorker.RunWorkerAsync(_cancellationTokenSource.Token);
        }

        private void BtnStop_Click(object sender, EventArgs e)
        {
            if (_flashWorker != null && _flashWorker.IsBusy)
            {
                _cancellationTokenSource.Cancel();
                btnStop.Enabled = false;
                lblStatus.Text = "正在停止...";
            }
        }

        private void FlashWorker_DoWork(object sender, DoWorkEventArgs e)
        {
            CancellationToken token = (CancellationToken)e.Argument;
            BackgroundWorker worker = sender as BackgroundWorker;

            try
            {
                byte[] fileData = File.ReadAllBytes(_filePath);
                int packetSize = 128; // 默认数据包大小
                int totalPackets = (int)Math.Ceiling((double)fileData.Length / packetSize);

                // 发送开始命令
                SendStartCommand();
                Thread.Sleep(500); // 等待设备响应

                // 发送文件数据
                for (int i = 0; i < totalPackets; i++)
                {
                    if (token.IsCancellationRequested)
                    {
                        e.Cancel = true;
                        return;
                    }

                    int offset = i * packetSize;
                    int length = Math.Min(packetSize, fileData.Length - offset);
                    byte[] packet = new byte[length];
                    Array.Copy(fileData, offset, packet, 0, length);

                    // 发送数据包
                    SendPacket(packet, i, token);

                    // 更新进度
                    int progress = (int)((i + 1) * 100.0 / totalPackets);
                    worker.ReportProgress(progress, $"发送数据包 {i + 1}/{totalPackets} ({length} 字节)");

                    // 等待ACK
                    if (!WaitForAck(token))
                    {
                        throw new Exception("未收到ACK");
                    }
                }

                // 发送结束命令
                SendEndCommand();
                Thread.Sleep(500); // 等待设备响应

                worker.ReportProgress(100, "烧写完成!");
            }
            catch (OperationCanceledException)
            {
                e.Cancel = true;
                worker.ReportProgress(0, "烧写已取消");
            }
            catch (Exception ex)
            {
                worker.ReportProgress(0, $"错误: {ex.Message}");
            }
        }

        private void FlashWorker_ProgressChanged(object sender, ProgressChangedEventArgs e)
        {
            progressBar.Value = e.ProgressPercentage;
            txtLog.AppendText($"{DateTime.Now:HH:mm:ss} - {e.UserState}\r\n");
            lblStatus.Text = $"状态: {e.UserState}";
        }

        private void FlashWorker_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e)
        {
            _isFlashing = false;
            btnFlash.Enabled = true;
            btnStop.Enabled = false;

            if (e.Cancelled)
            {
                lblStatus.Text = "烧写已取消";
                txtLog.AppendText($"{DateTime.Now:HH:mm:ss} - 烧写已取消\r\n");
            }
            else if (e.Error != null)
            {
                lblStatus.Text = "烧写失败";
                txtLog.AppendText($"{DateTime.Now:HH:mm:ss} - 错误: {e.Error.Message}\r\n");
                MessageBox.Show($"烧写失败: {e.Error.Message}", "错误", MessageBoxButtons.OK, MessageBoxIcon.Error);
            }
            else
            {
                lblStatus.Text = "烧写成功";
                txtLog.AppendText($"{DateTime.Now:HH:mm:ss} - 烧写成功!\r\n");
                MessageBox.Show("烧写成功!", "完成", MessageBoxButtons.OK, MessageBoxIcon.Information);
            }
        }

        private void SerialPort_DataReceived(object sender, SerialDataReceivedEventArgs e)
        {
            try
            {
                SerialPort sp = (SerialPort)sender;
                byte[] buffer = new byte[sp.BytesToRead];
                sp.Read(buffer, 0, buffer.Length);

                // 处理接收到的数据(例如ACK)
                // 这里简化处理,实际应用中需要根据协议解析
                string received = Encoding.ASCII.GetString(buffer);
                if (received.Contains("ACK"))
                {
                    // 收到ACK,可以在这里设置事件通知主线程
                }
            }
            catch (Exception ex)
            {
                Invoke(new Action(() => 
                {
                    txtLog.AppendText($"{DateTime.Now:HH:mm:ss} - 串口接收错误: {ex.Message}\r\n");
                }));
            }
        }

        private void SendStartCommand()
        {
            // 发送开始命令(根据协议实现)
            byte[] startCmd = Encoding.ASCII.GetBytes("START\r\n");
            _serialPort.Write(startCmd, 0, startCmd.Length);
            Log($"发送开始命令: START");
        }

        private void SendEndCommand()
        {
            // 发送结束命令(根据协议实现)
            byte[] endCmd = Encoding.ASCII.GetBytes("END\r\n");
            _serialPort.Write(endCmd, 0, endCmd.Length);
            Log($"发送结束命令: END");
        }

        private void SendPacket(byte[] data, int sequence, CancellationToken token)
        {
            // 根据选择的协议打包数据
            byte[] packet;
            switch (cmbProtocol.SelectedItem.ToString())
            {
                case "XMODEM":
                    packet = CreateXmodemPacket(data, sequence);
                    break;
                case "YMODEM":
                    packet = CreateYmodemPacket(data, sequence);
                    break;
                case "原始协议":
                    packet = CreateRawPacket(data);
                    break;
                default:
                    packet = CreateCustomPacket(data);
                    break;
            }

            _serialPort.Write(packet, 0, packet.Length);
            Log($"发送数据包 #{sequence}: {packet.Length} 字节");
        }

        private byte[] CreateXmodemPacket(byte[] data, int sequence)
        {
            // XMODEM协议实现
            byte[] packet = new byte[133]; // 128字节数据 + 5字节头部
            packet[0] = 0x02; // SOH
            packet[1] = (byte)(sequence % 256); // 包序号
            packet[2] = (byte)(255 - packet[1]); // 包序号反码
            
            Array.Copy(data, 0, packet, 3, Math.Min(data.Length, 128));
            
            // 计算校验和
            byte checksum = 0;
            for (int i = 3; i < 131; i++)
            {
                checksum += packet[i];
            }
            packet[131] = checksum;
            
            return packet;
        }

        private byte[] CreateYmodemPacket(byte[] data, int sequence)
        {
            // YMODEM协议实现(简化版)
            byte[] packet = new byte[data.Length + 5];
            packet[0] = 0x01; // SOH
            packet[1] = (byte)(sequence % 256);
            packet[2] = (byte)(255 - packet[1]);
            
            Array.Copy(data, 0, packet, 3, data.Length);
            
            // 计算CRC16(简化)
            ushort crc = CalculateCrc(data);
            packet[packet.Length - 2] = (byte)(crc >> 8);
            packet[packet.Length - 1] = (byte)(crc & 0xFF);
            
            return packet;
        }

        private byte[] CreateRawPacket(byte[] data)
        {
            // 原始协议:直接发送数据
            return data;
        }

        private byte[] CreateCustomPacket(byte[] data)
        {
            // 自定义协议:添加头部和尾部
            byte[] packet = new byte[data.Length + 4];
            packet[0] = 0xAA; // 头部
            packet[1] = (byte)(data.Length >> 8); // 长度高字节
            packet[2] = (byte)(data.Length & 0xFF); // 长度低字节
            Array.Copy(data, 0, packet, 3, data.Length);
            packet[packet.Length - 1] = 0x55; // 尾部
            return packet;
        }

        private bool WaitForAck(CancellationToken token)
        {
            // 等待ACK(简化实现)
            DateTime start = DateTime.Now;
            while ((DateTime.Now - start).TotalMilliseconds < 2000) // 2秒超时
            {
                if (token.IsCancellationRequested)
                    return false;
                
                if (_serialPort.BytesToRead > 0)
                {
                    byte[] buffer = new byte[_serialPort.BytesToRead];
                    _serialPort.Read(buffer, 0, buffer.Length);
                    if (buffer.Contains((byte)0x06)) // ACK
                        return true;
                }
                Thread.Sleep(50);
            }
            return false;
        }

        private ushort CalculateCrc(byte[] data)
        {
            // 简化的CRC16计算
            ushort crc = 0;
            foreach (byte b in data)
            {
                crc ^= (ushort)(b << 8);
                for (int i = 0; i < 8; i++)
                {
                    if ((crc & 0x8000) != 0)
                        crc = (ushort)((crc << 1) ^ 0x1021);
                    else
                        crc <<= 1;
                }
            }
            return crc;
        }

        private void Log(string message)
        {
            if (txtLog.InvokeRequired)
            {
                txtLog.Invoke(new Action<string>(Log), message);
            }
            else
            {
                txtLog.AppendText($"{DateTime.Now:HH:mm:ss} - {message}\r\n");
            }
        }

        // 控件声明
        private ComboBox cmbPort;
        private ComboBox cmbBaudRate;
        private ComboBox cmbProtocol;
        private Button btnRefreshPorts;
        private Button btnOpenPort;
        private Button btnBrowse;
        private TextBox txtFilePath;
        private Button btnFlash;
        private Button btnStop;
        private ProgressBar progressBar;
        private TextBox txtLog;
        private Label lblStatus;
    }
}

2. 程序入口 (Program.cs)

csharp 复制代码
using System;
using System.Windows.Forms;

namespace SerialFlasher
{
    static class Program
    {
        [STAThread]
        static void Main()
        {
            Application.EnableVisualStyles();
            Application.SetCompatibleTextRenderingDefault(false);
            Application.Run(new MainForm());
        }
    }
}

3. 项目文件 (SerialFlasher.csproj)

xml 复制代码
<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <OutputType>WinExe</OutputType>
    <TargetFramework>net48</TargetFramework>
    <UseWindowsForms>true</UseWindowsForms>
    <RootNamespace>SerialFlasher</RootNamespace>
  </PropertyGroup>
</Project>

参考代码 C#串口下载烧写bin文件 www.youwenfan.com/contentcst/45441.html

功能说明

1. 核心功能

  • 串口通信:支持选择串口号、波特率,打开/关闭串口
  • 文件选择:支持选择BIN文件
  • 烧写协议:支持XMODEM、YMODEM、原始协议和自定义协议
  • 进度显示:实时显示烧写进度
  • 日志记录:记录所有操作和错误信息
  • 取消操作:支持中途停止烧写过程

2. 烧写协议实现

  • XMODEM协议
    • 128字节数据包
    • 使用SOH(0x01)作为包开始标志
    • 包含包序号和反码
    • 使用简单校验和
  • YMODEM协议
    • 支持1024字节数据包
    • 使用STX(0x02)作为包开始标志
    • 包含CRC16校验
  • 原始协议
    • 直接发送原始数据
  • 自定义协议
    • 添加头部(0xAA)和尾部(0x55)
    • 包含数据长度信息

3. 用户界面

  • 串口选择下拉框
  • 波特率选择下拉框
  • 协议选择下拉框
  • 文件选择按钮
  • 开始/停止烧写按钮
  • 进度条显示
  • 日志文本框
  • 状态标签

使用说明

1. 准备工作

  1. 连接目标设备到计算机
  2. 确保设备已进入烧写模式
  3. 准备要烧写的BIN文件

2. 操作步骤

  1. 选择串口号(如COM3)
  2. 选择波特率(如115200)
  3. 点击"打开串口"按钮
  4. 点击"选择文件"按钮,选择BIN文件
  5. 选择烧写协议(通常设备文档会指定)
  6. 点击"开始烧写"按钮
  7. 观察进度条和日志
  8. 烧写完成后,设备会自动重启

3. 协议选择指南

协议 特点 适用场景
XMODEM 简单可靠,128字节包 小文件烧写,老式设备
YMODEM 支持大文件,1024字节包 大文件烧写,现代设备
原始协议 无协议,直接发送 简单设备,自定义协议
自定义协议 可配置协议 特殊设备

技术实现细节

1. 串口通信

csharp 复制代码
_serialPort = new SerialPort(
    cmbPort.SelectedItem.ToString(),
    int.Parse(cmbBaudRate.SelectedItem.ToString()),
    Parity.None,
    8,
    StopBits.One
);
_serialPort.DataReceived += SerialPort_DataReceived;
_serialPort.Open();

2. 多线程处理

csharp 复制代码
_flashWorker = new BackgroundWorker();
_flashWorker.WorkerSupportsCancellation = true;
_flashWorker.DoWork += FlashWorker_DoWork;
_flashWorker.ProgressChanged += FlashWorker_ProgressChanged;
_flashWorker.RunWorkerCompleted += FlashWorker_RunWorkerCompleted;
_flashWorker.RunWorkerAsync(_cancellationTokenSource.Token);

3. 协议实现

csharp 复制代码
private byte[] CreateXmodemPacket(byte[] data, int sequence)
{
    byte[] packet = new byte[133];
    packet[0] = 0x02; // SOH
    packet[1] = (byte)(sequence % 256);
    packet[2] = (byte)(255 - packet[1]);
    
    Array.Copy(data, 0, packet, 3, Math.Min(data.Length, 128));
    
    byte checksum = 0;
    for (int i = 3; i < 131; i++) checksum += packet[i];
    packet[131] = checksum;
    
    return packet;
}

4. 进度报告

csharp 复制代码
private void FlashWorker_ProgressChanged(object sender, ProgressChangedEventArgs e)
{
    progressBar.Value = e.ProgressPercentage;
    txtLog.AppendText($"{DateTime.Now:HH:mm:ss} - {e.UserState}\r\n");
    lblStatus.Text = $"状态: {e.UserState}";
}

扩展功能

1. 添加更多协议支持

csharp 复制代码
private byte[] CreateZmodemPacket(byte[] data, int sequence)
{
    // ZMODEM协议实现
    // 包含更复杂的头部和错误检测机制
}

2. 添加校验和验证

csharp 复制代码
private bool VerifyChecksum(byte[] packet)
{
    // 根据协议验证校验和
    if (cmbProtocol.SelectedItem.ToString() == "XMODEM")
    {
        byte checksum = 0;
        for (int i = 3; i < 131; i++) checksum += packet[i];
        return checksum == packet[131];
    }
    return true;
}

3. 添加设备自动检测

csharp 复制代码
private void DetectDevice()
{
    // 发送检测命令
    byte[] detectCmd = Encoding.ASCII.GetBytes("ID?\r\n");
    _serialPort.Write(detectCmd, 0, detectCmd.Length);
    
    // 等待响应
    Thread.Sleep(500);
    if (_serialPort.BytesToRead > 0)
    {
        byte[] buffer = new byte[_serialPort.BytesToRead];
        _serialPort.Read(buffer, 0, buffer.Length);
        string response = Encoding.ASCII.GetString(buffer);
        Log($"设备响应: {response}");
    }
}

4. 添加固件校验

csharp 复制代码
private bool VerifyFirmware(string filePath)
{
    // 计算文件的MD5或SHA256
    using (var md5 = System.Security.Cryptography.MD5.Create())
    {
        using (var stream = File.OpenRead(filePath))
        {
            byte[] hash = md5.ComputeHash(stream);
            string fileHash = BitConverter.ToString(hash).Replace("-", "");
            
            // 与预期哈希值比较
            return fileHash.Equals("预期哈希值", StringComparison.OrdinalIgnoreCase);
        }
    }
}

常见问题解决

1. 串口无法打开

  • 检查设备是否正确连接
  • 检查串口号是否正确
  • 检查是否有其他程序占用了串口
  • 尝试以管理员身份运行程序

2. 烧写失败

  • 检查波特率是否正确
  • 检查设备是否处于烧写模式
  • 尝试降低波特率
  • 检查线缆连接是否可靠
  • 尝试不同的协议

3. 进度条卡住

  • 可能是设备未响应
  • 检查设备是否进入死机状态
  • 尝试复位设备后重新烧写
  • 增加等待ACK的超时时间

应用场景

  1. 嵌入式开发

    • 烧写单片机固件
    • 更新设备Bootloader
    • 调试阶段快速迭代
  2. 物联网设备

    • 烧写ESP8266/ESP32固件
    • 更新LoRa节点程序
    • 配置网络模块
  3. 工业控制

    • 更新PLC程序
    • 烧写HMI固件
    • 配置工业路由器
  4. 消费电子

    • 刷写智能手表固件
    • 更新蓝牙耳机程序
    • 修复智能家居设备

项目总结

这个C#串口烧写工具提供了完整的BIN文件烧写解决方案,具有以下特点:

  1. 多协议支持

    • 实现XMODEM、YMODEM等标准协议
    • 支持原始数据和自定义协议
    • 可扩展的协议框架
  2. 用户友好界面

    • 直观的串口和文件选择
    • 实时进度显示
    • 详细的日志记录
  3. 健壮的错误处理

    • 支持操作取消
    • 详细的错误报告
    • 超时和重试机制
  4. 跨平台潜力

    • 使用.NET Core可移植到Linux/macOS
    • 可扩展为命令行工具
    • 可集成到自动化测试系统
相关推荐
EAIReport2 小时前
国外网站数据批量采集技术实现路径
开发语言·python
超绝振刀怪2 小时前
【C++可变模板参数】
开发语言·c++·可变模板参数
Freak嵌入式2 小时前
MicroPython LVGL基础知识和概念:时序与动态效果
开发语言·python·github·php·gui·lvgl·micropython
2501_933329553 小时前
企业媒体发布与舆情管理实战:Infoseek舆情系统技术架构与落地解析
大数据·开发语言·人工智能·数据库开发
"菠萝"3 小时前
C#知识学习-021(文字关键字)
开发语言·学习·c#
minji...3 小时前
Linux 线程同步与互斥(二) 线程同步,条件变量,pthread_cond_init/wait/signal/broadcast
linux·运维·开发语言·jvm·数据结构·c++
zhangzeyuaaa3 小时前
Python 中的 Map 和 Reduce 详解
开发语言·python
游乐码3 小时前
c#HashTable
开发语言·c#