C#实战100例:从语法基础到上位机、机器视觉与Web开发全攻略
2026/9/9 16:06:57 网站建设 项目流程

简介:面向C#初学者的编程实例合集,通过100个精选案例系统覆盖变量、流程控制、面向对象、集合与数据结构、LINQ、异步编程、Windows Forms/WPF等核心主题,帮助读者从语法基础平滑过渡到实际项目开发。压缩包内含1717个文件,以.cs源代码、.exe可运行程序、.jpg/.ico界面资源及项目配置文件为主,完整呈现每个实例的源码、编译结果与设计素材,包体仅9.65MB,便于随时下载学习。已有490人学习使用,适合自学编程、准备面试或作为教学辅助材料。每个示例独立成篇,既包含可直接运行的完整项目,也配有可对照的说明与调试思路,通过动手实践可深入理解委托事件、反射、多线程、文件I/O、XML/JSON处理等进阶机制,同时涉及单元测试与网络编程的典型应用,有助于巩固理论、举一反三。 一直想把手头这套 C# 100 例整理成文字版,因为问的人实在太多了。这 100 个例子里,既有 byte 和 char 这种最底层的数据类型题,也有上位机 Socket 通信、海康 VisionMaster 对接、Halcon 快速匹配这种工业现场题,还有 MVC 集成 Vue、Swagger 加账号密码、后台处理 Excel 这类 Web 方向题。基本把我这些年做 C# 开发踩过的、填平的坑都浓缩进去了。这篇文章就挑里面最有代表性的几个方向展开聊,适合刚入门 C# 的、准备转上位机/机器视觉的、以及做 Web 项目遇到瓶颈的朋友,直接拿去当实战手册用。

1. 100例的选题逻辑:从语法点到大项目的四层架构

一开始定的标题是“c#100例”,但“100例”绝不能是 100 个孤立的知识点。我把它拆成了四层递进结构,每一层解决一类问题。做这套题的初衷就是让读者学完以后,既能在面试里把基础题答得漂亮,也能真到工控现场或者 Web 项目里直接上手干活。

1.1 第一层:语言本身的语法地基

第一层覆盖的是语言本身:数据类型(byte、char、string)、运算符、分支循环、方法、类与接口、泛型、集合、LINQ。很多人在这一步容易犯的错误是“看懂了但写不出来”,所以我每个例子都刻意安排成“能跑出结果的小程序”,而不是干巴巴的语法片段。比如 byte 和 char 互转那道题,看起来是基础中的基础,实际上一扯到编码、溢出、Unicode 就不是那么简单了。这类题的好处是能快速建立代码手感,后面学任何框架都有底。

1.2 第二层:常用类库与设计模式

第二层是“工具箱”:字符串处理、反射、单例模式、事件与委托、File/Stream、串口 SerialPort、Socket、Task 异步编程。这一层我会刻意把相关联的知识点放在同一个例子里。比如反射那题,不仅是Assembly.LoadFrom加载 DLL,还顺手考了特性(Attribute)怎么通过反射读取,以及 “为什么反射慢、怎么缓存”。这样每一题都能带出至少一个高频面试点,学习性价比很高。

1.3 第三层和第四层:框架与行业场景

第三层开始进入框架生态:MVC、Web API、EF Core、ABP 框架、MySQL、Swagger、Json 解析。第四层就是行业场景了:上位机通信、扫码枪接入、循环数据采集、Halcon 机器视觉、VisionMaster 与 C# 通讯、ONNX 模型推理、后台处理 Excel。选题标准很明确——这个题能不能在真实项目里立刻用上。用不上的例子再炫技我也不收进这 100 例里。很多读者问“为什么没有 WPF 动画那一类”,就是因为这些方向虽然写着爽,但实际项目里一年也碰不到两次,先保证高频场景全覆盖。

2. 基础高频题拆解:字符串、反射、单例,三种典型考法

这章挑三个被反复问到的例子,都是“看起来简单、实际很考功底”的典型。它们的共同点是:表面考语法,实际考对 CLR 运行机制的理解。

2.1 字符串截取:从 Substring 到 Span 的进阶路线

字符串截取是 C# 里出现频率最高的操作之一,但不同写法性能差很多。最基础的写法是Substring,两个参数分别是起始索引和长度,容易踩的坑是索引从 0 开始、以及“截到字符串末尾”时长度越界。很多初学者会把第二个参数当成结束位置,写str.Substring(3, 8)以为是从第 3 位截到第 8 位,结果直接抛异常。正确的末尾截取要写成str.Substring(3),只传一个参数。

string str = "Hello, C# World"; // 从索引7开始截取到末尾 string sub1 = str.Substring(7); // 从索引7开始截取5个字符 string sub2 = str.Substring(7, 5); // 如果只是判断是否包含某个字串,用 Contains,不要 Substring 再比较 bool hasCSharp = str.Contains("C#");

如果是在循环里大量做字符串切片,Substring每次都会分配新字符串,内存压力不小。这时候可以用ReadOnlySpan<char>配合 Range 运算符避免分配:

ReadOnlySpan<char> span = str.AsSpan(); ReadOnlySpan<char> slice = span[7..]; // 不产生新字符串

提示:.NET Core 3.0+ 支持 Range 语法[1..^1]^1表示倒数第一个字符。但要注意,Span 只能在栈上临时用,不能存到字段里。

2.2 反射:能做什么、为什么慢、怎么用才对

反射这套题,我是和“插件化开发”放在一起讲的。核心就三个动作:拿到Type、拿到成员信息、动态调用。最常用的场景是从配置的 DLL 路径加载程序集,再根据类名创建实例:

Assembly asm = Assembly.LoadFrom("MyPlugin.dll"); Type type = asm.GetType("MyPlugin.Main"); object instance = Activator.CreateInstance(type); MethodInfo method = type.GetMethod("Run"); method.Invoke(instance, new object[] { "参数" });

但反射不是免费的。Invoke走的是后期绑定,参数要装箱、方法要解析,性能比直接调用慢一到两个数量级。我通常的做法是:如果这个调用会频繁发生,比如循环里每次都要执行,那就把MethodInfo转成强类型委托缓存起来,这样只反射一次,后面都走委托调用。另外反射拿到的成员信息可以配合特性做数据映射,我做过一个导入导出功能,就是依靠PropertyInfo.GetCustomAttribute去读 Excel 列名映射的,不需要手写几十个字段的赋值。

2.3 单例模式:线程安全的正确打开方式

单例几乎是面试必问题,但面试考的和项目里用的完全是两码事。最直接的写法是懒汉式,但不能用,因为多线程下可能创建两个实例。加lock的双重检查锁是经典写法,不过代码稍长,容易写错。

public sealed class ConfigManager { private static readonly Lazy<ConfigManager> _instance = new Lazy<ConfigManager>(() => new ConfigManager()); public static ConfigManager Instance => _instance.Value; private ConfigManager() { } }

实际项目里我推荐Lazy<T>这个写法,IsValueCreated可以判断是否已创建,而且默认就是线程安全的。有一点很多人忽略:单例不只要锁构造函数,如果单例对象里有可变状态,读写的成员方法也要考虑线程安全。我在上位机项目里用单例管理串口对象时,就因为没有锁发送方法,导致两个线程同时写串口,数据直接乱掉了。

3. 工业上位机专题:串口、Socket 与扫码枪

上位机开发是 C# 的一大主战场。这类项目的典型特征是:有硬件、有通信、有 UI,还要求长时间稳定运行。100 例里工业方向占了近 20 题,足见这个方向在真实需求里的分量。

3.1 Socket 通信与 TCP 连接数量的边界

C# 做工业级网口通信,最常见的方案是TcpClient/TcpListener,底层是 Socket。对于“C# TCP 连接数量多少”这个问题,答案不是固定的。客户端这边,一个 IP 出去最多能建立的连接数取决于可用端口范围,Windows 默认动态端口大约是 28000 个左右,所以单客户端主动连同一个服务端,理论上不会超过这个数;服务端这边,瓶颈更多在内存、句柄和监听队列设置上。如果做高并发网关,要用SocketAsyncEventArgs或直接上 Kestrel 这类异步网络库。

TcpListener listener = new TcpListener(IPAddress.Any, 9000); listener.Start(100); // backlog 队列长度 while (true) { TcpClient client = await listener.AcceptTcpClientAsync(); _ = HandleClientAsync(client); // 后台处理,不要阻塞 }

这里有个重要的注意点:连接建立不等于通信正常。我踩过最多的坑是读写超时设置。工业设备经常出现“网线松了但 TCP 连接还在”的情况,如果不设ReceiveTimeout/SendTimeout,程序会在读数据时无限期卡住。超时时间建议根据设备协议来定,一般 1~3 秒比较合理,太短容易误判,太长故障发现不及时。

3.2 扫码枪触发事件的三种接入方式

扫码枪接入几乎是每个上位机项目都会遇到的需求。但很多教程只讲一种接入方式,实际现场三种都常见:USB 模拟键盘型、串口型、网口型。

USB 模拟键盘型最简单,扫码枪插上就像键盘一样把条码字符“敲”进当前焦点控件。检测方式是在窗口或文本框的KeyDown/KeyPress事件里判断回车,然后把累积的字符当作一次完整扫码结果。注意一个坑:如果界面上有意个 TextBox 有焦点,扫码内容会直接出现在输入框里,要在事件里处理后清除。如果系统输入法是中文状态,有些字符会被吞,我一直用的是处理PreviewTextInput或者在TextChanged里做累积判断,比纯KeyDown稳。

串口型扫码枪就是发串口数据,用SerialPort.DataReceived接收。波特率常见 9600 或 115200,注意ReceivedBytesThreshold如果设置不对,一帧数据会拆成多次触发,需要自己组装缓存、按结尾换行符判断一帧。网口型扫码枪则是设备作为 TCP 客户端连到电脑的监听端口,所以上位机其实是被动接收方,要给扫码枪配置好服务器 IP 和端口。

3.3 循环数据采集与 UI 刷新卡顿的根治方案

“C# 循环数据采集和 UI 刷新卡顿”是上位机里问得最多的问题,没有之一。根本原因有两个:一是采集循环直接在 UI 线程跑,界面自然卡死;二是采集线程用Invoke高频刷新控件,把 UI 线程塞爆了。

正确思路是采集和显示解耦。采集线程只负责读数据、存数据;UI 的刷新单独用一个定时器,比如每 200 毫秒从缓存里取一次最新值去更新界面。缓存的设计可以用Channel<T>ConcurrentQueue<T>,保证线程安全。

private readonly Channel<float> _dataChannel = Channel.CreateUnbounded<float>(); // 采集线程 while (_isRunning) { float value = ReadDevice(); await _dataChannel.Writer.WriteAsync(value); } // UI 刷新,用 DispatcherTimer 或 System.Timers.Timer private void RefreshTimer_Tick(object sender, EventArgs e) { while (_dataChannel.Reader.TryRead(out float value)) { txtValue.Text = value.ToString("F2"); } }

如果数据量实在太大、每帧几万个点,那就不适合逐点刷新,应该把数据聚合之后一次性刷新,比如每 200 毫秒只显示最新一组曲线的最大值、最小值、平均值。实测下来,这种方法能把 UI 占用率从 90% 降到 5% 左右。

4. 机器视觉集成:Halcon、VisionMaster 与 C# 那些事

机器视觉是 C# 上位机方案里比较有技术含量的一块。这个方向最常被问到的问题不是算法本身,而是“C# 和视觉软件怎么互相调”。我在这套题里放了 Halcon 联合编程、VisionMaster 通讯协议、ONNX 模型推理三块内容。

4.1 Halcon 联合编程的基本流程

Halcon 提供 .NET 接口,核心是HSmartWindowControl做显示控件,HOperatorSet里的算子做图像处理。C# 调用 Halcon 实现“快速匹配并排除不需要点”这类功能,流程是:读图或 GrabImage 取图、做模板匹配、拿到匹配点位,然后用掩膜把不需要的点剔除。

HImage image = new HImage("png", "image.png"); HTuple modelId; HOperatorSet.CreateNccModel(image, 0, 0, 0, 0, "use_polarity", out modelId); HOperatorSet.FindNccModel(image, modelId, 0, 0, 0, 0, 0.5, "true", 0, out HTuple row, out HTuple col, out HTuple angle, out HTuple score); // 用区域掩膜排除指定矩形内的点 HRegion maskRegion = new HRegion(100, 100, 200, 200); HImage masked = image.ReduceDomain(maskRegion);

这里最容易踩的坑是 Halcon 的版本兼容。HSmartWindowControl有 WinForm 和 WPF 两个版本,NuGet 包和已安装的 Halcon 版本对不上时会直接加载失败。建议用 Halcon 自带安装目录里的halcondotnet.dll而不是 NuGet 的默认包,并且项目目标平台 x64 要和 Halcon 的位数一致,不然运行时报BadImageFormatException

4.2 VisionMaster 与 C# 的通讯协议选型

海康的 VisionMaster 软件和 C# 上位机通讯,实际项目里主流有三条路:TCP/IP 明文协议、Modbus TCP、SDK 二次开发。很多新手问“哪个协议比较好”,我的答案是分场景。

如果是 C# 自己做上位机,同时对实时性要求高,直接选 SDK 最稳,VisionMaster 提供的二次开发接口可以做到真正意义上的“集成”,图像结果能直接以对象形式拿到 C# 里。但是 SDK 版本有强绑定,升级 VisionMaster 后 DLL 也要跟着升,这个维护成本要想清楚。

如果只是把 VisionMaster 当成独立检测单元,C# 通过协议拿结果,推荐 TCP/IP。流程是 C# 发指令帧(比如触发拍照、请求结果),VisionMaster 检测完把 OK/NG 结果和坐标以固定格式回传。字段和数据用 JSON 或 XML 都行,但建议用版本号字段,方便以后扩展。Modbus TCP 则更适合有 PLC 的产线,C# 只需要把视觉结果映射到寄存器地址,PLC 直接读寄存器就行。所以不是哪个“更好”,而是看 C#、VisionMaster、PLC 谁和谁通信。

4.3 ONNX 模型推理:Sketch 素描模型怎么和 C# 结合

C# 里跑 ONNX 模型,首选Microsoft.ML.OnnxRuntime这个 NuGet 包。网上有人用 C# 调素描模型做图像风格化,原理是一样的:加载 ONNX、准备输入 tensor、跑推理、解析输出。

using var session = new InferenceSession("sketch.onnx"); var input = new DenseTensor<float>(new[] { 1, 3, 512, 512 }); // 填充像素数据... var inputs = new List<NamedOnnxValue> { NamedOnnxValue.CreateFromTensor("input", input) }; using var results = session.Run(inputs); var output = results.First().AsTensor<float>(); // 把输出 tensor 转回图像

有几个关键点。第一,预处理必须和训练时一致,比如归一化是除以 255 还是均值方差,错了结果会完全不对。第二,输入尺寸要按模型的固定尺寸来,比如 512x512 就 resize 成 512x512,别贪方便直接传原图。第三,如果连续做大批量推理,InferenceSession建议复用,不要每次 new,性能差很多。最后输出 tensor 到图像,如果是分类模型输出就是标签概率;如果是素描模型,输出通常是单通道的灰度图,把它转成 Bitmap 显示时记得处理像素值范围,经常要乘一个系数才看得见。

5. Web 与混合方向:MVC、Vue、JObject 与异步处理

C# 不止有上位机,Web 后端同样是重头戏。100 例里安排了不少 Web 相关的题目,核心原则是:解决“前后端怎么衔接”和“大数据量怎么不卡死”这两类问题。

5.1 MVC 项目如何优雅集成 Vue

“C# MVC 项目支持 Vue”有三种做法,各有利弊。第一种是完全前后端分离,Vue 项目独立开发部署,MVC 只提供 Web API,跨域问题用 CORS 解决。第二种是 MVC 视图内嵌 Vue,也就是 Razor 页面引 Vue 的 CDN 或打包后的 JS 文件,这种方案上手最快,适合在现有系统里局部改造。第三种是用 Node 中间件做集成,把 Vue 的 dev server 代理给 ASP.NET,开发体验好但部署复杂。

我的建议是,如果是老项目改造,别一上来就搞前后端分离。先保证 API 层能用 RESTful 风格暴露,再在 Razor 视图里局部引入 Vue 做交互组件,这样迁移风险最小。这里要记住一个原则:MVC 的防伪令牌(AntiForgeryToken)和 Vue 的 Ajax 请求要配合好。如果直接用 Vue 发 POST 但没有带RequestVerificationToken,MVC 会直接 400 拒绝请求。解决办法是在布局页把 token 渲染成一个隐藏字段,Vue 每次请求都从表单里取出来放到 header 里。

5.2 Swagger UI 增加账号密码访问

Swagger 是接口文档神器,但生产环境把 Swagger 裸奔出来等于把接口结构全暴露了。给 Swagger UI 加账号密码访问,最简单的方案是加一个中间件,在 Swagger 的请求路径上做 Basic 认证。

app.UseSwaggerUI(c => { c.SwaggerEndpoint("/swagger/v1/swagger.json", "My API V1"); }); app.UseWhen(context => context.Request.Path.StartsWithSegments("/swagger"), appBuilder => { appBuilder.Use(async (context, next) => { var auth = context.Request.Headers["Authorization"].ToString(); // 校验 user:pass 的 Base64,错误则返回 401 }); });

这个方案比自定义ISwaggerProvider要简单很多,也不影响 Swagger 本身的逻辑。注意一点:Basic 认证走的是 HTTP Header,如果服务是 HTTP 明文传输,账号密码等于裸奔,生产环境建议挂 HTTPS。还有人把认证逻辑写在Configure最前面,结果把整个 API 都拦了,这也是常见问题,所以一定要用UseWhen只匹配/swagger路径。

5.3 后台处理前端传过来的 Excel:从同步卡死到异步任务

“C# 后台处理前端传过来的 Excel”这个需求看起来简单,实际坑很多。最典型的是直接用HttpContext.Request.Body去读,读完发现 Body 是空的。这是因为Request.Body默认只能读一次,而且某些中间件已经消费过了。解决办法是提前启用缓冲:

Request.EnableBuffering(); using var reader = new StreamReader( Request.Body, Encoding.UTF8, leaveOpen: true); string body = await reader.ReadToEndAsync(); Request.Body.Position = 0;

另一个坑是 Excel 文件不能同步处理。比如导入 5 万行,同步处理会让接口几十秒不返回,前端要么超时要么以为崩了。标准做法是:接口只接收文件并扔进后台队列,返回一个任务 ID;后台任务慢慢处理,前端轮询任务状态。处理 Excel 的库我优先推荐ClosedXMLEPPlus,前者 API 友好、后者性能好,NPOI虽然免费但写起来啰嗦,适合维护老项目时才用。

5.4 JObject 与 JSON 解析的坑

用 Newtonsoft.Json 解析 JObject 时,判断某个字段是否存在,很多新手直接写jObject["lffh"] == null,结果字段存在但是值为"null"字符串,或者是个空数组[],判断就失效了。正确姿势是用TryGetValue

JObject obj = JObject.Parse(json); if (obj.TryGetValue("lffh", out JToken? value) && value.Type != JTokenType.Null) { // 字段存在且非空 }

另外一个经验:如果字段的值是从其他系统传过来的,别假设它一定是字符串。数字、布尔、对象、数组都会出现。稳妥的做法是先把JToken转成一个明确的类型,比如value?.Type == JTokenType.Array再遍历,而不是祈祷它长得和文档一样。

6. 高频问题与面试经验:文本框、数值变化检测、必考题

最后这部分是 100 例里容易被忽略但实战价值极高的“小需求”题和处理经验。它们单个看都不难,但组合起来就是“做过”和“没做过”的区别。

6.1 文本框失去焦点与变量数值变化检测

文本框失去焦点是最常见的交互需求,比如输入完数字自动格式化、校验范围。WinForm 里可以用ValidatingValidated事件,配合ErrorProvider做提示;WPF 里则用绑定和验证规则更干净。一个很容易踩的坑是:在Leave事件里直接修改文本框的值,会再次触发Leave,造成递归。我一般用一个标志位,或者在Validated事件里修改并关闭验证。

检测变量数值变化,工业项目里经常用。比如读取到某个点位值从 0 变 1,要触发一个动作。最笨的是写死轮询比较上一次值,能用但代码很散。我推荐实现INotifyPropertyChanged,或者给变化检测封装一个方法:

public class ValueMonitor { private float _lastValue; public event Action<float>? ValueChanged; public void Update(float newValue) { if (Math.Abs(newValue - _lastValue) > float.Epsilon) { _lastValue = newValue; ValueChanged?.Invoke(newValue); } } }

这样把“检测变化”和“响应变化”解耦,后续加历史记录、报警都方便。

6.2 面试必问但实战也容易翻车的几个知识点

面试题里 C# 方向问来问去逃不出几个点。我最常给读者强调的是下面这张表,每个点都对应一个容易翻车的细节。

考点考察的核心容易翻车的地方
值类型 vs 引用类型struct/class 在栈和堆上的分配差异以为 int 在方法里改了会影响外部
装箱/拆箱值类型转 object 发生了什么循环里大量ArrayList.Add(1)性能爆炸
string 不可变为什么拼接用 StringBuilder误以为 string 是引用类型就能原地修改
ref/out/in参数传递方式in 参数是只读引用,不能给其赋值
async/await 死锁同步上下文阻塞UI 线程.Result直接死锁,要ConfigureAwait(false)
lock 对象选择锁的粒度stringthis加锁,容易锁错对象
LINQ 延迟执行IQueryable/IEnumerable 的区别以为Where之后集合变少了,其实没执行

这里重点说in参数。它是只读引用传递,适合传大 struct 时避免拷贝,但很多人因为加了in就在方法里尝试给参数赋值,编译直接报错。另外async/await死锁这个问题,我在 WinForm 上位机里遇到过很多次,UI 线程调用.Result等异步方法完成,但异步方法的继续要回到 UI 线程,两边互相等,界面就卡死了。解法是异步方法里尽早ConfigureAwait(false),或者调用方也用await

整套 100 例刷下来,我自己最大的体会是:C# 的知识点不是孤立的,字符串题能接到 JSON 解析,反射题能接到插件架构,Socket 题能接到扫码枪,扫码枪又和 UI 刷新卡顿串在一起。真正值钱的不是背下某个 API,而是知道“遇到哪类问题该从哪个方向下手”。如果说这套题能给你留一个印象,我希望是:任何复杂项目,都能拆成“数据从哪来、怎么传输、怎么处理、怎么显示”四个环节,逐层去解,就不会慌。

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

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

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

立即咨询