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

    标记了一个条件方法。

    执行依赖于指定的预处理标识符。

    会引起方法调用的条件编译,取决于指定值,比如 DebugTrace

    [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 用于线程工作,允许创建并访问多线程程序中的单个线程。

而进程中第一个被执行的线程被称为 主线程。

主线程是自动创建的。


C# 高级内容
http://localhost:8080/archives/2c191476-59dd-4413-891a-134fa54b1286
作者
inksha
发布于
2024年09月14日
许可协议