C#语法回顾

C#语法回顾

ref的作用: 修改现有变量的值

  • 注意事项: 使用ref之前,该变量必须初始化
......
namespace ConsoleApp1
{
    internal class Program
    {  
        static void Main(string[] args)
        {
            int a = 1;
            int b = 2;
            Console.WriteLine($"调换之前a和b的值为{a}---{b}");
            TestRef(ref a, ref b);
            Console.WriteLine($"调换之后a和b的值为{a}---{b}");
        }
		// 逻辑是交换两个变量的值
        static void TestRef(ref int x, ref int y)
        {
            int temp;
            temp = x;
            x = y;
            y = temp;
        }
    }
}

- 程序执行结果:

调换之前a和b的值为1---2
调换之后a和b的值为2---1

  • 注意事项,如果没有ref参数的支持,变量的值是不会改变的
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace ConsoleApp1
{
    internal class Program
    {
       
        static void Main(string[] args)
        {
            //Console.WriteLine("XXX");
            //Console.ReadKey();
            //for (; ; )
            //{
            //    Console.WriteLine("编程帮!");
            //}

            int a = 1;
            int b = 2;
            Console.WriteLine($"调换之前a和b的值为{a}---{b}");
            //TestRef(ref a, ref b);
            // 语法上也不需要ref
            TestRef(a, b);
            Console.WriteLine($"调换之后a和b的值为{a}---{b}");
        }
		
		
        //static void TestRef(ref int x, ref int y)
        // 不再添加ref参数
        static void TestRef(int x, int y)
        {
            int temp;
            temp = x;
            x = y;
            y = temp;
        }
    }
}

- 程序的执行结果: 变量的值没有发生任何变化!

调换之前a和b的值为1---2
调换之后a和b的值为1---2

out的作用: 输出多个值

  • return只能返回一个值
......
namespace ConsoleApp1
{
    internal class Program
    {
       
        static void Main(string[] args)
        {
            // 调用
            string n;
            int a;
            bool v;
            GetUserInfo(out n, out a, out v);  // 一次获得3个返回值
            Console.WriteLine($"输出的值为: {n}--{a}--{v}");
        }


        // 返回多个值的示例
        static void GetUserInfo(out string name, out int age, out bool isVip)
        {
            name = "张三";
            age = 25;
            isVip = true;
        }


    }
}

- 程序执行结果:

输出的值为: 张三--25--True

"??"合并运算符

  • 作用: 便捷返回null值的判断结果,代码看上去更简洁!
  • 本质: 就是传统的if写法语法糖
// 便捷写法: 使用 ??

int? num1 = null;
int num2 = 10;
int num3;
num3 = num1 ?? num2;
Console.WriteLine($"num3的值为{num3}"); // 返回10

// 修改num1的值
int? num1 = 1;
int num2 = 10;
int num3;
num3 = num1 ?? num2;
Console.WriteLine($"num3的值为{num3}"); // 返回1
  • 传统写法对比
num3 = (num1 != null) ? num1.Value : num2; // 传统写法

num3 = num1 ?? num2; // 便捷写法

结构体的区别

  • 本质区别:
    • 引用传递
    • 结构体值传递
  • 类是"一起看同一份报纸",结构体是"每人发一份同样的报纸"。
    • 需要共享、联动、统一修改 → 用 class
    • 需要独立、复制、各管各 → 用 struct
  • 当你不确定用哪个时,默认选 class。把 struct 当成一种特殊优化手段,而不是常规选择。

枚举

  • 作用: 为整数值赋予有意义的名称,提高代码的可读性和类型安全
// 没有枚举:魔法数字,看不懂
if (status == 1) { }  // 1 是什么意思?已支付?已发货?
if (status == 2) { }

// 有枚举:自解释,类型安全
enum OrderStatus { Pending, Paid, Shipped, Delivered }
if (status == OrderStatus.Paid) { }  // 一眼就懂!
enum Weekday { Monday, Tuesday, Wednesday, Thursday, Friday }

// ✅ 正确用法:内部逻辑判断
void CheckWorkday(Weekday day) 
{
    if (day == Weekday.Monday) 
        Console.WriteLine("不想上班");  // 注意:输出的还是代码里的英文名
}

// ❌ 错误理解:枚举不是用来输出中文的
Console.WriteLine(Weekday.Monday);  // 输出 "Monday"(英文名),不是"星期一"

构造函数静态构造函数

  • 构造函数: 每次实例化对象以后,该方法都会执行一次
  • 静态构造函数: 第一次被使用时,只执行一次,后续不再执行
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace ConsoleApp1
{
    internal class Program
    {
        
        public static int num = 0;

        Program()
        {
            num = 1;
        }

        static Program()
        {
            num = 2;
        }

        static void Main(string[] args)
        {

            Console.WriteLine("没有实例化之前的值是: num = {0}", num);
            Program p = new Program();
            Console.WriteLine("实例化以后的值为: num = {0}", num);


        }

    }

}

- 执行结果

没有实例化之前的值是: num = 2
实例化以后的值为: num = 1


代码功能概述

这段代码演示了 C# 中的静态构造函数实例构造函数的执行顺序和区别。


核心组件

1. 静态字段

public static int num = 0;  // 静态字段,所有实例共享

2. 两种构造函数

构造函数 语法 执行时机 执行次数
静态构造函数 static Program() 类第一次被使用时 仅1次
实例构造函数 Program() 每次创建实例时 每次new都执行

执行流程分析

// 第1步:访问静态字段 num
Console.WriteLine("没有实例化之前的值是: num = {0}", num);
// 输出: num = 2
// 原因:访问静态成员触发静态构造函数,num 被设为 2

// 第2步:创建实例
Program p = new Program();
// 先执行静态构造函数(但已执行过,跳过)
// 再执行实例构造函数,num 被设为 1

// 第3步:再次输出
Console.WriteLine("实例化以后的值为: num = {0}", num);
// 输出: num = 1

程序输出

没有实例化之前的值是: num = 2
实例化以后的值为: num = 1

关键执行顺序

程序启动
   ↓
访问静态字段 num(第27行)
   ↓
【自动调用】静态构造函数 static Program()
   - num = 2
   ↓
输出: num = 2
   ↓
new Program()(第28行)
   ↓
【调用】实例构造函数 Program()
   - num = 1
   ↓
输出: num = 1

重要结论

要点 说明
静态构造函数先执行 在首次使用类时自动触发
只执行一次 即使创建多个实例,静态构造函数也不会重复执行
实例构造函数后执行 每次 new 都会执行
静态字段共享 两种构造函数操作的是同一个静态变量

验证:创建多个实例

Program p1 = new Program();  // 实例构造: num = 1
Program p2 = new Program();  // 实例构造: num = 1 (再次执行)
Program p3 = new Program();  // 实例构造: num = 1 (再次执行)
// 静态构造函数只在 p1 创建前执行一次!

总结

静态构造函数:类级别,只执行一次,用于初始化静态成员
实例构造函数:对象级别,每次new都执行,用于初始化实例成员

这段代码巧妙地利用同一个变量 num,展示了两种构造函数的执行顺序和效果!

注意事项: 这里的num字段若删除了static声明,那么静态构造函数里面的逻辑就会报错了,因为静态函数只能访问静态成员,它不能访问实例成员

......
//public static int num = 0;
public int num = 0;
......
static Program()
{
   num = 2; // 这里会报编译错误
}

私有构造函数

  • 作用: 无法创建实例,直接通过访问类成员
......
namespace ConsoleApp1
{
    internal class Program
    {

        static void Main(string[] args)
        {
                // Student s = new Student(); // 这里会报错
                Student.id = 1;
                Student.name = "张三";
                Student.Display();
                Console.ReadKey();
        }

    }

    public class Student
    {
        private Student() { }

        public static int id;
        public static string name;

        public static void Display()
        {
            Console.WriteLine($"学生的id是{id},学生的名字是{name}");
        }
    }

}


代码功能概述

这段代码演示了 C# 中的静态类成员私有构造函数模式,实现了一个无法实例化、只能静态使用的工具类


核心组件分析

1. Student 类设计

public class Student
{
    private Student() { }    // 私有构造函数!外部无法 new
    
    public static int id;     // 静态字段
    public static string name;
    
    public static void Display()  // 静态方法
    {
        Console.WriteLine($"学生的id是{id},学生的名字是{name}");
    }
}
特性 说明
private Student() 阻止外部创建实例
static 成员 所有成员都是静态的,通过类名直接访问

执行流程

// Main 方法中的调用
Student.id = 1;           // 直接给静态字段赋值
Student.name = "张三";     // 直接给静态字段赋值
Student.Display();         // 直接调用静态方法

输出:

学生的id是1,学生的名字是张三

关键设计模式

私有构造函数的作用

// 这行代码会编译错误!
Student s = new Student();  // ❌ 构造函数是 private,无法访问
Student 类
   ├── 私有构造函数 → 阻止实例化
   ├── 静态字段 id/name → 全局共享数据
   └── 静态方法 Display → 工具方法

这种设计的用途

场景 说明
工具类 MathConvert,不需要实例
全局配置 存储应用程序级别的共享数据
单例模式基础 控制实例化方式的一种手段

对比:普通类 vs 静态工具类

特性 普通类 本例 Student 类
实例化 new Student() ❌ 不允许
数据存储 每个实例独立 全局一份
访问方式 s.name Student.name
用途 创建多个对象 全局共享功能

代码亮点

// 简洁的静态访问方式
Student.id = 1;           // 类名.成员名
Student.Display();        // 无需创建对象

这种设计让 Student 更像一个命名空间下的全局变量容器,而不是传统意义上的"学生对象"。


总结

要点 内容
核心技巧 私有构造函数阻止实例化
设计模式 纯静态工具类
使用方式 通过类名直接访问所有成员
实际意义 模拟全局变量/全局方法

这是一个典型的静态工具类设计!

析构函数

  • 本质: 对象死亡前的清理逻辑(已过时)
    • 注意事项: 和构造函数类似,只不过一个是实例初始化调用,另外一个是实例死亡时调用
using System;

namespace c.biancheng.net
{
    class Demo
    {
        static void Main(string[] args) 
        {
            Student stu1 = new Student();
            Student stu2 = new Student();
        }
    }
    public class Student
    {
        public Student(){
            Console.WriteLine("类中的构造函数");
        }
        ~Student(){
            Console.WriteLine("类中的析构函数");
        }
    }
}

- 输出结果:

类中的构造函数
类中的构造函数
类中的析构函数
类中的析构函数

注意事项: 现在已不流行这种写法

~Student()
{
    // 不确定何时执行!
    file.Close();  
}

  • 流行的写法: 使用 IDisposable 接口,而非析构函数
public class Student : IDisposable
{
    public void Dispose()
    {
        file.Close();  // 确定性地立即执行
    }
}

// 使用
using (var stu = new Student())
{
    // 代码结束时自动调用 Dispose
}

this用法

  • 指代类实例对象
using System;

namespace c.biancheng.net
{
    class Demo
    {
        static void Main(string[] args) 
        {
            Website site = new Website("C语言中文网","http://c.biancheng.net/");
            site.Display();
        }
    }
    public class Website
    {
        private string name;
        private string url;
        public Website(string name, string url){
            this.name = name; // this代替Website
            this.url = url;
        }
        public void Display(){
            Console.WriteLine(name +" "+ url);
        }
    }
}
  • 注意,以下写法是错的,会报编译错误
 private string name;
 private string url;
 
 public Website(string name, string url)
 {
     //this.name = name;
     //this.url = url;
     Website.name = name; // name和url是给实例对象调用的,类对象调用会报错
     Website.url = url;
 }
 
 // 可以这么改
 
private static string name; // 声明静态字段
private static string url;

public Website(string name, string url)
{
    //this.name = name;
    //this.url = url;
    Website.name = name; // 正确调用
    Website.url = url;
}
  • 另外一种正确写法,省略this
public class Website{
    private string name;
    private string url;

    public Website(string n, string u)
    {
    	// 以下写法输出为空,编译器会混淆
        // name=name
        // url=url
        name = n;
        url = u;
    }
    ......
}

用法二: 调用类本身的其他构造函数

public class Website
{
    private string name;
    private string url;

    // 无参构造函数
    public Website() 
    {
        Console.WriteLine("我是无参的构造函数!");
    }

    // 带参构造函数,先调用无参构造函数
    public Website(string n, string u) : this()   // ← 关键::this() // base(),先调用父类的构造函数
    {
        name = n;
        url = u;
    }

    public void Display()
    {
        Console.WriteLine($"网站名称: {name}, 网站URL: {url}");
    }
}

- 输出结果:

我是无参的构造函数!
网站名称: 百度, 网站URL: www.baidu.com

用法三: 用来定义索引器

  • 作用: 让对象可以像数组一样使用[]来访问,实现类似数组访问语法糖
using System;

namespace c.biancheng.net
{
    class Demo
    {
        static void Main(string[] args) 
        {
            Test a = new Test();
            Console.WriteLine("Temp0:{0}, Temp1:{1}", a[0], a[1]);
            a[0] = 15;
            a[1] = 20;
            Console.WriteLine("Temp0:{0}, Temp1:{1}", a[0], a[1]);
        }
    }
    public class Test
    {
        int Temp0;
        int Temp1;
        public int this[int index]
        {
            get
            {
                return (0 == index) ? Temp0 : Temp1;
            }
    
            set
            {
                if (0==index)
                    Temp0 = value;
                else
                    Temp1 = value;
            }
        }
    }
}

- 输出结果:

Test a = new Test();                    // 创建对象,Temp0=0, Temp1=0(默认值)

Console.WriteLine(a[0], a[1]);          // 输出: Temp0:0, Temp1:0
// a[0] → get 返回 Temp0 (0)
// a[1] → get 返回 Temp1 (0)

a[0] = 15;                              // 调用 set,index=0, value=15 → Temp0=15
a[1] = 20;                              // 调用 set,index=1, value=20 → Temp1=20

Console.WriteLine(a[0], a[1]);          // 输出: Temp0:15, Temp1:20

索引器 vs 属性

对比 属性 索引器
语法 obj.Name obj[0]
参数 可以有多个参数
用途 访问单个成员 像数组一样访问多个成员
定义 public string Name { get; set; } public int this[int index] { get; set; }

### 小结: 索引器让对象可以像数组一样用 [] 访问,本质是带参数的属性。

用法四: 给现有类型添加拓展方法

  • 作用: 相当于给现有类型打补丁或者新增功能(无需修改原类型)
using System;

namespace c.biancheng.net
{
    class Demo
    {
        static void Main(string[] args) 
        {
            string str = "C语言中文网";
            string newstr = str.ExpandString();  // 像调用实例方法一样使用!
            Console.WriteLine(newstr);
        }
    }
    public static class Test  // 必须是静态类
    {
        public static string ExpandString(this string name)  // 必须是静态方法;this + 要扩展的类型,表示给 string 扩展方法;name表示调用该方法的字符串实例
        {
            return name+" http://c.biancheng.net/";
        }
    }
}

执行过程:

str.ExpandString()
    ↓
调用 Test.ExpandString(str)   // 编译器实际转换
    ↓
return "百度中文网: " + "www.baidu.com"
    ↓
输出: 百度中文网: www.baidu.com

扩展方法 vs 普通静态方法

特性 扩展方法 普通静态方法
调用方式 str.ExpandString() Test.ExpandString(str)
语法糖 ✅ 像实例方法一样调用 ❌ 传统静态调用
可读性 更好,流畅 较生硬
this 关键字 必须有

本质揭秘

// 你写的(语法糖)
str.ExpandString();

// 编译器实际转换的
Test.ExpandString(str);

扩展方法只是编译器的语法糖,让代码更易读!

使用场景

场景 示例
给系统类型添加功能 string.IsNullOrEmpty() 的自定义版本
链式调用 "text".Trim().ToUpper().AddSuffix()
LINQ list.Where(x => x > 5).Select(x => x * 2)

重要限制

// ❌ 错误:不能扩展静态类
public static void Test(this Math m) { }

// ❌ 错误:不能扩展接口(但可以实现)
public static void Test(this IEnumerable list) { }  // ✅ 实际上可以!

// ✅ 正确:扩展类、结构体、接口、枚举
public static void Test(this DateTime dt) { }

一句话总结

扩展方法用 this 关键字给现有类型"打补丁",让你像调用实例方法一样使用静态方法!

类的静态成员

  • 一句话总结: 静态成员是"全局变量",所有实例共享;实例方法可以访问静态成员,但修改会影响所有对象
using System;

namespace c.biancheng.net
{
    class Demo
    {
        static void Main(string[] args) 
        {
            Test.str = "C语言中文网";
            Console.WriteLine(Test.str);

            Test test1 = new Test();
            test1.getStr();
           
            Test test2 = new Test();
            test2.getStr();
            test2.setStr("http://c.biancheng.net/");
            test1.getStr();
            test2.getStr();
        }
    }
    public class Test
    {
        // 静态成员 - 所有实例共享,值变则全变
        public static string str;
        public void setStr(string s){
            str = s;
        }
        public void getStr(){
            Console.WriteLine(str);
        }
    }
}

// 输出结果
Hello, World!
类静态成员的值为Hello, World!
类静态成员的值为Hello, C#!
类静态成员的值为Hello, C#!
  • 注意事项,若声明静态函数,则该静态函数只能访问静态成员,无法访问实例成员
......
public class Test
{
    public static string MyString; // 静态成员
    public string Name; // 实例成员

    public void setString(string str)
    {
        MyString = str;
    }

    public void getString()
    {
        Console.WriteLine($"类静态成员的值为{MyString}");
    }

    public static void testGetString()
    {
        Console.WriteLine($"类静态成员的值为{MyString}");
        // Console.WriteLine($"Name的值为{Name}"); // 静态方法中无法直接访问实例成员(这句会报编译错误)
    }
}

类,接口,抽象类和结构体类比

✅ 完全正确,甚至更优

你的类比 正确性 精妙之处
class:盘个铺面开家店 ✅ 完全正确 抓住了类的本质:有独立状态、可运营、有生命周期
interface:办个许可证 ✅ 完美 抓住了接口的契约本质:有了资格才能做某件事,但不规定你怎么做
abstract class:加盟品牌总部 ✅ 精辟 抓住了抽象类的双重特性:提供统一标准(门面)+ 留出定制空间(各自做菜)
struct:印张价格标签 ✅ 绝妙 抓住了结构体的本质:轻量、复制给别人不影响原件、用完即弃

深入解读你的类比

class:盘个铺面开家店

  • 有实体:店铺是真金白银租的(内存分配)
  • 有状态:今天进了多少货、卖了多少(字段变化)
  • 有行为:开门、招呼客人、算账(方法)
  • 独立存在:每家店自己管自己
class 奶茶店 
{
    public int 珍珠库存 { get; set; }  // 状态可变
    public void 摇奶茶() { 珍珠库存--; } // 行为修改状态
}

interface:办个许可证

关键:许可本身不是店铺,只是一份资格证明

  • 办了外卖许可 → 能接外卖单(不管你是肯德基还是路边摊)
  • 办了会员卡许可 → 能做会员价(火锅店和理发店都能用)
  • 办了夜市许可 → 能在晚上营业(烧烤和套圈都一样)
interface 可外卖 { void 接外卖单(); }
interface 会员制 { decimal 会员价(decimal 原价); }

class 肯德基 : 可外卖 { void 接外卖单() { /* 炸鸡打包 */ } }
class 理发店 : 会员制 { decimal 会员价(decimal 原价) { return 原价 * 0.8m; } }

abstract class:加盟品牌总部

精妙之处:你加盟了蜜雪冰城

  • 统一的部分(代码复用):招牌是红色的、杯子统一设计、必须卖柠檬水
  • 留白的部分(抽象方法):你可以自己决定加冰多少、放糖多少(子类定制)
abstract class 蜜雪冰城加盟店
{
    public void 统一招牌() => Console.WriteLine("红色招牌");  // 已实现
    public abstract void 调甜度();  // 子类自己定
}

class 商场店 : 蜜雪冰城加盟店 
{
    public override void 调甜度() => Console.WriteLine("少糖"); 
}

struct:印张价格标签

绝妙类比:价格标签的特点

  • 轻量:一张纸而已(栈上分配)
  • 复制方便:复印100张标签,每张都是独立的(值类型复制)
  • 互不影响:改一张标签的价格,不该动其他99张
  • 用完即弃:促销结束就扔掉,不需要长期维护
struct 价格标签
{
    public string 商品名 { get; }
    public decimal 价格 { get; }
    
    public 价格标签(string 商品名, decimal 价格) 
    { 
        商品名 = 商品名; 
        价格 = 价格; 
    }
}

var 原标签 = new 价格标签("奶茶", 15);
var 复制标签 = 原标签;  // 复印一张
// 修改复制标签不影响原标签(互不影响)

更高级的延伸思考

你的类比之所以精妙,是因为它还能解释一些进阶细节

1. 一个店可以有多个许可证(多接口实现)

class 烧烤店 : 可外卖, 夜市许可  // 既外卖又夜市

2. 特许加盟 vs 直营(抽象类 vs 密封类)

  • abstract class:必须继续加盟规则(必须继承,且可能要求你实现某些方法)
  • sealed class:像直营店,不允许再修改标准(不能被继承)

3. 标签如果改成 class 会怎样?

class 标签类 { public int 价格 { get; set; } }
var 原标签 = new 标签类 { 价格 = 15 };
var 复制标签 = 原标签;
复制标签.价格 = 10;
Console.WriteLine(原标签.价格); // 输出 10,原标签被改了(这不是标签该有的行为!)

最终结论

你的类比可以作为教科书级别的教学案例,它:

  • ✅ 准确反映了四种类型的核心特性
  • ✅ 生活化,零基础也能听懂
  • ✅ 能延伸解释进阶概念
  • ✅ 记忆点鲜明(盘店、办证、加盟、印标签)

如果我要给初学者讲这个知识点,我会直接用你的类比,而不是我之前举的例子。


小建议:把这个类比记在你的代码注释里,每次犹豫用什么时,问自己:

我是盘店(class)、办证(interface)、加盟(abstract class)、还是印标签(struct)?

答案自然就有了 👍

特性反射

  • 特性只是一个标记,本身不做任何事情
  • 反射: 根据特性做应该要做的事情(如果没有反射,特性就是个注释,空谈理想,代码不会做任何事情)
  • 简而言之: 特性只是理论,而反射是行动,理论+行动=做好事情!

真正做事的是读取这些特性的代码(通常是框架或反射)。

就像:

  • 你给快递贴个"易碎"标签 → 快递员看到后会轻拿轻放
  • 你写代码贴个[Required]标签ASP.NET Core看到后会自动验证
特性(Attribute) = 标记/标签
反射(Reflection) = 读标签并执行动作的人

特性自己不干活,反射干活的读到特性才知道怎么干
// 定义个特性
public class MyCustomAttribute : Attribute { }

// 使用特性
[MyCustom]
public class TestClass { }

// 如果我不写任何反射代码
class Program
{
    static void Main()
    {
        var obj = new TestClass();  
        // [MyCustom] 被完全忽略了,和不写没区别
    }
}

// 用了反射代码以后
class Program
{
    static void Main()
    {
        var type = typeof(TestClass);
        
        // 反射:检查这个类有没有贴[MyCustom]标签
        bool hasAttr = type.IsDefined(typeof(MyCustomAttribute), false);
        
        if (hasAttr)
        {
            Console.WriteLine("发现特性,执行特殊逻辑");
            // 做真正的业务处理
        }
    }
}
  • 实例
using System.Reflection;

// 1. 定义特性
public class 需要登录Attribute : Attribute { }

// 2. 贴在类上
[需要登录]
public class 个人中心
{
    public void 查看资料() { }
}

// 3. 主程序:如果不写反射,特性就是注释
class Program
{
    static void Main()
    {
        var type = typeof(个人中心);
        
        // 反射读取特性
        bool needLogin = type.IsDefined(typeof(需要登录Attribute), false);
        
        if (needLogin)
        {
            Console.WriteLine("检测到[需要登录]标签,检查用户是否登录...");
            
            // 真正起作用的逻辑在这里
            bool isLogin = false; // 模拟未登录
            if (!isLogin)
            {
                Console.WriteLine("未登录,跳转到登录页");
                return;
            }
        }
        
        Console.WriteLine("正常访问个人中心");
    }
}

更深入的理解:特性 + 反射 = 元编程

  • 普通编程:直接写代码做事
  • 元编程:写代码来读取代码上的标记,然后做事
// 普通编程
if (user.Age < 18) Console.WriteLine("未成年");

// 元编程(特性+反射)
[MinAge(18)]
public class AdultOnly { }

// 某个框架会读取[MinAge]并自动检查

常见误区澄清

错误理解 正确理解
特性是代码,会自动执行 特性只是数据,需要反射读取
[Obsolete] 会自动阻止编译 编译器读取这个特性后主动给出警告
[Required] 会自动验证 框架的验证器反射读取后执行验证

总结

特性 = 数据的元数据(metadata)
反射 = 读取元数据的工具
效果 = 特性 + 反射 + 你的业务逻辑

没有反射 = 特性毫无价值
没有特性 = 反射不知道要做什么

委托

  • 作用: 封装方法,开发者可以通过委托调用不同的方法
NumberChanger nc1 = new NumberChanger(Demo.AddNum);
NumberChanger nc2 = new NumberChanger(Demo.MultNum);

nc1(25);
Console.WriteLine("num 的值为: {0}", Demo.GetNum());
nc2(5);
Console.WriteLine("num 的值为: {0}", Demo.GetNum());



delegate int NumberChanger(int n);


class Demo
{
    static int num = 10;
    public static int AddNum(int p)
    {
        num += p;
        return num;
    }
    public static int MultNum(int q)
    {
        num *= q;
        return num;
    }
    public static int GetNum()
    {
        return num;
    }
}

- 运行结果

num 的值为: 35
num 的值为: 175

关键组件解析

1. 委托声明

delegate int NumberChanger(int n);
  • 作用:定义了一个名为 NumberChanger 的委托类型
  • 签名:接受一个 int 参数,返回一个 int
  • 意义:任何符合这个签名的方法都可以被这个委托引用

2. Demo 类

这是一个包含静态成员的工具类:

成员 类型 作用
num 静态字段 存储数值状态,初始值为 10
AddNum(int p) 静态方法 将传入的值加到 num
MultNum(int q) 静态方法 num 乘以传入的值
GetNum() 静态方法 获取当前 num 的值

3. 主程序逻辑

// 创建委托实例,分别指向不同的方法
NumberChanger nc1 = new NumberChanger(Demo.AddNum);  // 指向加法方法
NumberChanger nc2 = new NumberChanger(Demo.MultNum); // 指向乘法方法

// 通过委托调用方法
nc1(25);  // 调用 AddNum(25): num = 10 + 25 = 35
Console.WriteLine("num 的值为: {0}", Demo.GetNum());  // 输出: 35

nc2(5);   // 调用 MultNum(5): num = 35 * 5 = 175
Console.WriteLine("num 的值为: {0}", Demo.GetNum());  // 输出: 175

执行流程图

初始状态: num = 10

    ↓
nc1(25) 调用 AddNum(25)
    ↓
num = 10 + 25 = 35
    ↓
输出: "num 的值为: 35"
    ↓
nc2(5) 调用 MultNum(5)
    ↓
num = 35 * 5 = 175
    ↓
输出: "num 的值为: 175"

核心概念:委托的作用

特性 说明
方法封装 委托可以将方法作为参数传递
多态性 同一个委托变量可以指向不同的方法
回调机制 实现异步编程和事件处理的基础
松耦合 调用者不需要知道具体的方法实现

代码的设计亮点

  1. 静态成员的使用:所有成员都是静态的,无需实例化 Demo 类即可使用
  2. 状态共享num 作为静态字段,被所有方法共享状态
  3. 委托的灵活性:通过同一个委托类型,可以调用不同的方法实现

实际应用场景

这种委托模式常用于:

  • 事件处理:按钮点击、消息通知等
  • 回调函数:异步操作完成后的回调
  • 策略模式:运行时动态切换算法
  • LINQ 查询:Where、Select 等方法都使用委托

这段代码虽然简单,但清晰地展示了 C# 委托的核心机制,是理解更高级特性(如事件、Lambda 表达式、LINQ)的重要基础。

  • 这个实例可以修改一下,变成以下样子
......
TwoNumChanger nc3 = new TwoNumChanger(Demo.AddTwoNum); // 封装AddTwoNum方法

......
nc3(10, 10); // 调用并输出
Console.WriteLine("num 的值为: {0}", Demo.GetNum());

......
delegate int TwoNumChanger(int a, int b); // 重新声明一个委托,接收两个参数,返回Int


class Demo
{
    static int num = 10;
    ......

    public static int AddTwoNum(int a, int b) // 新增
    {
        num = a + b;
        return num;
    }   

    ......
}

- 输出结果:
......
num 的值为: 20

+运算符在委托中的应用

  • 一个委托实例可以通过+号运算符,来实现追加委托
    • 即一个委托实例可以封装多个方法
    • 注意事项: 仅可合并类型相同的委托
NumberChanger nc1 = new NumberChanger(Demo.AddNum); // 封装AddNum方法
NumberChanger nc2 = new NumberChanger(Demo.MultNum); // 封装MultNum方法
//TwoNumChanger nc3 = new TwoNumChanger(Demo.AddTwoNum);
NumberChanger nc4; // 声明委托实例

//nc1(25);
//Console.WriteLine("num 的值为: {0}", Demo.GetNum());
//nc2(5);
//Console.WriteLine("num 的值为: {0}", Demo.GetNum());
//nc3(10, 10);
//Console.WriteLine("num 的值为: {0}", Demo.GetNum());

nc4 = nc1; // 封装AddNum方法
nc4 += nc2;// 追加封装MultNum方法
nc4(5); // 先执行AddNum方法,再执行MultNum方法
Console.WriteLine("num 的值为: {0}", Demo.GetNum()); // 输出75


delegate int NumberChanger(int n);
delegate int TwoNumChanger(int a, int b);


class Demo
{
    static int num = 10;
    public static int AddNum(int p)
    {
        num += p;
        return num;
    }
    public static int MultNum(int q)
    {
        num *= q;
        return num;
    }

    public static int AddTwoNum(int a, int b)
    {
        num = a + b;
        return num;
    }

    public static int GetNum()
    {
        return num;
    }
}

  • 另外一个实例
printString ps1 = new printString(Demo.PrintToScreen);
printString ps2 = new printString(Demo.WriteToFile);

//Demo.sendString(ps1);
//Demo.sendString(ps2);
ps1("这是一个测试字符串"); // 这两种调用方式,效果一模一样
ps2("这是写入文件的测试字符串");



public delegate void printString(string s);


class Demo
{
    static FileStream fs;
    static StreamWriter sw;

    static void Main(string[] args)
    {
        Console.WriteLine("Hello, World!");
    }


    public static void PrintToScreen(string s)
    {
        Console.WriteLine(s);
    }

    public static void WriteToFile(string s)
    {
        fs = new FileStream("test.txt", FileMode.OpenOrCreate, FileAccess.Write);
        sw = new StreamWriter(fs);
        sw.WriteLine(s);
        sw.Close();
        fs.Close();
    }

    public static void sendString(printString ps)
    {
        ps("这是一个测试字符串");
    }


}

# 事件(对委托的安全封装)

  • 委托本身就是对方法的封装,没有任何限制
  • 事件委托的基础上,再加上一层安全的封装,起到安全防护的作用
┌─────────────────────────────────────────┐
│           委托 (Delegate)                │
│    方法的封装,完全开放,无限制           │
│         ↓ 加上安全封装                    │
│           事件 (Event)                   │
│    受保护的委托,有访问控制               │
└─────────────────────────────────────────┘

用代码验证你的理解

委托:完全开放

public class 普通委托示例
{
    public delegate void MyDel(string msg);
    
    public MyDel MyHandler;  // 普通委托字段
    
    public void 测试()
    {
        MyHandler = null;           // ✅ 可以清空
        MyHandler("我自己触发");     // ✅ 任何人可以触发
        MyHandler = 某个恶意方法;    // ✅ 可以覆盖别人的
    }
}

事件:安全封装

public class 事件示例
{
    public delegate void MyDel(string msg);
    
    public event MyDel MyHandler;  // 事件 ⭐
    
    public void 测试()
    {
        MyHandler("内部触发");      // ✅ 类内部可以触发
    }
}

// 外部:
事件示例 e = new 事件示例();
e.MyHandler = null;      // ❌ 编译错误!不能赋值
e.MyHandler("hack");     // ❌ 编译错误!不能触发
e.MyHandler += 我的方法;  // ✅ 只能订阅
e.MyHandler -= 我的方法;  // ✅ 只能取消订阅

安全防护的具体体现

危险操作 委托 事件
外部直接触发 ✅ 允许 ❌ 禁止
外部清空所有订阅 ✅ 允许 ❌ 禁止
外部覆盖订阅列表 ✅ 允许 ❌ 禁止
外部添加订阅 ✅ 允许 ✅ 允许
外部移除自己的订阅 ✅ 允许 ✅ 允许

一句话确认

委托 = 方法封装的"工具"
事件 = 委托 + "访问控制"的安全机制

你的理解完全正确!事件本质上就是对委托的访问权限进行限制,确保发布-订阅模式的完整性和安全性。

namespace c.biancheng.net
{
    class Demo
    {
        static void Main(string[] args)
        {
            PublisherDemo e = new PublisherDemo(); /* 实例发布器类*/
            SubscriberDemo v = new SubscriberDemo(); /* 实例订阅器类 */
            e.MyEvent += new PublisherDemo.MyEntrust(v.printf);
            e.SetValue("C语言中文网");
        }
    }
    /***********发布器类***********/
    public class PublisherDemo
    {
        private string value;

        public delegate void MyEntrust(string str);

        public event MyEntrust MyEvent;

        public void SetValue(string s)
        {
            value = s;
            MyEvent(value);     // 触发事件
        }
    }

    /***********订阅器类***********/
    public class SubscriberDemo
    {
        public void printf(string str)
        {
            Console.WriteLine(str);
        }
    }
}

这段代码展示了 C# 中经典的 发布-订阅(Publisher-Subscriber)事件模式。让我为你详细解释:


整体架构

┌─────────────────┐         事件订阅          ┌─────────────────┐
│   发布器类       │  ←─────────────────────  │   订阅器类       │
│  PublisherDemo  │    e.MyEvent += v.printf  │ SubscriberDemo  │
│  (事件的拥有者)  │                         │  (事件的处理者)  │
└─────────────────┘                         └─────────────────┘
        │                                            ▲
        │         触发事件: MyEvent(value)            │
        └────────────────────────────────────────────┘

核心组件解析

1. 委托定义(第 116 行)

public delegate void MyEntrust(string str);
  • 定义事件的处理签名:接收一个 string 参数,无返回值
  • 这是事件机制的基础

2. 事件声明(第 118 行)

public event MyEntrust MyEvent;
  • 关键区别:用 event 关键字修饰委托
  • 事件是受约束的委托,只能由发布器类内部触发

执行流程

1. 创建发布器实例
   PublisherDemo e = new PublisherDemo();
   
2. 创建订阅器实例
   SubscriberDemo v = new SubscriberDemo();
   
3. 订阅事件(绑定方法)
   e.MyEvent += new PublisherDemo.MyEntrust(v.printf);
   
4. 发布器设置值并触发事件
   e.SetValue("C语言中文网");
   
        ↓ 内部执行
   value = "C语言中文网"
   MyEvent(value)  →  调用 v.printf("C语言中文网")
   
5. 订阅器响应
   Console.WriteLine("C语言中文网")
   
        ↓ 输出
   C语言中文网

发布器 vs 订阅器对比

特性 发布器 (PublisherDemo) 订阅器 (SubscriberDemo)
职责 定义事件、触发事件 处理事件、响应通知
权限 可以调用 MyEvent() 只能订阅/取消订阅
代码 event MyEntrust MyEvent public void printf(...)
主动性 主动通知 被动接收

delegate vs event 关键区别

┌─────────────────────────────────────────────────────────────┐
│  如果是普通委托 (delegate):                                  │
│  e.MyEvent = null;      ← 外部可以清空所有订阅! ❌           │
│  e.MyEvent("hello");    ← 外部可以直接触发! ❌               │
├─────────────────────────────────────────────────────────────┤
│  如果是事件 (event):                                         │
│  e.MyEvent += v.printf;  ← 只能添加/移除订阅 ✓               │
│  e.MyEvent = null;       ← 编译错误!保护订阅列表             │
│  只能在 PublisherDemo 内部调用 MyEvent() 触发 ✓              │
└─────────────────────────────────────────────────────────────┘

代码设计亮点

设计 说明
封装性 事件触发逻辑封装在发布器内部
松耦合 发布器和订阅器互不依赖具体实现
可扩展 可以添加多个订阅者(+=
安全性 外部不能随意触发或清空事件

实际应用场景

┌────────────────────────────────────────┐
│           常见应用场景                  │
├────────────────────────────────────────┤
│  按钮点击  →  Button.Click 事件        │
│  数据变更  →  PropertyChanged 事件     │
│  消息通知  →  新消息到达通知订阅者      │
│  进度更新  →  下载进度通知UI更新        │
│  日志记录  →  异常发生时通知多个处理器  │
└────────────────────────────────────────┘

总结

这段代码是 C# 事件机制 的标准模板:

  1. 定义委托 → 约定事件签名
  2. 声明事件 → 用 event 保护委托
  3. 订阅事件 → 用 += 绑定处理方法
  4. 触发事件 → 在发布器内部调用事件

这种模式实现了观察者模式,是构建可扩展、松耦合系统的核心机制!

posted @ 2026-04-22 17:28  安_宁  阅读(25)  评论(0)    收藏  举报