C#基础入门2 <枚举><结构体><委托和事件><泛型>

发布于 14 天前  52 次阅读


C# 进阶知识点总结

适合初学者的C#进阶指南


目录

  1. 枚举 (enum)
  2. 结构体 (struct)
  3. 运算符重载
  4. 装箱与拆箱
  5. ArrayList 为什么不推荐
  6. 委托 (delegate)
  7. 事件 (event)
  8. Lambda 表达式
  9. in / out / ref 参数修饰符
  10. 泛型 (Generic)

1. 枚举 (enum)

枚举用于定义一组命名的常量,让代码更易读。

基本语法

// 定义枚举
public enum Weekday
{
    Monday,    // 默认从0开始
    Tuesday,   // 1
    Wednesday, // 2
    Thursday,  // 3
    Friday,    // 4
    Saturday,  // 5
    Sunday     // 6
}

// 也可以手动指定值
public enum StatusCode
{
    Success = 200,
    NotFound = 404,
    Error = 500
}

使用枚举

Weekday today = Weekday.Friday;
Console.WriteLine(today);          // 输出: Friday
Console.WriteLine((int)today);     // 输出: 4

StatusCode code = StatusCode.NotFound;
Console.WriteLine((int)code);      // 输出: 404

枚举的底层

枚举本质上是整数,所以可以与 int 相互转换:

int dayNum = 3;
Weekday day = (Weekday)dayNum;     // int → enum
Console.WriteLine(day);            // Thursday

int num = (int)Weekday.Sunday;     // enum → int
Console.WriteLine(num);            // 6

switch 配合枚举

public string GetDayMessage(Weekday day)
{
    switch (day)
    {
        case Weekday.Monday:
            return "开始新的一周!";
        case Weekday.Friday:
            return "明天就是周末啦!";
        case Weekday.Saturday:
        case Weekday.Sunday:
            return "休息日";
        default:
            return "工作日";
    }
}

2. 结构体 (struct)

结构体是值类型,与类(引用类型)有重要区别。

值类型 vs 引用类型

特性 结构体 (struct) 类 (class)
类型 值类型 引用类型
存储位置 栈 (Stack) 堆 (Heap)
赋值行为 复制整个对象 只复制引用
默认值 自动初始化为零 默认 null
性能 更轻量、更快 有GC开销
适用场景 小型数据结构 复杂对象、需要继承

图解

【值类型 (struct)】
┌─────────────────┐
│  栈              │
│  ┌─────────────┐ │
│  │ a: Point    │ │ ← 完整数据
│  │ x=10, y=20   │ │
│  └─────────────┘ │
│                  │
│  ┌─────────────┐ │
│  │ b: Point    │ │ ← 复制了一份新数据
│  │ x=10, y=20   │ │
│  └─────────────┘ │
└─────────────────┘

【引用类型 (class)】
┌─────────────────┐    ┌─────────────────┐
│  栈              │    │  堆              │
│  ┌─────────────┐ │    │  ┌─────────────┐ │
│  │  a: Person   │─┼───────│ Person对象    │ │
│  │  (引用地址)   │ │    │  │ name="张三"  │ │
│  └─────────────┘ │    │  └─────────────┘ │
│                  │    │                  │
│  ┌─────────────┐ │    │  ┌─────────────┐ │
│  │  b: Person   │─┼─┐  │  │ Person对象    │ │
│  │  (引用地址)   │ │ └─────│ name="李四"  │ │
│  └─────────────┘ │    │  └─────────────┘ │
└─────────────────┘    └─────────────────┘

结构体示例

// 定义结构体
public struct Point
{
    public int X { get; set; }
    public int Y { get; set; }

    public Point(int x, int y)
    {
        X = x;
        Y = y;
    }

    public double DistanceTo(Point other)
    {
        int dx = X - other.X;
        int dy = Y - other.Y;
        return Math.Sqrt(dx * dx + dy * dy);
    }
}

// 使用
Point a = new Point(0, 0);
Point b = new Point(3, 4);
Console.WriteLine(a.DistanceTo(b));  // 5

赋值行为对比

// 结构体 - 赋值时复制整个对象
Point p1 = new Point(1, 2);
Point p2 = p1;        // 复制了一份!
p2.X = 10;
Console.WriteLine(p1.X);  // 1 ← p1没变

// 类 - 赋值时复制引用(共享同一个对象)
Person c1 = new Person { Name = "张三" };
Person c2 = c1;       // 指向同一个对象
c2.Name = "李四";
Console.WriteLine(c1.Name);  // 李四 ← c1也变了!

什么时候用结构体?

// ✅ 适合用结构体的场景:
public struct RGBColor { public byte R, G, B; }           // 小型数据
public struct Vector3 { public float X, Y, Z; }            // 几何数据
public struct DateTimeOffset { ... }                      // .NET内置值类型

// ❌ 不适合用结构体的场景:
public struct Employee { public string Name; ... }          // 包含大对象
public struct Order { public List<Item> Items; }           // 需要继承

3. 运算符重载

允许自定义类在使用运算符时的行为。

基本语法

public class Vector2
{
    public int X { get; set; }
    public int Y { get; set; }

    public Vector2(int x, int y)
    {
        X = x;
        Y = y;
    }

    // 重载 + 运算符
    public static Vector2 operator +(Vector2 a, Vector2 b)
    {
        return new Vector2(a.X + b.X, a.Y + b.Y);
    }

    // 重载 == 运算符(必须同时重载 !=)
    public static bool operator ==(Vector2 a, Vector2 b)
    {
        return a.X == b.X && a.Y == b.Y;
    }

    public static bool operator !=(Vector2 a, Vector2 b)
    {
        return !(a == b);
    }
}

使用

Vector2 v1 = new Vector2(1, 2);
Vector2 v2 = new Vector2(3, 4);
Vector2 v3 = v1 + v2;          // 自动调用我们的 + 重载
Console.WriteLine(v3.X);       // 4
Console.WriteLine(v3.Y);       // 6

可以重载的运算符

运算符 说明
+, -, *, /, % 算术运算符
==, !=, <, >, <=, >= 比较运算符
&&, \|\| 不能重载!
= 不能重载

4. 装箱与拆箱

什么是装箱?

值类型 → 引用类型 的转换,需要在堆上分配内存。

int i = 123;        // 值类型,在栈上
object o = i;       // 装箱!把值类型包装成引用类型

// 内部发生的事:
// 1. 在堆上分配内存
// 2. 复制栈上的值到堆
// 3. 返回堆上的引用地址

什么是拆箱?

引用类型 → 值类型 的转换,需要类型检查。

object o = 123;     // 装箱
int i = (int)o;      // 拆箱!从堆上取回值类型

// 如果类型不匹配,会抛异常:
object o2 = "hello";
// int i2 = (int)o2;  // ❌ InvalidCastException!

图解装箱拆箱

┌─────────────────────────────────────────────┐
│  装箱 (Boxing)                               │
│  ┌───────┐         ┌───────────────────┐    │
│  │ int i │  ────►  │ object o          │    │
│  │ = 123 │         │ ┌───────────────┐ │    │
│  └───────┘         │ │ 123 (堆上)     │ │    │
│   栈               │ └───────────────┘ │    │
│                    └───────────────────┘    │
│                     堆                      │
└─────────────────────────────────────────────┘

┌─────────────────────────────────────────────┐
│  拆箱 (Unboxing)                             │
│  ┌───────────────────┐    ┌───────┐         │
│  │ object o          │───►│ int i │         │
│  │ ┌───────────────┐ │    │ = 123 │         │
│  │ │ 123 (堆上)     │ │    └───────┘         │
│  │ └───────────────┘ │     栈               │
│  └───────────────────┘                      │
│   堆                   ✓ 需要类型检查        │
└─────────────────────────────────────────────┘

性能问题

装箱拆箱有性能开销,应尽量避免:

// ❌ 性能差:大量装箱
ArrayList list = new ArrayList();
for (int i = 0; i < 100000; i++)
{
    list.Add(i);  // 每次都装箱!int → object
}

// ✅ 性能好:使用泛型
List<int> list2 = new List<int>();
for (int i = 0; i < 100000; i++)
{
    list2.Add(i);  // 没有装箱!
}

什么时候会发生装箱?

int num = 10;

// 这些都会装箱!
object o = num;                    // 直接赋值
Console.WriteLine(num);            // Console.WriteLine 接收 object
ArrayList list = new ArrayList();
list.Add(num);                     // ArrayList.Add 接收 object
MethodThatTakesObject(num);        // 方法参数是 object

// 解决办法:用泛型
List<int> list = new List<int>();
list.Add(num);                     // 不装箱!

5. ArrayList 为什么不推荐

ArrayList 的问题

// ArrayList 可以存储任意类型
ArrayList list = new ArrayList();
list.Add(10);          // int → object (装箱)
list.Add("hello");     // string
list.Add(new Person()); // 对象

// 读取时返回 object,需要强制转换
int num = (int)list[0];    // 拆箱
string str = (string)list[1];

三大问题

问题 说明
装箱拆箱 存储值类型时会装箱,读取时拆箱,性能差
类型不安全 可以放入任何类型,编译时不检查
需要强制转换 读取时必须手动转换,容易出错

推荐:使用泛型 List

// ✅ 泛型List - 类型安全,没有装箱
List<int> numbers = new List<int>();
numbers.Add(10);              // 直接添加,不会装箱
int num = numbers[0];          // 直接读取,不需要转换

List<string> words = new List<string>();
words.Add("hello");
string word = words[0];        // 类型安全,编译时就检查

// ❌ 编译错误!类型安全
numbers.Add("hello");  // 错误:不能把string放入List<int>

一句话总结

ArrayList = 装箱拆箱 + 类型不安全 + 强制转换 = 不推荐

List = 无装箱拆箱 + 类型安全 + 无需转换 = 推荐


6. 委托 (delegate)

委托是一种类型安全的函数指针,可以引用方法。

基本概念

// 1. 声明委托类型
public delegate int Calculate(int a, int b);

// 2. 定义符合委托的方法
public int Add(int a, int b) { return a + b; }
public int Multiply(int a, int b) { return a * b; }

// 3. 创建委托实例
Calculate calc = Add;           // 引用 Add 方法
Calculate calc2 = Multiply;     // 引用 Multiply 方法

// 4. 调用
int result = calc(5, 3);        // 调用 Add(5, 3),结果 8
int result2 = calc2(5, 3);     // 调用 Multiply(5, 3),结果 15

委托的类型安全

// ❌ 编译错误!方法签名不匹配
Calculate calc = Add;        // Add 签名是 (int, int) → int ✓
Calculate calc2 = "hello";   // 错误!不能赋字符串

// 方法参数或返回值不匹配也会报错
public int Subtract(int a, int b, int c) { return a - b - c; }
Calculate calc3 = Subtract;   // ❌ 编译错误!参数个数不匹配

多播委托

一个委托可以引用多个方法

public delegate void Action(string message);

public void SendEmail(string msg) { Console.WriteLine("邮件: {msg}"); }
public void SendSMS(string msg) { Console.WriteLine("短信: {msg}"); }
public void Log(string msg) { Console.WriteLine($"日志: {msg}"); }

// 创建多播委托
Action actions = SendEmail;
actions += SendSMS;    // 添加
actions += Log;        // 再添加

// 调用一次,所有方法都会执行
actions("你好!");
// 输出:
// 邮件: 你好!
// 短信: 你好!
// 日志: 你好!

// 移除方法
actions -= SendSMS;
actions("再见!");
// 输出:
// 邮件: 再见!
// 日志: 再见!

内置委托类型

C# 提供了常用的内置委托,不需要自己声明:

// Action - 无返回值的方法
Action<string> print = Console.WriteLine;
Action add = () => Console.WriteLine("添加成功");

// Func - 有返回值的方法
Func<int, int, int> calculate = (a, b) => a + b;
Func<int> getRandom = () => new Random().Next();

// Predicate - 返回 bool 的方法
Predicate<int> isEven = n => n % 2 == 0;

7. 事件 (event)

事件是基于委托的封装,限制外部只能订阅/取消订阅,不能直接调用

为什么需要事件?

// ❌ 问题:用委托,外部可以直接调用(不安全)
public class Button
{
    public Action OnClick;  // 外部可以直接调用:button.OnClick()
}

Button btn = new Button();
btn.OnClick();  // 谁都可以随便调用!

用事件保护委托

// ✅ 事件 - 外部只能 += 和 -=,不能直接调用
public class Button
{
    // 使用 event 关键字
    public event Action OnClick;

    public void Click()
    {
        Console.WriteLine("按钮被点击");
        // 只有内部可以触发事件
        OnClick?.Invoke();  // 安全调用
    }
}

Button btn = new Button();
btn.OnClick += () => Console.WriteLine("处理点击1");  // 可以订阅
btn.OnClick += () => Console.WriteLine("处理点击2");  // 再次订阅

btn.Click();  // 输出:
// 按钮被点击
// 处理点击1
// 处理点击2

// ❌ 外部不能这样调用:
// btn.OnClick();  // 编译错误!

事件 vs 委托

特性 委托 事件
外部可以调用 ✅ 可以 ❌ 不可以
外部可以订阅 ✅ 可以 ✅ 可以
外部可以取消订阅 ✅ 可以 ✅ 可以
封装性
使用场景 回调、函数参数 通知机制

典型应用:观察者模式

public class Stock
{
    // 事件
    public event Action<decimal> PriceChanged;

    private decimal _price;
    public decimal Price
    {
        get => _price;
        set
        {
            if (_price != value)
            {
                _price = value;
                // 价格变化时通知所有订阅者
                PriceChanged?.Invoke(_price);
            }
        }
    }
}

// 使用
Stock apple = new Stock { Price = 100 };

apple.PriceChanged += (newPrice) => 
    Console.WriteLine($"价格变为: {newPrice}");

apple.Price = 105;  // 触发事件
apple.Price = 105;  // 价格没变,不触发

// 输出: 价格变为: 105

8. Lambda 表达式

Lambda 是匿名函数的简写语法,常用于委托。

基本语法

// 完整写法
Func<int, int, int> add = (int a, int b) => { return a + b; };

// 简化:类型推断(最常用)
Func<int, int, int> add2 = (a, b) => { return a + b; };

// 简化:单表达式可省略 return 和 {}
Func<int, int, int> add3 = (a, b) => a + b;

// 单参数可省略括号
Predicate<int> isPositive = n => n > 0;

// 无参数
Action greet = () => Console.WriteLine("你好!");

Lambda 配合委托使用

// 传统方法
public bool IsEven(int n) { return n % 2 == 0; }
List<int> nums = new List<int> { 1, 2, 3, 4, 5 };

// 用 Lambda 过滤
List<int> evens = nums.FindAll(n => n % 2 == 0);
foreach (var n in evens) Console.WriteLine(n);
// 输出: 2, 4

// 用 Lambda 排序
nums.Sort((a, b) => b.CompareTo(a));
// 从大到小排序: 5, 4, 3, 2, 1

Lambda 在 LINQ 中的应用

List<int> numbers = new List<int> { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };

var result = numbers
    .Where(n => n % 2 == 0)           // 过滤偶数
    .Select(n => n * n)               // 平方
    .OrderByDescending(n => n)       // 降序排列
    .Take(3);                         // 取前3个

foreach (var n in result)
    Console.WriteLine(n);
// 输出: 100, 64, 36

9. in / out / ref 参数修饰符

ref - 传入引用,可读可写

public void Swap(ref int a, ref int b)
{
    int temp = a;
    a = b;
    b = temp;
}

int x = 5, y = 10;
Swap(ref x, ref y);
Console.WriteLine($"x={x}, y={y}");  // x=10, y=5

out - 传出参数,必须返回

// 解析字符串返回多个值
public bool TryParse(string input, out int result)
{
    try
    {
        result = int.Parse(input);
        return true;
    }
    catch
    {
        result = 0;
        return false;
    }
}

if (int.TryParse("123", out int number))
{
    Console.WriteLine($"解析成功: {number}");
}
else
{
    Console.WriteLine("解析失败");
}

in - 只读传入(性能优化)

public double CalculateDistance(in Point a, in Point b)
{
    // a 和 b 在方法内只能读取,不能修改
    int dx = a.X - b.X;  // ✓ 可以读取
    // a.X = 100;         // ❌ 编译错误!不能修改
    return Math.Sqrt(dx * dx + (a.Y - b.Y) * (a.Y - b.Y));
}

// 优点:告诉编译器参数不会修改,可以安全地进行优化

三者对比

修饰符 方向 方法内能修改 调用时必须初始化 调用时必须加修饰符
值传递 参数需要 不需要
ref 双向 参数需要 需要 (ref)
out 仅输出 参数不需要 需要 (out)
in 仅输入 参数需要 需要 (in)

10. 泛型 (Generic)

泛型让你编写可复用的代码,同时保持类型安全

为什么需要泛型?

// ❌ 问题:ArrayList 丧失类型信息
ArrayList list = new ArrayList();
list.Add(1);
list.Add("hello");        // 错误运行时才发现
string s = (string)list[0]; // 运行时异常!

// ✅ 解决:泛型 - 编译时类型检查
List<int> list2 = new List<int>();
list2.Add(1);
list2.Add("hello");       // ❌ 编译错误!
int num = list2[0];       // ✅ 不需要转换

基本语法

// 定义泛型类
public class Container<T>
{
    private T _item;

    public T Item
    {
        get => _item;
        set => _item = value;
    }
}

// 使用
Container<int> intContainer = new Container<int>();
intContainer.Item = 123;

Container<string> strContainer = new Container<string>();
strContainer.Item = "你好";

泛型方法

public T Max<T>(T a, T b) where T : IComparable<T>
{
    return a.CompareTo(b) > 0 ? a : b;
}

Console.WriteLine(Max(3, 5));           // 5
Console.WriteLine(Max("apple", "banana"));  // banana

泛型约束

// where T : class        T 必须是引用类型
// where T : struct       T 必须是值类型
// where T : new()        T 必须有默认构造函数
// where T : Person       T 必须是 Person 或其子类
// where T : IComparable  T 必须实现 IComparable 接口

public class Repository<T> where T : class, new()
{
    public List<T> GetAll()
    {
        return new List<T>();  // 因为有 new() 约束,可以创建实例
    }
}

常用泛型类型

// 泛型集合
List<T>           // 动态数组
Dictionary<TKey, TValue>  // 键值对
HashSet<T>        // 不重复集合
Queue<T>          // 队列 (先进先出)
Stack<T>          // 栈 (先进后出)

// 泛型类/接口
IComparable<T>
IEnumerable<T>
IComparer<T>