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 → 工具方法
这种设计的用途
| 场景 | 说明 |
|---|---|
| 工具类 | 如 Math、Convert,不需要实例 |
| 全局配置 | 存储应用程序级别的共享数据 |
| 单例模式基础 | 控制实例化方式的一种手段 |
对比:普通类 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"
核心概念:委托的作用
| 特性 | 说明 |
|---|---|
| 方法封装 | 委托可以将方法作为参数传递 |
| 多态性 | 同一个委托变量可以指向不同的方法 |
| 回调机制 | 实现异步编程和事件处理的基础 |
| 松耦合 | 调用者不需要知道具体的方法实现 |
代码的设计亮点
- 静态成员的使用:所有成员都是静态的,无需实例化
Demo类即可使用 - 状态共享:
num作为静态字段,被所有方法共享状态 - 委托的灵活性:通过同一个委托类型,可以调用不同的方法实现
实际应用场景
这种委托模式常用于:
- 事件处理:按钮点击、消息通知等
- 回调函数:异步操作完成后的回调
- 策略模式:运行时动态切换算法
- 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# 事件机制 的标准模板:
- 定义委托 → 约定事件签名
- 声明事件 → 用
event保护委托 - 订阅事件 → 用
+=绑定处理方法 - 触发事件 → 在发布器内部调用事件
这种模式实现了观察者模式,是构建可扩展、松耦合系统的核心机制!

浙公网安备 33010602011771号