C#进阶学习日记

发布于:2025-07-12 ⋅ 阅读:(20) ⋅ 点赞:(0)

迭代器

迭代器(Iterator)一种设计模式,让你可以顺序访问某个集合(如数组、链表)中的每一个元素,而不需要知道它的内部结构

在 C# 中,能被 foreach 遍历的对象,都实现了迭代器机制。

实现迭代器的两种方式
 方法1:手动实现IEnumeratorIEnumerable

class 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);
}
  1. list.GetEnumerator() 获取迭代器对象;

  2. MoveNext() 让光标移动;

  3. Current 获取当前元素;

  4. 如果 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 returnC# 提供的语法糖

  • 每次调用 MoveNext() 时,都会从 yield return 上次停止的位置继续;

  • 不需要手动维护 positionCurrentReset

  • 编译器会自动为你生成一个内部的状态机类,来实现 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 or struct


🔚 总结精华口诀


 

值类型拷贝不连心,引用传递共悲欢;
类在堆上值也跟,结构体中要分身;
数组值存堆中房,引用数组指指忙;
接口装箱要小心,副本修改主不明。






 


网站公告

今日签到

点亮在社区的每一天
去签到