迭代器
迭代器(Iterator)是一种设计模式,让你可以顺序访问某个集合(如数组、链表)中的每一个元素,而不需要知道它的内部结构。
在 C# 中,能被
foreach
遍历的对象,都实现了迭代器机制。
实现迭代器的两种方式
方法1:手动实现IEnumerator和IEnumerableclass CustomList : IEnumerable, IEnumerator { private int[] list; private int position = -1; public CustomList() { list = new int[] { 1, 2, 3, 4, 5, 6, 7, 8 }; } // 实现 IEnumerable 接口,返回枚举器 public IEnumerator GetEnumerator() { Reset(); // 重置光标,每次 foreach 前从头开始 return this; } // 实现 IEnumerator 接口,用于遍历元素 public object Current => list[position]; // 当前元素 public bool MoveNext() => ++position < list.Length; // 移动光标 public void Reset() => position = -1; // 重置光标 }
工作流程(配合foreach):
foreach (int item in list) { Console.WriteLine(item); }
list.GetEnumerator()
获取迭代器对象;
MoveNext()
让光标移动;
Current
获取当前元素;如果
MoveNext()
返回false
,表示结束。这种方式比较传统,但更清晰地揭示了迭代器底层的本质。
方法2:使用yield return简化实现class CustomList2 : IEnumerable { private int[] list = new int[] { 1, 2, 3, 4, 5, 6, 7, 8 }; public IEnumerator GetEnumerator() { for (int i = 0; i < list.Length; i++) { yield return list[i]; // 自动保存状态 } } }
yield return
是什么?
yield return
是 C# 提供的语法糖;每次调用
MoveNext()
时,都会从yield return
上次停止的位置继续;不需要手动维护
position
、Current
、Reset
;编译器会自动为你生成一个内部的状态机类,来实现
IEnumerator
。✅ 优点:
代码更清晰;
不易出错;
推荐实际开发中优先使用。
三、支持泛型的迭代器class CustomList<T> : IEnumerable { private T[] array; public CustomList(params T[] array) { this.array = array; } public IEnumerator GetEnumerator() { for (int i = 0; i < array.Length; i++) { yield return array[i]; } } }
你在
Main
方法中使用了:CustomList<string> list2 = new CustomList<string>("123", "321", "333", "555"); foreach (string item in list2) { Console.WriteLine(item); }
说明这个泛型迭代器可以灵活适配任意类型:
int
,string
,MyClass
,等等。四、总结对比表
特点 IEnumerator
实现yield return
实现代码复杂度 高:要手动写 Current
/MoveNext
等低:只用 yield return
状态维护 手动管理 position
编译器自动管理 控制灵活性 高:可定制更多逻辑 中等:适用于顺序遍历 推荐使用场景 教学/需要完全控制迭代过程的情况 实际开发/遍历逻辑简单时首选
特殊语法
一、var隐式类型变量
var i = 5; // 推断为 int var s = "123"; // 推断为 string var array = new int[] { 1, 2, 3, 4 };
✅ 特点:
项目 内容 推断方式 编译器通过右侧初始化表达式来判断类型 使用限制 不能声明为类字段,只能用于局部变量
必须立即初始化优点 减少重复、增强泛型兼容性 ✅ 推荐:配合 匿名类型、LINQ 查询 使用非常方便!
二、对象初始化设定器(Object Initializer )Person p = new Person(100) { sex = true, Age = 18, Name = "怪盗基德" };
本质:语法糖
Person p = new Person(100); p.sex = true; p.Age = 18; p.Name = "怪盗基德";
优势:
更直观地一次性初始化对象;
常用于构造匿名对象列表、UI数据绑定等场景。
三、集合初始化设定器(Collection Initializer )List<int> listInt = new List<int>() { 1, 2, 3 }; Dictionary<int, string> dic = new Dictionary<int, string>() { { 1, "123" }, { 2, "456" } };
本质:调用 .Add()方法
上面的代码相当于
listInt.Add(1)
,dic.Add(1, "123")
支持集合类、泛型、字典等
✅ 推荐在初始化数据列表或配置参数时使用。
四、匿名类型var v = new { age = 10, money = 11, name = "小明" };
✅ 特点:
类型在编译时由编译器生成;
只读属性;
不能作为方法参数或返回值(除非用
dynamic
);常用于临时数据封装、LINQ 查询结果。
五、可控类型(Nullable)int? c = 3; // 值类型后加 ?,表示可以为 null
常用成员和用法:
方法/语法 说明 c.HasValue
是否有值 c.Value
获取实际值(前提是非 null) c.GetValueOrDefault()
获取值或默认值 ?.
安全访问(null 不调用方法) ??
空合并运算符 object o = null; Console.WriteLine(o?.ToString()); // 安全,不会抛异常
六、空合并操作符 ??
int? intV = null; int intI = intV ?? 100; // 如果 intV 为 null,则使用 100
简化了三目运算符
常用于“设置默认值”、“可空结构体”、“空对象处理”等场景。
七、字符串插值$string name = "怪盗基德"; int age = 18; Console.WriteLine($"你好,{name},你今年{age}岁");
优于旧的拼接方式:
Console.WriteLine("你好," + name + ",你今年" + age + "岁");
可读性高、效率好,配合格式化也很强:
Console.WriteLine($"金额:{123.456:F2}元");
八、单句逻辑简写
if (true) Console.WriteLine("OK");
✅ 原则:
如果
if/for/while
后面只有一句代码,可以省略大括号;适合短逻辑,但不建议滥用,否则不易维护;
🚨 建议:开发中要根据可读性判断是否省略大括号!
总结:特殊语法总览
语法 作用 推荐使用场景 var
自动类型推断 局部变量、匿名类型、LINQ 对象初始化器 一步初始化对象的多个属性 创建配置类、测试数据初始化 集合初始化器 初始化列表/字典 构造预设数据结构 匿名类型 临时数据结构 LINQ、展示数据、只读包装 可空类型 值类型支持 null 数据库值、条件赋值 ??
空合并赋默认值 设置默认配置、空判断优化 ?.
安全访问防止 null 异常 判断前调用方法 $
字符串插值清晰优雅拼接字符串 日志、提示、界面文本 单句简写 精简代码 短逻辑(注意可读性)
值类型 VS 引用类型
✅ 一、值类型 vs 引用类型:本质区别
特性 值类型(Value Type) 引用类型(Reference Type) 存储位置 栈(Stack) 栈上存引用,数据存在堆(Heap) 数据存储方式 存储实际值本身 存储引用地址(指针) 拷贝行为 拷贝值(独立副本) 拷贝引用(共享同一内存) GC管理 系统自动回收(栈自动出栈) 垃圾回收机制(GC)管理堆内存 类型声明方式 struct
,enum
, 所有基本类型class
,interface
,delegate
,string
,array
修改是否互相影响 不影响(值是拷贝) 会影响(同一引用) ✅ 二、变量的生命周期与作用域
int i = 1; // 中层语句块:局部变量 Test t = new Test(); // t 是引用变量,new 出的对象在堆上
🔁 生命周期说明:
局部变量:在中/底层语句块声明(函数/条件/循环),执行完就被销毁
成员变量/静态变量:在类中声明,有更长生命周期(跟随类或类加载周期)
✅ 三、结构体中的值和引用
struct TestStrict { public Test t; // 引用类型 public int i; // 值类型 }
🔍 存储分析:
TestStrict
是值类型,存在栈中;其中的
int i
直接存在栈中;其中的
Test t
是引用类型,它的“地址”存在栈中,“对象本体”仍在堆上;✅ 小结:
结构体中的引用类型成员,其实只是持有了引用,不是像值一样被复制!
✅ 四、类中的值和引用
class Test { int b = 0; // 值类型,存在堆中 string str = "123"; // 引用类型(string是特殊的) TestStrict ts = new TestStrict(); // 引用类型成员,堆中也有引用 }
💡 规律:
类本身是引用类型,所有成员(不管是值类型还是引用类型)都存在堆中;
类中的值类型字段也随着类的对象一起在堆上分配;
✅ 五、数组的值/引用存储规则
int[] arrayInt = new int[5]; // 数组是引用类型 → 在堆中开空间 object[] objs = new object[5]; // 引用类型数组 → 堆中存5个“引用房间”
🌟 总结:
int[]
是值类型数组,堆中存实际值;
object[]
是引用类型数组,堆中存5个引用地址,每个引用指向堆上的对象;
✅ 六、结构体实现接口后的装箱拆箱
TestStruct obj1 = new TestStruct(); ITest iObj1 = obj1; // 🔁 装箱:结构体转接口,会复制内容到堆中
🎯 装箱(Boxing)机制:
将值类型对象赋值给接口/引用类型,会自动拷贝到堆中,产生一个新的引用;
拷贝的是“副本”,原值和副本是分离的!
🔧 示例解释:
TestStruct obj1 = new TestStruct(); obj1.Value = 1; ITest iObj1 = obj1; // 装箱:堆上新建了副本 iObj1.Value = 99; // 修改了副本! Console.WriteLine(obj1.Value); // 1(原始结构体未变) Console.WriteLine(iObj1.Value); // 99(副本)
💥 所以结构体配合接口使用时要小心“装箱开销”和“值丢失”问题。
✅ 七、图示理解(简化模型)
// 类 Test t = new Test(); // 栈: t ──────┐ // ↓ // 堆: Test 实例 { int b, string str, TestStrict ts } // 结构体 TestStrict ts = new TestStrict(); // 栈: ts.i --> 值 // ts.t --> 引用地址(堆中有个 Test 实例) // 数组(值类型) int[] arr = new int[3]; // 栈: arr ─────┐ // ↓ // 堆: [0, 0, 0] // 数组(引用类型) Game[] games = new Game[2]; // 栈: games ─────┐ // ↓ // 堆: [null, null](每个元素存一个“引用房间”)
✅ 实战建议
场景 推荐做法 性能敏感场景(如游戏) 结构体少用接口,避免频繁装箱 多人共享数据结构 使用引用类型类 不可变数据、传值处理 使用结构体,值不会被修改 初始化结构体时小心 结构体作为类字段时不要直接赋值 检查类型值/引用 用 F12
进源码,看class
orstruct
🔚 总结精华口诀
值类型拷贝不连心,引用传递共悲欢;
类在堆上值也跟,结构体中要分身;
数组值存堆中房,引用数组指指忙;
接口装箱要小心,副本修改主不明。