IEnumerable<T> 详细讲解

IEnumerable<T> 是 .NET 中所有可遍历集合的统一标准接口 ,命名空间 System.Collections.Generic。你可以把它理解为「集合界的通用协议」:只要一个集合实现了这个接口,就代表它支持被逐个遍历元素 ,并且可以使用所有 LINQ 扩展方法(比如 FirstOrDefaultAllWhere 等)

一、核心本质:只定义能力,不存储数据

1. 接口本身只做一件事

IEnumerable<T> 接口里只有一个核心方法:

IEnumerator<T> GetEnumerator();

返回一个「枚举器」IEnumerator<T>,枚举器才是真正负责「逐个访问集合元素」的对象,它包含 Current(当前元素)、MoveNext()(移动到下一个元素)等核心成员。

2. 通俗类比(工控视角)

IEnumerable<T> 比作 Modbus 标准总线协议

  • 协议本身不产生数据,只定义 "怎么读写数据" 的统一规则
  • 不管是汇川 PLC、西门子 PLC、第三方仪表,只要支持 Modbus 协议,上位机就能用同一套指令通信
  • 同理:不管是 List<T>、数组、Queue<T>Dictionary<TKey,TValue>.Values,只要实现了 IEnumerable<T>,就能用同一套方式遍历、用同一套 LINQ 方法操作

. 和你代码里集合的对应关系

你代码里的:

复制代码
List<CodeReader> scanners;
List<PlcBase> plcs;
List<string> tempSjCodeList;

List<T> 这个类本身就实现了 IEnumerable<T> 接口。 所以你写 scanners.FirstOrDefault(...) 的时候,本质是在调用 IEnumerable<CodeReader> 的扩展方法 ------ 这也是 VS 提示里写 IEnumerable<CodeReader>.FirstOrDefault 的原因:它不是 List 自带的方法,是所有 IEnumerable 集合通用的方法。

二、为什么要用 IEnumerable?(对你的代码价值在哪)

1. 统一所有集合的操作逻辑

不用为每种集合单独写逻辑:

  • 筛选元素用 Where()
  • 找第一个匹配项用 FirstOrDefault()
  • 判断是否存在用 Any()
  • 判断全部满足用 All()
  • 统计数量用 Count()

不管是 List、数组、还是数据库查询返回的集合,只要实现了 IEnumerable,方法完全通用。

比如你 AddDrivers 方法里的:

复制代码
driverList.All(obj => obj is CodeReader)

All 就是 IEnumerable 的扩展方法,只要是可枚举集合都能用。

2. 支持延迟执行(懒加载)

大部分 IEnumerable 上的操作(比如 WhereSelect)不会立刻执行,而是等到你真正遍历 / 取值的时候才执行。

注意:FirstOrDefaultCountToList 这类需要拿到最终结果的方法,是立即执行的。

3. 解耦业务代码和具体集合类型

你写方法的时候可以用 IEnumerable<T> 做参数,这样调用方传 List、数组、Queue 都可以,方法不用改。

三、扩展方法:为什么 IEnumerable 能用 FirstOrDefault

你截图里标注的「(扩展)」,就是扩展方法的意思。

1. 什么是扩展方法

扩展方法是 C# 的语法特性,可以在不修改原有类 / 接口代码的前提下,给它 "追加" 新方法。 FirstOrDefault 并不是 IEnumerable<T> 接口里定义的方法,而是 .NET 框架里的 Enumerable 静态类,专门给 IEnumerable<T> 扩展出来的工具方法。

2. 为什么这么设计

如果每个集合方法都写进接口里,那所有实现 IEnumerable 的类都要重写一遍这些方法,工作量巨大。 用扩展方法的话,只要给 IEnumerable 扩展一次,所有实现这个接口的集合(List、数组、Queue 等)就都能直接用,一次编写,处处可用。

四、结合你截图的 FirstOrDefault 再对应理解

你截图里的完整签名:

复制代码
IEnumerable<CodeReader>.FirstOrDefault<CodeReader>(Func<CodeReader, bool> predicate)

对应到你的代码:

复制代码
scanners.FirstOrDefault(a => a.name == $"{stationName}顶盖来料码扫码")
  • 调用者 scanners:是 List<CodeReader>,兼容 IEnumerable<CodeReader>
  • 泛型参数 <CodeReader>:集合里元素的类型
  • 入参 Func<CodeReader, bool> predicate:筛选条件,也就是你写的 lambda 表达式
  • 返回值:CodeReader,找到返回匹配对象,找不到返回 default(CodeReader)null

五、常见坑点(工控代码高频踩坑)

1. 集合本身为 null → 抛 ArgumentNullException

就是你截图里异常部分写的 ArgumentNullException

复制代码
List<CodeReader> scanners = null;
var result = scanners.FirstOrDefault(...); // 直接抛空参数异常

对应你的场景:如果 AddDrivers 没执行、或者执行失败没给 scanners 赋值,后面调用 FirstOrDefault 就会直接崩溃。

2. 集合为空(有对象,没元素)→ 不抛异常,返回 null

复制代码
List<CodeReader> scanners = new List<CodeReader>(); // 空集合
var result = scanners.FirstOrDefault(...); // 不报错,返回 null

这也是你代码里写 if (CheckBatchCodeScanner == null) 能正常工作的原因。

3. 遍历过程中修改集合 → 抛 "集合已修改" 异常

复制代码
foreach(var item in scanners)
{
    scanners.Remove(item); // 遍历的时候删元素,直接抛异常
}

工控代码里如果在循环里动态增删扫码枪、PLC 集合,很容易踩这个坑。

4. 动态序列多次遍历可能重复执行

如果 IEnumerable 不是基于 List / 数组这种内存集合(比如是数据库查询、动态生成的序列),每次遍历都会重新执行一次生成逻辑,可能会有性能问题或数据不一致。

你的代码里都是 List 内存集合,多次遍历没问题,不用太担心这个。


六、你工控代码里常用的 IEnumerable 方法

表格

方法 作用 你的代码场景
FirstOrDefault(predicate) 找第一个满足条件的元素,找不到返回 null 按 name 找扫码枪、找 PLC
All(predicate) 判断所有元素都满足条件 AddDrivers 里判断集合里全是 CodeReader/PLC
Any(predicate) 判断是否存在至少一个满足条件的元素 判断有没有指定名称的扫码枪
Where(predicate) 筛选所有满足条件的元素,返回新的 IEnumerable 筛选出所有启用的扫码枪
Count() 统计元素数量 判断 PLC 数量、扫码枪数量
Cast<TResult>() 把集合元素强转成指定类型 AddDrivers 里把 object 转成 CodeReader

一句话总结

IEnumerable<T> 就是所有集合的「通用遍历协议」,List、数组等都实现了它;FirstOrDefault 是给这个协议扩展的通用工具方法,所以所有集合都能直接调用。

相关推荐
Lost of 程序猿1 小时前
.NET 10 船端单机部署与离线升级实战:从闪退事故到无感回滚
c#·asp.net·.net
淡海水2 小时前
11-04-Unity-Span-foreach-LINQ的分配与热路径边界
unity·c#·linq·foreach·span
唐青枫16 小时前
对象到底住在哪里?C#.NET 托管堆从分配、GC 到内存排查
c#·.net
fogota18 小时前
Emoji与Segoe MDL2 Assets的比较
c#·wpf
灸木19 小时前
C# 多线程与异步
c#
鱼听禅1 天前
C# 学习笔记-使用 类型化客户端搭建HTTP客户端服务
学习·http·c#·工厂方法模式
geovindu1 天前
CSharp:Condition Variable Pattern
后端·设计模式·c#·.net·.netcore·条件变量模式·同步型模式
淡海水1 天前
11-03-Unity-List-T-和Dictionary-TKey-TValue-的性能调优实战
算法·unity·c#·list·dictionary
fogota1 天前
C# 如何调用系统图标
c#·.net