C# 高级内容
C# 高级
特性 (Attribute)
可以认为是更高级的注释。
用于添加元数据等。
用于在运行时传递程序中各种元素的行为信息的声明性标签。
可以通过使用特性来向程序添加声明性信息。
一个声明性标签是通过放置在所在对应元素前的方括号描述的。
.Net 提供了两类特性: 预定义特性和自定义特性。
[attribute(位置参数,名称参数 = 值, ...)] element
特性名称和值是在方括号内定义的。
放置在所应用元素前。
位置参数规定必须信息,而名称参数则是可选的。
预定义特性
.Net 提供了三种预定义特性:
AttributeUsage
描述了如何使用一个自定义特性类。
规定了特性可应用到的项目类型。
[AttributeUsage ( // 规定可被放置的语言元素 // 是枚举 AttributeTargets 值组合 // 默认是 AttributeTargets.All validon, // 可选的为特性的 AllowMultiple 属性提供一个布尔值 // 为 true 则是多用的,默认是 false 单用的 AllowMultiple=allowmultiple, // 可选的为特性的 Inherited 提供一个布尔值 // 为 true 可被派生类继承,默认 false 不被继承 Inherited=inherited )] [AttributeUsage( AttributeTargets.Class | AttributeTargets.Constructor | AttributeTargets.Field | AttributeTargets.Method | AttributeTargets.Property, AllowMultiple = true )]
Conditional
标记了一个条件方法。
执行依赖于指定的预处理标识符。
会引起方法调用的条件编译,取决于指定值,比如
Debug
或Trace
。[Conditional( conditionalSymbol // 条件标识符 )] // ... #define DEBUG #define DEV // ... [Conditional("DEBUG")] public static void SendMsg (string msg) { // ... } // ... static void Main () { // ... // 如果没有定义标识符 DEBUG // 则不会执行 SendMsg("msg"); } // ...
Obsolete
标记一个不应被使用的程序实体。
可以通知编译器丢弃某个特定目标元素。
[Obsolete (
message, // 描述为什么应该被舍弃且替代使用什么
iserror // 值为 true 则编译时作为错误
)]
自定义特性
.Net 允许创建自定义特性。
共有四个步骤:
- 声明自定义特性。
- 构建自定义特性。
- 应用自定义特性。
- 访问特性。
// 一个新的自定义特性应该派生自 System.Attribute 类
[AttributeUsage(
AttributeTargets.Class |
AttributeTargets.Constructor |
AttributeTargets.Field |
AttributeTargets.Method |
AttributeTargets.Property,
AllowMultiple = true
)]
// 声明了一个名叫 DevInfo 的特性
public class DevInfo: System.Attribute {
// 构建特性
// 此特性将保存程序开发的信息
// 存储有 开发日期 开发者 审查者
private DateTime now;
private string author;
private string review;
// 每个特性都要有一个构造函数
// 必须的定位参数通过构造参数传递
public DevInfo (DateTime now, string author, string review) {
this.now = now;
this.author = author;
this.review = review;
}
public DateTime now {
get { return now; }
}
public string author {
get { return author; }
}
public string review {
get { return review; }
}
public string message {
get { message; }
set { message = value; }
}
}
// 应用特性
// 需要在紧接着目标前应用特性
[DevInfo(new DateTime(), "张三", "李四")]
class Example {
// ...
}
// 访问特性
Example e = new();
Type t = typeof(Example);
foreach(object attributes in t.GetCustomAttributes(false)) {
devInfo info = attributes as devInfo;
}
反射 (Reflection)
指程序可以访问,检测,修改它本身状态或行为的能力。
程序集包含模块,模块包含类型,类型包含成员。
反射提供了封装程序集,模块和类型的对象。
使得可以使用反射动态的创建类型的实例,将类型绑定到现有对象,或从现有对象中获取类型。
然后可以调用类型的方法或访问其字段或属性。
优点
- 提高了程序的灵活性和扩展性。
- 降低耦合度,提高自适应能力。
- 允许程序创建和控制任何类对象,无需提前硬编码目标。
缺点
- 性能问题,反射基本上是一个解释操作,用于字段和方法接入时要远慢于直接代码。因此主要应用于对于灵活性,扩展性要求极高的框架上。普通程序不推荐。
- 会模糊内部逻辑,因为反射绕过了源代码,因此会被直接代码更复杂。
用途
- 允许在运行时查看特性信息。
- 允许审查和实例化集合中的各类型。
- 允许延迟绑定方法和属性。
- 允许在运行时创建新类型,然后通过这些类型执行任务。
// 使用反射,可以查看特性信息
// System.Reflection 类的 MemberInfo 对象需要被初始化
// 用于发现与类相关的特性
System.Reflection.MemberInfo info = type(Example);
// attrs 就是自定义特性的集合列表
object[] attrs = info.GetCustomAttributes(true);
属性 (Property)
是类,结构,接口的命名成员。
类或结构中的成员变量或方法称为 域 (Field),属性则是域的扩展。
且可以使用相同语法访问。
它们使用访问器(accessors)使得私有域的值可被读写或操作。
访问器 (Accessors)
包含有助于获取或设置属性的可执行语句。
public int Count {
get {
// 执行获取操作时
// 比如对 int 属性进行每次获取时增加
return Count++;
}
set {
// 执行设置操作 即 赋值等改变属性的操作
// 比如拦截操作 对属性值进行更改
Count = 0;
// Count = value;
}
}
抽象属性 (Abstract Properties)
抽象类可以具有抽象属性,会在派生类中实现。
public abstract class AbstractExample {
public abstract string Name {
get;
set;
}
// ...
}
class Example: AbstractExample {
// 如果没有额外的操作 那么 可以直接使用 Name { get; set; }
public override string Name {
get {
return Name;
}
set {
Name = value;
}
}
}
索引 (Indexer)
允许一个对象可以像数组一样使用下标的方式访问。
当为类定义一个索引器时,该类行为会像一个虚拟数组 (virtual array) 一般,可以使用数组访问运算符 [] 访问该类成员。
element-type this[int index] {
get {
// 访问 index 对应值时
// ...
}
set {
// 修改 index 对应值时
// ...
}
}
用途
虽然与属性类似,但是属性返回和设置的是特定的成员,而索引则是返回和设置对象的特定值。
即,索引将实例分为了更小的部分,并索引每个部分进行获取或修改。
定义一个属性时需要提供属性名称,但索引器不需要,它需要的是 this 关键字以指向对象实例。
class IndexerExample {
private string[] list = new string[size];
static public int size = 10;
public string this[int index] {
get {
string val = "";
return ( val );
}
set {
if (index >= 0 && index <= size - 1) {
list[index] = value;
}
}
}
}
重载索引器
索引器可以被重载。
声明的时候也可以带多个不同类型的参数。
class IndexerExample {
private string[] list = new string[size];
static public int size = 10;
public string this[int index] {
get {
string val = "";
return ( val );
}
set {
if (index >= 0 && index <= size - 1) {
list[index] = value;
}
}
}
public int this[string attr] {
get {
int index = 0;
// ...
return index;
}
}
}
委托 (Delegate)
类似 C/C++ 函数的指针,是存有对某个方法的引用的一种引用类型变量。
可在运行时改变引用。
特别用于实现事件和回调方法。
所有的委托都派生自 System.Delegate
。
// 声明委托
// 决定了可由该委托引用的方法
// 可执行一个与其有相同标签的方法
// 此委托可用于引用任何一个单一 string 参数 且 返回 int 的方法
public delegate int DelegateExample (string str);
// 实例化
// 委托对象必须使用 new 创建 且 要与特定方法有关
// 创建时,传递到 new 语句的参数就像方法调用一样
// 但是不需要参数
public int Fn (string str) {}
DelegateExample DE1 = new DelegateExample(Fn);
委托的多播 (Multicasting of a Delegate)
相同类型的委托对象可以使用 + 运算符合并,使用 - 运算符移除。
一个合并委托可以调用合并的委托。
通过多播,可以创建一个委托被调用时的方法的调用列表,这被称为多播 (multicasting) ,也叫组播。
// ...
delegate int NumberChanger(int n);
// ...
public int addNum(int n) {}
public int removeNum(int n) {}
// ...
NumberChanger nc;
NumberChanger nc1 = new NumberChanger(addNum);
NumberChanger nc2 = new NumberChanger(removeNum);
nc = nc1;
nc += nc2;
// 调用多播
nc(5);
匿名方法 (Anonymous methods)
提供了一种传递代码作为委托参数的技术,匿名方法是没有名称,只有方法体的方法。
delegate void NumberChanger(int n);
// 不需要指定返回类型 这是自动推导的
NumberChanger nc = delegate(int x) {
// ...
}; // 匿名方法主体后需要跟随 分号
事件 (Event)
即一个用户操作,比如按键,点击等,或是一些提示信息,比如系统通知。
应用会在事件发生时响应它,并执行操作。
C# 中使用事件机制实现线程间通信。
而事件使用的是 发布-订阅 (publisher - subscriber) 模型。
使用委托
事件在类中声明生成,且通过类或其他类中的委托处理程序关联。
包含事件的类用于发布事件,被称为发布器 (publisher) 类。
其他接受该事件的类则称为订阅器 (subscriber)。
发布器
是一个包含事件和委托定义的对象,事件和委托之间的关联也定义在此。发布器会调用这个事件并通知其他对象。
订阅器
是一个接受事件并提供事件处理程序的对象,在发布器中的委托会调用订阅器的方法。
声明事件
在类的内部声明事件,首先需要声明事件的委托类型。
接着才能声明事件本身。
public delegate void EventHandler(string status);
// 声明事件使用 event 关键字
// 声明一个 EventHandler 的委托和名为 EventLog 的事件
// 事件生成时会调用委托
public event EventHandler EventLog;
// 调用事件
// 但是在此之前 需要先对事件的委托传入方法
EventLog += new EventHandler(Fn);
EventLog ("status"); // 会触发 EventHandler 委托 然后执行 Fn 方法
不安全的代码
C# 默认不允许使用指针变量。
使用了指针变量的代码即是不安全的代码。
使用 unsafe
关键字,可以标识允许使用指针变量。
指针变量
是值为另一个变量的地址的变量,即指针的值就是内存位置的直接地址。
在使用前,必须先声明指针。
type* variablename;
int* p; // 指向整数的指针
float* p; // 指向浮点的指针
int** p; // 指向整数的指针的指针
// 同一声明或多个声明时,仅使用一个 *
int* p1, p2, p3;
static unsafe void Main () {
int n = 20;
int* p = &n;
}
// 也可是仅声明一段代码为不安全代码
static void Main () {
unsafe {
int n = 20;
int* p = &n;
}
}
传递指针参数
可以使用指针作为方法的参数。
// * 号不论是放置在左侧还是右侧都可 这取决于代码风格
public unsafe void swap (int* p, int *q) {
int tmp = *p;
*p = *q;
*q = tmp;
}
public static void Main () {
int n1 = 10;
int n2 = 20;
unsafe {
int* p1 = &n1;
int* p2 = &n2;
swap(p1, p2);
}
}
访问数组
C#中,数组名称 和 一个指向与数组类型相同的指针 是不同的变量。
int* p; // 可以增加 此指针变量 因为它在内存在不固定
int[] p; // 不能增加 因为数组地址是在内存中固定的
c# 声明的变量在内存中由 GC (垃圾回收管理器) 管理。
因此一个变量有可能在运行中移动到其他位置。这样的话,指针就没有意义了。
所以使用 fixed
关键字固定变量位置不动。
public unsafe static void Main () {
int[] list = { 10, 20, 30 };
fixed(int *ptr = list);
for (int i = 0; i < 3; i++) {
int address = (int)(ptr + i);
int val = *(ptr + i);
}
// 通过 stackalloc 关键字可以在堆栈上分配内存
// 分配的内存不受到 GC 管理,因而不需要固定
int *ptr = stackalloc int[1];
}
编译
默认情况下,不允许编译不安全的代码。
需要使用 /unsafe
指定允许开启编译不安全代码。
多线程
线程被定义为程序的执行路径。
每个线程都定义了一个独特的控制流。
如果程序涉及复杂耗时操作,那么设置多个不同的线程执行不同的特定任务是有益的。
线程是轻量级线程。常见应用于并行编程的实现。节省了 CPU 周期的浪费,且提高了程序效率。
线程生命周期
线程生命周期开始于 System.Threading.Thread
实例创建。
结束于线程被终止或完成执行时。
未启动状态:
线程实例创建但为调用 Start 方法。
就绪状态:
线程准备运行并等待 CPU 周期。
不可运行状态:
已调用 Sleep 方法。
已调用 Wait 方法。
IO 操作导致阻塞。
死亡状态:
线程完成执行或已终止。
主线程
System.Threading.Thread
用于线程工作,允许创建并访问多线程程序中的单个线程。
而进程中第一个被执行的线程被称为 主线程。
主线程是自动创建的。