首页 / 资讯中心 / 文章详情

C# WinForms随机数排序:用委托实现升序降序切换

C# WinForms随机数排序:用委托实现升序降序切换 ★ FEATURED ARTICLE
带新人做 C# 窗体程序时我最喜欢拿“随机生成 1-100 之间的 10 个数字再用委托实现升序或降序排列”这个小题目开刀。它看起来像课后练习实际上把窗体事件、集合操作、随机数边界、委托赋值、排序比较规则和界面刷新这几件事全串起来了。很多人第一次写能把数字显示出来也能调用排序但一换升序降序就开始堆 if-else最后代码越写越乱。这个内容适合刚接触 C# 窗体程序的读者也适合已经会拖控件、但对委托还停留在“知道有这个东西”阶段的人。下面我按实际做项目的思路把这个小窗体从需求拆解、控件布局、委托设计、完整代码、运行验证到常见问题排查讲透代码可以直接照着复现。标题里常见的写法是“升序或降序”下面按这个理解展开。1. 项目整体设计与委托切入思路1.1 窗体程序的需求边界与控件清单先把需求说清楚界面上要有一个按钮负责生成 10 个 1-100 之间的随机整数一个下拉框选择升序或降序一个按钮执行排序一个列表控件展示结果再加一个状态标签提示当前动作。这里不要一上来就写代码先把控件命名定好因为后面事件绑定、代码可读性、排查问题都依赖命名。我的习惯是控件名前缀统一按钮用btn下拉框用cmb列表用lst标签用lbl。这样在窗体代码里一眼能看出类型不会出现button1_Click里找不到对应控件的情况。控件类型建议命名作用关键属性ButtonbtnGenerate生成 10 个随机数Text 设为“生成随机数”ButtonbtnSort按当前选择排序Text 设为“排序”ComboBoxcmbOrder选择升序或降序DropDownStyle 设为 DropDownListListBoxlstNumbers展示数字列表IntegralHeight 可设为 FalseLabellblStatus显示状态提示Text 初始为“等待生成”LabellblOrder说明下拉框用途Text 设为“排序方式”这个项目的边界要控制住随机数允许重复因为题目只说“随机生成 1-100 之间的 10 个数字”没有要求不重复。如果实际业务要求不重复再引入HashSetint做去重不要默认把需求改掉。排序只针对当前这一组数字不涉及数据库、不涉及文件读写也不需要在排序时重新生成随机数。把边界定清楚后面代码就不会越写越散。1.2 为什么这个小题值得用委托如果不用委托常见写法是一个SortNumbers(bool ascending)方法里面根据布尔值决定a.CompareTo(b)还是b.CompareTo(a)。这样也能跑但问题是排序方法既负责“排序”又负责“判断升序还是降序”后面再加“按个位数排序”“按随机优先级排序”方法里就会不断加分支。委托的价值在于把“比较规则”变成一个可以替换的参数排序方法只负责调用比较规则具体是升序还是降序由外部传进来。用生活化的话说委托像插座排序方法只负责通电插上去的是升序插头还是降序插头由你决定。方案写法优点隐患布尔参数SortNumbers(true)初学者容易理解分支会越来越多枚举参数SortNumbers(SortMode.Asc)比布尔值清楚仍要在方法内写分支委托参数SortNumbers(comparison)比较规则可替换、可扩展需要理解委托签名接口策略ISortStrategy工程化程度高小题大做类数量变多这个小项目用委托刚好不会重到需要接口和工厂又能把“策略可替换”讲明白。Comparisonint是 C# 里现成的泛型委托签名是“接收两个 int返回一个 int”。返回负数表示第一个参数排在前面返回正数表示第二个参数排在前面返回 0 表示两者相等。排序方法拿到这个委托后只负责调用它不关心内部到底怎么比。这个设计的影响范围主要在排序策略层界面层不需要知道比较细节后续扩展排序方式也不用改窗体事件。1.3 数据流和事件流怎么走整个窗体的数据流很短点击“生成随机数”后清空旧集合循环 10 次调用Random.Next(1, 101)把结果放进Listint再刷新ListBox点击“排序”后先检查集合是否为空再根据下拉框选择给委托变量赋值调用Listint.Sort(comparison)最后刷新列表。这里不要把排序结果直接写回控件文本也不要在排序按钮里重新生成数字否则用户会以为排序把数据换了。数据只保存在Listint里控件只是展示层这个分层习惯对后续加功能很重要。事件流上两个按钮的Click事件都在 UI 线程执行。10 个数字的排序量非常小不需要开线程也不要用Thread.Sleep做假动画。列表刷新用BeginUpdate和EndUpdate包起来虽然只有 10 条数据看不出差别但这是好习惯数据量变大时能减少闪烁。状态标签用来告诉用户“已生成”还是“排序完成升序”排查问题时也能快速确认按钮到底有没有进事件。2. 核心细节解析随机数、比较器与窗体刷新2.1 Random.Next 的边界与 10 个数的生成方式Random.Next(minValue, maxValue)的规则是包含minValue不包含maxValue。所以生成 1-100 之间的整数必须写Next(1, 101)而不是Next(1, 100)。后者只能得到 1-99100 永远不会出现。这个边界问题是新手最容易踩的坑而且因为随机结果看起来“也像那么回事”不打印边界值很难发现。更稳妥的做法是把范围写成常量或者参数例如const int MinValue 1; const int MaxValue 100;调用时写_random.Next(MinValue, MaxValue 1)这样意图更清楚。private readonly Random _random new Random(); for (int i 0; i 10; i) { _numbers.Add(_random.Next(1, 101)); // 1 到 100包含 1 和 100 }重复问题要单独说明默认写法允许重复比如两次都生成 37 是正常的。如果需求改成“10 个不重复的数字”不能简单在循环里判断一下就算了因为数量接近范围上限时可能反复生成重复值。更稳的写法是用HashSetint去重循环条件改成集合数量小于 10HashSetint set new HashSetint(); while (set.Count 10) { set.Add(_random.Next(1, 101)); } _numbers.AddRange(set);另外Random对象不要在循环里反复new。如果短时间内创建多个Random它们可能拿到相同种子导致生成的序列很像。把_random作为窗体字段只创建一次是这个小项目里最稳妥的用法。2.2 Comparison 与自定义委托的取舍C# 里实现比较委托有两种常见写法。第一种是用现成的Comparisonint它是泛型委托签名固定为int Comparison(int x, int y)。第二种是自己定义委托类型例如public delegate int NumberCompare(int x, int y);。两者在功能上都能完成排序区别主要在可读性和复用范围。Comparisonint是Listint.Sort直接接受的类型代码更短和标准库配合更自然自定义委托更适合教学演示能让人一眼看到“委托是一种类型”。// 方式一使用现成的 Comparisonint private Comparisonint _comparison; _comparison (a, b) a.CompareTo(b); // 方式二自定义委托 public delegate int NumberCompare(int a, int b); private NumberCompare _numberCompare; _numberCompare (a, b) a.CompareTo(b);如果后面要调用_numbers.Sort(...)直接用Comparisonint最省事因为Listint.Sort有一个重载就接收Comparisonint。自定义委托还需要再包一层 lambda例如_numbers.Sort((a, b) _numberCompare(a, b));。所以这个小项目我建议用Comparisonint把重点放在“比较规则可以替换”上而不是纠结委托类型怎么声明。自定义委托可以作为理解补充不必为了显得高级而多写一层。2.3 升序和降序比较函数的返回值规则比较委托的返回值不是“布尔值”而是整数。以a.CompareTo(b)为例a小于b时返回负数a等于b时返回 0a大于b时返回正数。排序方法看到负数就知道a应该排在b前面看到正数就知道b应该排在a前面。升序就是“小的在前”所以直接a.CompareTo(b)降序就是“大的在前”把参数顺序反过来写b.CompareTo(a)。不要写成a - b虽然 1-100 的范围不会溢出但换到大整数场景就容易出问题CompareTo更稳。比较表达式两个数示例返回值含义排序效果a.CompareTo(b)a8, b20负数8 排在 20 前面升序a.CompareTo(b)a20, b8正数8 排在 20 前面升序b.CompareTo(a)a8, b20正数20 排在 8 前面降序b.CompareTo(a)a20, b8负数20 排在 8 前面降序写比较函数时方法名也要直白Ascending和Descending比Compare1、Compare2好得多。事件里只做委托赋值不把比较逻辑写成一大坨 lambda这样后面排查时能快速定位。如果比较规则以后要加“按绝对值升序”只需要新增一个方法再把委托指向它排序按钮的代码基本不用动。2.4 窗体刷新与排序后的显示列表控件刷新有一个常见坑先Items.Clear()再逐条Items.Add()中间如果数据多界面会闪。解决方法是BeginUpdate()和EndUpdate()包住刷新过程。虽然这里只有 10 条数据但养成习惯没坏处。另一个坑是排序后忘记刷新Listint已经排好了ListBox还显示旧顺序用户就会以为排序没生效。我的做法是把刷新抽成RefreshNumbers(string statusText)方法生成和排序都调用它避免两处刷新逻辑不一致。private void RefreshNumbers(string statusText) { lstNumbers.BeginUpdate(); try { lstNumbers.Items.Clear(); foreach (int number in _numbers) { lstNumbers.Items.Add(number); } } finally { lstNumbers.EndUpdate(); } lblStatus.Text statusText; }这里用try/finally是为了保证即使添加数据过程中出现异常EndUpdate()也能执行避免控件一直处于更新状态。虽然这个练习里不太会抛异常但这是窗体刷新时的稳妥写法。状态文本也不要只写“完成”要写清楚“排序完成升序”或“排序完成降序”用户一眼能确认当前结果对应哪个选项。3. 实操过程从空白窗体到可切换排序3.1 控件布局与命名规范新建一个 C# 窗体应用后先把窗体标题改成“随机数排序演示”StartPosition设为CenterScreenFormBorderStyle保持默认即可。然后从工具箱拖入两个按钮、一个下拉框、一个列表控件和两个标签。布局不用追求花哨按从上到下的顺序排列第一行放“排序方式”标签和下拉框第二行放两个按钮第三行放列表控件最下面放状态标签。列表控件可以设置IntegralHeight false这样调整窗体大小时不会出现半行显示如果希望列表随窗体拉伸可以把Anchor设置为Top, Bottom, Left, Right。下拉框的DropDownStyle一定要改成DropDownList否则用户可以在里面手动输入“升序”以外的文字代码按索引判断时就会出问题。按钮的事件绑定可以在设计器里双击按钮自动生成也可以手动写btnGenerate.Click btnGenerate_Click;。不管用哪种方式方法名要和按钮语义对应不要用button1_Click这种默认名。控件命名统一之后后面看代码就像看一张清单排查时也更容易。3.2 完整 Form1.cs 代码下面这份代码可以直接放在窗体类里前提是设计器里的控件名和表格一致。代码里用Listint保存数据用Comparisonint保存当前比较规则生成和排序都走同一个刷新方法。_comparison在构造函数里先赋值为升序避免第一次排序时出现空委托。using System; using System.Collections.Generic; using System.Windows.Forms; namespace RandomSortDemo { public partial class Form1 : Form { private readonly Listint _numbers new Listint(); private readonly Random _random new Random(); private Comparisonint _comparison; public Form1() { InitializeComponent(); cmbOrder.Items.Add(升序); cmbOrder.Items.Add(降序); cmbOrder.SelectedIndex 0; _comparison Ascending; } private int Ascending(int a, int b) { return a.CompareTo(b); } private int Descending(int a, int b) { return b.CompareTo(a); } private void btnGenerate_Click(object sender, EventArgs e) { _numbers.Clear(); for (int i 0; i 10; i) { _numbers.Add(_random.Next(1, 101)); } RefreshNumbers(已生成 10 个 1-100 之间的随机数); } private void btnSort_Click(object sender, EventArgs e) { if (_numbers.Count 0) { MessageBox.Show(请先生成随机数再执行排序。, 提示, MessageBoxButtons.OK, MessageBoxIcon.Information); return; } if (cmbOrder.SelectedIndex 0) { _comparison Ascending; } else { _comparison Descending; } _numbers.Sort(_comparison); RefreshNumbers(排序完成 cmbOrder.Text); } private void RefreshNumbers(string statusText) { lstNumbers.BeginUpdate(); try { lstNumbers.Items.Clear(); foreach (int number in _numbers) { lstNumbers.Items.Add(number); } } finally { lstNumbers.EndUpdate(); } lblStatus.Text statusText; } } }这段代码的核心只有三处_numbers.Add(_random.Next(1, 101))负责生成正确范围的数据_comparison Ascending或_comparison Descending负责切换策略_numbers.Sort(_comparison)负责把委托交给排序方法。事件方法本身不写比较细节这就是用委托拆开之后的好处。如果以后要加“降序后再按原顺序稳定显示”可以换用 LINQ 的OrderBy但这个小项目用List.Sort已经足够。3.3 事件绑定与设计器检查如果双击按钮没有生成事件方法可以在设计器里选中按钮在事件面板里找到Click手动选择btnGenerate_Click和btnSort_Click。检查时重点看三件事按钮的Click事件有没有绑定下拉框的Items里是不是只有“升序”和“降序”列表控件的名字是不是lstNumbers。有时候代码编译通过但运行没反应就是因为控件名改了设计器里的字段和代码里的字段对不上。还有一个细节cmbOrder.SelectedIndex 0要放在Items.Add之后。如果先设置索引再添加选项索引会变成 -1排序按钮读不到当前选择。把初始化顺序写清楚比后面出问题再猜要省时间。状态标签的初始文本可以在设计器里设为“等待生成”这样程序刚打开时界面不会空着。3.4 运行验证记录与结果对照运行后先点“生成随机数”列表里应该出现 10 个整数每个都在 1-100 之间。此时不排序列表顺序就是生成顺序。然后把下拉框切到“升序”点“排序”列表应该从小到大排列再切到“降序”点“排序”列表应该从大到小排列。为了确认不是碰巧可以连续生成三次每次排序前先看原顺序排序后再核对首尾值。下面是一次模拟记录实际运行时的数字会不同。操作列表内容示例状态提示生成随机数36, 8, 91, 22, 57, 14, 76, 3, 88, 45已生成 10 个 1-100 之间的随机数选择升序并排序3, 8, 14, 22, 36, 45, 57, 76, 88, 91排序完成升序选择降序并排序91, 88, 76, 57, 45, 36, 22, 14, 8, 3排序完成降序验证时还要看两个边界生成结果里有没有出现 100有没有出现小于 1 的数。如果从来没出现过 100优先怀疑Next(1, 100)写错了如果出现 0说明下限写成了 0。再点一次“生成随机数”列表应该被新数据替换而不是追加在后面。如果出现追加说明生成前没有清空集合或者没有清空列表控件。4. 常见问题与排查技巧实录4.1 排序没有反应或顺序始终不变排序没反应先看状态标签有没有变成“排序完成”。如果状态标签变了列表顺序没变重点查刷新方法有没有被调用或者lstNumbers.Items.Clear()后面有没有重新添加数据。如果状态标签都没变说明按钮事件根本没进检查Click事件绑定和按钮的Enabled状态。还有一种情况是排序执行了但比较委托始终指向升序切到降序也不变这时要看cmbOrder.SelectedIndex的判断逻辑以及下拉框是不是被设置成了可编辑状态导致选中项异常。我遇到过一种很隐蔽的写法在排序按钮里重新声明了一个局部Comparisonint comparison Ascending;然后根据下拉框赋值但排序时用的却是窗体字段_comparison。局部变量赋值了字段没变结果永远按旧规则排序。排查这种问题时不要只看排序那一行要顺着委托变量从声明、赋值到使用的路径看一遍。把委托变量统一成窗体字段事件里只改字段能减少这类问题。4.2 随机数越界、重复与每次一样随机数越界通常是Next的第二个参数写错。记住“左闭右开”要 1-100就写Next(1, 101)。重复是正常现象题目没有要求不重复如果业务要求不重复用HashSetint循环补齐。每次运行结果一样常见原因是把new Random()写在了循环里或者短时间内反复创建多个Random对象。把Random作为窗体字段只创建一次基本就能解决。现象常见原因处理方式最大值只到 99Next(1, 100)改成Next(1, 101)出现 0 或负数下限参数写错检查minValue10 个数里有重复默认允许重复需要不重复时用HashSetint每次生成序列很像循环内反复new Random()把Random提成字段生成后旧数据还在没清空集合或列表生成前Clear()并刷新4.3 委托为空、按钮状态与异常处理如果构造函数里忘记给_comparison赋值第一次点排序就可能出现空引用。解决方式有两个在声明时直接初始化为升序或者在构造函数里赋值。更稳的做法是排序前判断_comparison null但这个小项目里构造函数初始化已经足够。另一个细节是“排序”按钮在未生成数据时应该给出提示而不是让空列表走一遍排序。代码里用_numbers.Count 0判断弹出提示后直接return避免继续执行刷新。BeginUpdate和EndUpdate要成对出现最好放在try/finally里。有人只写BeginUpdate忘记EndUpdate列表会一直不重绘看起来像卡死。还有人在排序事件里用MessageBox.Show弹窗后继续执行结果用户连续点击按钮弹出多个窗口体验很差。按钮点击后先做校验校验不过就退出校验通过再执行排序逻辑会清楚很多。4.4 常见问题速查表问题排查顺序解决办法点生成没反应事件是否绑定、按钮是否启用重新绑定Click检查Enabled数字不在 1-100看Next两个参数改为Next(1, 101)升序降序切换无效下拉框索引、委托赋值确认SelectedIndex判断正确排序后列表不更新是否调用刷新方法统一走RefreshNumbers编译报委托类型不匹配方法签名是否为两个 int 返回 int检查参数和返回值列表闪烁或半行显示刷新方式和控件属性用BeginUpdate/EndUpdate调IntegralHeight重复点击排序结果异常是否重新生成数据排序只读当前集合不重新生成5. 从能跑到好用可扩展设计与自测清单5.1 数量与范围可配置化固定生成 10 个数字适合练习但实际使用时经常要改数量。可以把“10”提成常量或者在窗体上加一个NumericUpDown让用户选择数量。范围也可以做成两个输入框生成时读取最小值和最大值。注意Random.Next的上限是排他的所以读取最大值后要加 1。如果最大值允许到int.MaxValue加 1 会溢出需要单独判断这个练习范围是 1-100不存在这个问题但写通用方法时要有这个意识。private Listint GenerateNumbers(int count, int minValue, int maxValue) { if (count 0) { throw new ArgumentOutOfRangeException(nameof(count), 数量必须大于 0); } if (minValue maxValue) { throw new ArgumentException(最小值不能大于最大值); } Listint result new Listint(count); for (int i 0; i count; i) { result.Add(_random.Next(minValue, maxValue 1)); } return result; }把生成逻辑抽成方法后窗体事件只负责读控件、调方法、刷新界面。这样后面要加“生成 20 个 50-200 的数字”只需要改参数不用在按钮事件里改循环。方法里做参数校验也能避免用户输入 0 或负数时出现空列表。5.2 把排序策略抽成独立映射如果排序方式以后会变多可以用字典把下拉框文本和比较委托映射起来。这样新增一种排序方式时只需要加一行映射不用改if-else。例如private readonly Dictionarystring, Comparisonint _comparisons new Dictionarystring, Comparisonint { [升序] (a, b) a.CompareTo(b), [降序] (a, b) b.CompareTo(a) }; private void btnSort_Click(object sender, EventArgs e) { if (_numbers.Count 0) { MessageBox.Show(请先生成随机数再执行排序。, 提示); return; } string key cmbOrder.Text; if (!_comparisons.ContainsKey(key)) { MessageBox.Show(请选择有效的排序方式。, 提示); return; } _numbers.Sort(_comparisons[key]); RefreshNumbers(排序完成 key); }这种写法的好处是把策略集中在一处排查时不用在多个分支里找比较逻辑。代价是字符串键容易写错所以下拉框的选项和字典的键要统一维护。对小项目来说原来的if-else也够用对要持续加功能的项目字典映射更清爽。5.3 输入校验、状态提示与可测试性窗体程序容易把逻辑全塞进按钮事件导致以后想测试排序结果时只能手点界面。更稳的做法是把“生成数字”和“排序数字”抽成不依赖窗体的方法窗体只做展示。例如SortNumbers(Listint numbers, Comparisonint comparison)内部调用numbers.Sort(comparison)测试时直接传一个列表和升序委托就能验证结果。这样拆分之后界面层负责交互逻辑层负责计算后面加日志、加配置都不会互相牵扯。private void SortNumbers(Listint numbers, Comparisonint comparison) { if (numbers null) { throw new ArgumentNullException(nameof(numbers)); } if (comparison null) { throw new ArgumentNullException(nameof(comparison)); } numbers.Sort(comparison); }状态提示也要统一生成成功、排序完成、未生成数据、选择无效这几种情况分别给什么提示最好在纸上列一下。提示语不要含糊例如“操作失败”不如“请先生成随机数”有用。用户看到明确提示才知道下一步该做什么。5.4 交付前自测清单最后给一份自测清单做完这个小窗体后逐项过一遍基本能覆盖常见坑。自测时不要只点一次要连续操作、切换选项、重复点击很多问题只有在反复操作时才暴露。检查项预期结果检查方式生成数量每次正好 10 个数列表项数值范围全部在 1-100 之间看最小值、最大值重复数据允许重复且程序不报错多次生成观察升序排序从小到大排列对比首尾和相邻项降序排序从大到小排列切换选项后排序重复排序结果稳定不叠加数据连续点排序按钮未生成就排序弹出提示不崩溃启动后直接点排序连续生成旧数据被替换点两次生成观察列表窗体缩放列表跟随布局不遮挡拖动窗体边缘我个人在实际操作中的体会是这个小项目最值得花时间的地方不是把排序写出来而是把“比较规则”从排序方法里拆出去。一旦委托变量独立了后面加排序方式、换数据源、做测试都会顺很多。还有一个小技巧调试时可以在排序前后各打印一次_comparison.Method.Name确认当前委托到底指向Ascending还是Descending比盯着下拉框猜要快得多。
阅读完成 · 觉得有帮助?
咨询建站