让我的所有时间段变成 24:00 的 Blazor 闭包 Bug 🕛 🕛
发布: (2026年1月4日 GMT+8 03:41)
4 min read
原文: Dev.to
Source: Dev.to
神秘的 Bug
我在 Blazor 中构建会议室预订系统时遇到了一个奇怪的 bug。我的时间轴组件显示的时间段是从上午 6 点到晚上 11 点,但无论点击哪个时间段,控制台总是显示 24:00。
// Console output – ALWAYS showed 24!
Time slot selected: 24:0
Start: 06-01-2026 00:00:00, End: 06-01-2026 01:00:00
视觉上时间轴是正确的,但每一个按钮都失效了。为什么所有从上午 6 点到晚上 11 点的按钮都会发送 “24” 作为小时?
最初的假设
- 时区问题?– 检查了
DateTime.Kind、UTC 转换 - DateTime 解析 bug?– 添加了大量验证
- 浏览器
datetime-localbug?– 在多个浏览器上测试
调试了 3 小时后,我添加了更多日志:
private async Task OnTimeSlotClick(int hour, int minute)
{
Console.WriteLine($"[DEBUG] Clicked hour: {hour}");
// Always showed 24, regardless of which button!
}
生成按钮的循环 看起来 完全正确:
@for (int hour = 6; hour < 24; hour++)
{
<button @onclick="() => OnTimeSlotClick(hour, 0)">
@hour:00
</button>
}
按钮显示 6:00, 7:00, 8:00…,但全部在点击时调用 OnTimeSlotClick(24, 0)!
闭包捕获的陷阱
在 C# 中,lambda 捕获的是变量,而不是它们的值。当任何按钮被点击时,循环已经结束,hour == 24(循环在 hour 达到 24 时退出)。
// When loop finishes: hour = 24
// ALL click handlers now reference hour = 24!
解决方案
1. 使用 foreach 与 Enumerable.Range
@foreach (var hour in Enumerable.Range(6, 18))
{
<button @onclick="() => OnTimeSlotClick(hour, 0)">
@hour:00
</button>
}
foreach 的每一次迭代都会创建一个新变量,避免共享捕获。
2. 在 for 循环内部做局部副本(最常见的修复)
@for (int hour = 6; hour < 24; hour++)
{
var currentHour = hour;
<button @onclick="() => OnTimeSlotClick(currentHour, 0)">
@currentHour:00
</button>
}
3. 使用带参数的方法
@for (int hour = 6; hour < 24; hour++)
{
<button @onclick="() => HandleHourClick(hour)">
@hour:00
</button>
}
@code {
private void HandleHourClick(int hour) // ✅ New parameter each call
{
OnTimeSlotClick(hour, 0);
}
}
4. 使用 @key 强制重新渲染
@for (int hour = 6; hour < 24; hour++)
{
<button @key="hour" @onclick="() => OnTimeSlotClick(hour, 0)">
@hour:00
</button>
}
为什么 Blazor 更容易出现此类 bug
- 事件处理器是 稍后 执行的 lambda。
- 组件生命周期会把执行延迟到渲染之后。
- 状态变化会触发重新渲染,可能会改变已捕获的变量。
- 异步特性导致 lambda 常常在循环结束很久之后才运行。
这不仅是 Blazor 的问题
- JavaScript –
var在循环中的同样问题。 - C# – 任意在循环内部的 lambda。
- Java – 匿名内部类捕获循环变量。
- Python – 默认参数在定义时求值。
经验法则
在循环内部创建 lambda 或事件处理器时,捕获值而不是变量。
- 在 lambda 内部立即记录日志。
- 使用硬编码值进行测试以隔离问题。
- 在 lambda 之前创建局部副本(
var current = loopVar)。 - 将带 lambda 的循环视为“始终可疑”。
结束语
这个 bug 让我明白,最看似简单的代码也可能产生最令人惊讶的行为。如果不小心,循环中的 int 会变成所有事件处理器共享的引用。
讨论
- 你是否也被闭包捕获 bug 咬过?
- 你在 Blazor 中遇到过哪些其他坑?
- 分享你最喜欢的调试故事吧!