目录
1.sealed
1.1 核心概念
想象一下你设计了一个非常完美、不希望被别人修改或者扩展的东西。sealed关键字就是用来做这个的,它有两种主要的用法:
1.1.1 封印整个类 (Sealed Class)
-
目的: 禁止其他类从这个类继承。这个类就是“最终形态”,不允许有子类。
-
代码:
sealed class MyFinalClass { public int MyValue; } -
后果: 如果你尝试写
class MyDerivedClass : MyFinalClass { },编译器会直接报错:“你不能从一个被封印(sealed)的类继承!” -
注意: 结构体 (
struct) 天生就是隐式封印的(sealed),它们不能被继承。
1.1.2 封印类中的某个方法或属性
-
前提: 这个方法或属性 必须 是
override的。也就是说,它是在子类中重写了父类的虚方法(virtual)或抽象方法(abstract)。 -
目的: 允许这个类被继承(它本身不是
sealed),但禁止它的子类再次重写这个特定的方法或属性。 -
代码:
class BaseClass { public virtual void MyMethod() { } // 父类定义了一个虚方法 } class SubClass : BaseClass { public sealed override void MyMethod() { } // 子类重写并封印了这个方法 } class GrandChildClass : SubClass { // 尝试再次重写 MyMethod() 会导致编译错误! // public override void MyMethod() { } // 错误!方法已被封印 } -
后果:
GrandChildClass可以继承SubClass,但它不能改变MyMethod的行为。
1.2 作用
-
安全与控制:
-
防止其他人意外或有意地通过继承修改你的核心类或关键方法的行为,导致程序出错。
-
明确设计意图:这个类/方法就是最终版本,不希望被扩展或修改。
-
-
性能(潜在优化): 编译器有时能对密封类或密封方法进行更积极的优化,因为运行时不需要考虑它们被继承或重写的可能性。
注:不能把 abstract和sealed一起用在同一个类上。abstract类就是专门设计来被继承的(要求子类实现抽象成员),而sealed禁止继承,两者矛盾。
2.unsafe
2.1 核心概念
C# 语言设计的一个主要目标是安全。这意味着运行时环境(CLR)会严格检查你的代码,防止你做危险的事情,比如访问不属于你的内存区域(这会导致程序崩溃或安全漏洞)
指针的危险性
指针是直接指向内存地址的变量。操作指针非常强大(比如直接读写内存),但也非常危险:
- 访问无效内存: 指针可能指向一个无效的地址(比如已被释放的内存),操作它会导致程序崩溃。
-
破坏数据: 错误的指针操作可能覆盖掉其他变量或关键数据。
-
安全漏洞: 恶意利用指针可以绕过系统的安全检查。
2.2 作用
因为指针操作如此危险,C# 默认是不允许你使用指针的。unsafe关键字就是用来打破这个安全限制的。
-
声明一个“禁区”: 当你使用
unsafe修饰符(放在方法、类前面)或者用unsafe { }包裹一段代码时,你就是在告诉编译器和运行时:“这段代码区域我要做危险操作(用指针),我知道风险,责任我自负,你别管了。” -
允许指针操作: 必须先有
unsafe上下文。只有在这个标记为unsafe的区域里,你才能声明指针变量(如int* p;)、使用取地址运算符&(如&myVariable)、使用指针解引用运算符*(如*p = 10;)等。
编译器选项: 默认编译器不允许编译包含 unsafe代码的程序。你必须在编译时明确告诉编译器你允许不安全代码(在命令行加 -unsafe选项,或者在 Visual Studio 项目属性里勾选“Allow unsafe code”)。
用途: 高性能计算(直接操作内存避免复制)、调用需要指针的底层系统 API 或 C/C++ 库、特殊的数据结构实现等。普通业务开发极少需要。
2.3 举例
class UnsafeTest
{
// 声明一个不安全的方法,它接收一个指向整数的指针 (int* p)
unsafe static void SquarePtrParam(int* p)
{
*p *= *p; // 在unsafe块内:通过指针p找到它指向的内存位置,取出值,平方,再存回去。
}
unsafe static void Main() // Main方法也标记为unsafe,因为里面用了&
{
int i = 5;
// 在unsafe块内:使用取地址运算符&获取变量i的内存地址,这得到一个指向i所在位置的指针
// 传给SquarePtrParam
SquarePtrParam(&i);
Console.WriteLine(i); // 输出25
}
}
这个例子发生了什么?
-
SquarePtrParam方法接收一个指针p(这个p现在指向Main方法里的i)。 -
*p:解引用指针p。意思是“找到p指向的那个内存位置,访问那里的值”。这里*p就是5。 -
然后将结果(
25)写回p指向的内存位置(也就是i所在的位置)。 -
方法返回后,
Main里的i已经被修改成了25。
3.event
3.1 核心概念
事件就是一种“通知”机制。举例:
| 编程概念 | 现实比喻 |
|---|---|
| 事件源 (Publisher) | 门铃按钮 |
| 事件 (Event) | 按钮被按下的动作 |
| 事件处理程序 | 各种响应行为(喇叭响、手机通知、狗狗叫) |
| 订阅 ( | 把喇叭的线路接到按钮上 / 在手机APP里开启通知 / 狗狗学会听到门铃就叫 |
| 取消订阅 ( | 拔掉喇叭线路 / 关闭手机通知 / 给狗狗戴隔音耳罩 |
| 触发事件 | 有人实际按下了按钮 |
3.2 实现
// 定义事件数据
public class SampleEventArgs
{
public SampleEventArgs(string text) { Text = text; }
public string Text { get; } // readonly
}
public class Publisher
{
// 声明委托(非泛型模式)
// 处理程序必须接受一个 object(发送者) 和一个 SampleEventArgs(事件数据)
public delegate void SampleEventHandler(object sender, SampleEventArgs e);
// 声明事件
public event SampleEventHandler SampleEvent;
// 包装事件的方法,允许派生类触发事件
protected virtual void RaiseSampleEvent()
{
SampleEvent?.Invoke(this, new SampleEventArgs("Hello"));
}
}
class Program
{
static void Main()
{
// 创建发布者
Publisher publisher = new Publisher();
// 订阅事件
publisher.SampleEvent += Publisher_SampleEvent;
// 触发事件(在实际应用中,这通常在 Publisher 内部发生)
// 这里为了演示,我们假设 Publisher 有一个公共方法来触发事件
publisher.RaiseSampleEvent();
}
// 事件处理程序(必须匹配委托签名)
private static void Publisher_SampleEvent(object sender, SampleEventArgs e)
{
Console.WriteLine($"收到事件通知!消息: {e.Text}");
}
}
定义委托 (Delegate - 可选但常见):
-
委托就像一个“方法模板”,规定了事件处理程序必须长什么样(返回类型和参数列表)
-
.NET 提供了常用的委托类型
EventHandler和EventHandler<TEventArgs>,通常推荐使用它们,避免自己定义
声明事件 (Event):
-
在类(发布者)内部使用
event关键字声明 -
语法:
public event 委托类型 事件名;
定义事件数据 (EventArgs - 可选但推荐):
-
这个类用于传递事件发生时需要的数据
-
创建一个类(通常继承自
EventArgs)来封装这些数据。
触发事件:
-
当发生需要通知外界的事情时,发布者类需要“触发”或“引发”事件。通常通过一个受保护的、虚方法(如
RaiseSampleEvent)来触发。 -
在方法内部,使用
事件名?.Invoke(发送者, 事件数据); -
?.是空条件运算符,确保只有至少有一个处理程序订阅了该事件时才触发,避免空引用错误。 -
this通常作为发送者,表示是当前对象触发的。
订阅事件:
-
其他类(订阅者)想要在事件发生时得到通知并执行某些操作。
-
订阅者需要创建一个方法,其签名(参数和返回类型)必须匹配事件所基于的委托类型。这个方法就是事件处理程序。
-
使用
+=操作符将处理程序方法添加到事件的调用列表中。
取消订阅:
-
当订阅者不再需要接收通知时,使用
-=操作符将其处理程序从事件中移除。 -
例子:
publisher.SampleEvent -= MyEventHandlerMethod;
总结:
-
封装性: 事件只能在声明它的类(或派生类)内部被触发 (
Invoke)。外部代码只能订阅 (+=) 或取消订阅 (-=)。这保证了安全性。 -
多播委托: 一个事件可以有多个订阅者(多个处理程序)。当事件触发时,所有订阅的处理程序都会按添加顺序被调用。
-
基于委托: 事件在底层使用委托机制来存储和调用处理程序列表。
-
访问修饰符: 事件可以像字段和方法一样使用
public,private,protected等来控制谁可以订阅它。 -
特殊关键字:
static: 静态事件,不需要创建类的实例就能订阅/触发。public class AppEvents { // 静态事件:应用退出时触发 public static event EventHandler ApplicationExiting; public static void Exit() { ApplicationExiting?.Invoke(null, EventArgs.Empty); Environment.Exit(0); } } // 使用(不需要创建实例) AppEvents.ApplicationExiting += (s, e) => { Console.WriteLine("正在保存数据并退出..."); }; AppEvents.Exit();
补充:
-
传统方式:
// 先定义具体的方法 private static void OnApplicationExiting(object sender, EventArgs e) { Console.WriteLine("正在保存数据并退出..."); } // 然后订阅事件 AppEvents.ApplicationExiting += OnApplicationExiting; -
使用 Lambda 表达式(推荐):
AppEvents.ApplicationExiting += (s, e) => { Console.WriteLine("正在保存数据并退出..."); };
学到了这里,咱俩真棒,记得按时吃饭(吃饱饱~没烦恼~)
【本篇结束,新的知识会不定时补充】
感谢你的阅读!如果内容有帮助,欢迎 点赞❤️ + 收藏⭐ + 关注 支持! 😊
C#修饰符详解:sealed、unsafe与event
393

被折叠的 条评论
为什么被折叠?



