C# 编程中的魔法:理解反射、属性、索引器与事件的强大功能

当谈到C#编程语言时,我们往往会立刻想到它的类型系统、面向对象编程的特性,甚至是LINQ和异步编程这些热门话题,但在这背后还有一些功能强大且灵活的特性能够让你的代码更具表现力、可维护性,甚至颠覆你对编程的思维方式。

目录

C#特性

C#反射

C#属性

C#索引器

C#委托

C#事件


C#特性

特性:是应用于程序元素的一种标记,它通过元数据(附加信息)对代码进行注解并允许在运行时或编译时获取这些信息,特性通过继承System.Attribute类来实现,可以使用特性来对类、方法、属性等进行标记,标记后可以在运行时使用反射机制读取并处理这些特性。

在C#中特性是一种用于为程序元素(如类、方法、属性、字段、事件等)添加元数据的机制,通过特性程序员可以在不改变代码行为的情况下,提供额外的信息或指示给运行时或编译器,这些信息可以被用来控制程序的执行或者用于代码分析、调试、测试等方面。.Net框架提供了三种预定义特性:

AttributeUsage;Conditional;Obsolete

预定义特性AttributeUsage描述了如何使用一个自定义特性类,它规定了特性可应用到的项目的类型,规定该特性的语法如下:

cs 复制代码
/*
    参数 validon 规定特性可被放置的语言元素。它是枚举器 AttributeTargets 的值的组合。默认值是 AttributeTargets.All。
    参数 allowmultiple(可选的)为该特性的 AllowMultiple 属性(property)提供一个布尔值。如果为 true,则该特性是多用的。默认值是 false(单用的)。
    参数 inherited(可选的)为该特性的 Inherited 属性(property)提供一个布尔值。如果为 true,则该特性可被派生类继承。默认值是 false(不被继承)。
*/

[AttributeUsage(
   validon,
   AllowMultiple=allowmultiple,
   Inherited=inherited
)]

开发者可以根据需求自定义特性,例如创建一个特性,用于标记需要特定处理的类或方法:

cs 复制代码
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)]
public class MyCustomAttribute : Attribute
{
    public string Description { get; set; }

    public MyCustomAttribute(string description)
    {
        Description = description;
    }
}

常见特性类型:C# 中有许多内置的系统特性常见的包括:

Obsolete:标记已过时的代码元素。

当一个新方法被用在一个类中,但是您仍然想要保持类中的旧方法,您可以通过显示一个应该使用新方法,而不是旧方法的消息,来把它标记为 obsolete(过时的):

cs 复制代码
[Obsolete("This method is deprecated. Use NewMethod instead.")]
public void OldMethod() { }

Serializable:指示一个类可以序列化。

通过Serializable特性标记类告诉.NET Framework该类的实例可以被序列化,类似的特性还有 NonSerialized,它标记不应该序列化的字段。

Conditional:标记了一个条件方法,其执行依赖于指定的预处理标识符。

Conditional特性允许你基于某个条件来决定是否编译某个方法,常用于调试或条件编译:

cs 复制代码
[Conditional("DEBUG")]
public void Log(string message)
{
    Console.WriteLine(message);
}

TestMethod:用于标记单元测试方法(在 MSTest 中使用)。

在单元测试框架中特性通常用来标记测试方法或测试类,比如MSTest、NUnit 和 xUnit都使用特性来标记测试方法:

cs 复制代码
[TestMethod]
public void Test_Addition()
{
    // 测试代码
}

总结:特性在C#中是一种非常强大的机制,通过给代码元素附加元数据可以为开发者提供额外的控制和灵活性,它不仅能增强代码的可维护性还能够通过反射实现动态编程,帮助开发者在不修改现有代码的基础上扩展功能,特性广泛应用于各种场景如序列化、单元测试、编译时检查等,是 C# 高级编程的重要工具之一。

C#反射

反射概念:指程序可以访问、检测和修改它本身状态或行为的一种能力,在C#中反射是一种强大的机制,它允许程序在运行时检查和操作类型的元数据,通过反射可以动态地获取类、方法、属性、字段、事件等的定义信息,甚至可以在运行时创建对象、调用方法、访问字段等。

反射使用:反射通常有以下几种常见的操作方式:

获取类型信息:使用Type类来获取类型的相关信息,该类是所有类型的基础类且提供了大量的方法来获取类型的信息:

cs 复制代码
using System;
class Program
{
    static void Main()
    {
        // 获取类型信息
        Type type = typeof(string);  // 获取string类型的Type对象
        // 输出类型的名称
        Console.WriteLine(type.FullName);  // 输出 "System.String"
        // 获取该类型的所有方法
        var methods = type.GetMethods();
        foreach (var method in methods)
        {
            Console.WriteLine(method.Name);
        }
    }
}

创建对象实例:通过反射可以在运行时创建对象的实例,Activator.CreateInstance是实现这一功能的常用方法。

cs 复制代码
using System;
class Program
{
    static void Main()
    {
        // 创建类型的实例
        Type type = typeof(System.Text.StringBuilder);
        object obj = Activator.CreateInstance(type);  // 创建StringBuilder的实例
        Console.WriteLine(obj.GetType().Name);  // 输出 "StringBuilder"
    }
}

调用方法:可以使用MethodInfo对象来获取方法信息并通过反射调用它们,以下是一个示例

cs 复制代码
using System;
using System.Reflection;
class Program
{
    public void SayHello(string name)
    {
        Console.WriteLine($"Hello, {name}!");
    }
    static void Main()
    {
        // 获取当前类的类型信息
        Type type = typeof(Program);
        // 获取SayHello方法的信息
        MethodInfo method = type.GetMethod("SayHello");
        // 创建对象实例
        object obj = Activator.CreateInstance(type);
        // 使用反射调用SayHello方法
        method.Invoke(obj, new object[] { "Alice" });
    }
}

访问字段和属性:可以使用反射来访问或修改类的字段和属性,包括私有字段。

cs 复制代码
using System;
using System.Reflection;
class Person
{
    private string name;
    public Person(string name)
    {
        this.name = name;
    }
}

class Program
{
    static void Main()
    {
        // 创建对象实例
        Person person = new Person("John");
        // 获取类型信息
        Type type = typeof(Person);
        // 获取私有字段name
        FieldInfo fieldInfo = type.GetField("name", BindingFlags.NonPublic | BindingFlags.Instance);
        // 读取字段值
        string fieldValue = (string)fieldInfo.GetValue(person);
        Console.WriteLine(fieldValue);  // 输出 "John"
        // 修改字段值
        fieldInfo.SetValue(person, "Jane");
        fieldValue = (string)fieldInfo.GetValue(person);
        Console.WriteLine(fieldValue);  // 输出 "Jane"
    }
}

获取特性:通过反射可以获取与类、方法、属性等关联的特性,该特性提供了对程序元数据的注解并且可以在运行时查询和处理。

cs 复制代码
using System;
[Obsolete("This method is obsolete.")]
public class MyClass
{
    public void MyMethod() { }
}
class Program
{
    static void Main()
    {
        Type type = typeof(MyClass);
        // 获取所有特性
        object[] attributes = type.GetCustomAttributes(true);
        foreach (var attribute in attributes)
        {
            Console.WriteLine(attribute.GetType().Name);
        }
    }
}

反射不仅仅限于对类型进行基本操作,它还支持以下高级功能:

1)访问私有成员:

通过使用BindingFlags,反射可以访问类中的私有成员。

2)动态代理和面向切面编程(AOP):

在一些框架中(如PostSharp),反射可用于动态代理和面向切面编程可以在不修改原始代码的情况下向类中插入额外的逻辑(如日志记录、性能分析等)。

3)反射与动态编译:

结合CSharpCodeProvider反射可以用来在运行时动态编译代码并执行,这在某些场景中(例如脚本引擎、插件系统)非常有用。

C#属性

在C#中属性是一种特殊的类成员为字段提供了封装的功能,属性允许通过**访问器(getter)和修改器(setter)**来间接访问字段的值而不直接暴露字段,这有助于对字段的访问进行控制和验证。

简单的属性通常包括两个访问器:get访问器用于获取属性的值、set访问器用于设置属性的值,在C#中属性的定义形式如下:

cs 复制代码
public class Person
{
    private string name;  // 私有字段
    public string Name    // 属性
    {
        get { return name; }      // 获取字段值
        set { name = value; }     // 设置字段值
    }
}

只读属性:如果你只需要一个只读属性可以省略set访问器。

cs 复制代码
public class Person
{
    public string Name { get; }
    public Person(string name)
    {
        Name = name;
    }
}

只写属性:如果你只需要一个只写属性可以省略get访问器。

cs 复制代码
public class Person
{
    private string name;
    public string Name
    {
        set { name = value; }
    }
}

自定义逻辑:可以在get和set访问器中包含自定义的逻辑。

cs 复制代码
public class Person
{
    private string name;
    public string Name
    {
        get { return name; }
        set
        {
            if (string.IsNullOrWhiteSpace(value))
                throw new ArgumentException("Name cannot be empty.");
            name = value;
        }
    }
}

计算属性:属性的值可能需要通过计算或其他逻辑来返回而不仅仅是直接返回一个字段的值,例如可以在get访问器中执行某些计算:

cs 复制代码
public class Circle
{
    private double radius;
    public double Radius
    {
        get { return radius; }
        set { radius = value; }
    }
    // 计算属性
    public double Area
    {
        get { return Math.PI * radius * radius; }  // 计算圆的面积
    }
}

属性与字段的区别

1)封装:属性提供了一种封装机制允许在访问或修改数据时加入验证或额外的处理逻辑,字段则是直接存储数据的地方,访问字段时通常不会进行任何额外的操作。

2)控制访问:通过get和set访问器可以控制对属性的读取和写入,例如可以通过set访问器添加验证逻辑防止不合法的值被设置。

3)灵活性:属性可以在将来进行修改而不破坏外部对类的访问,如果需要添加额外的逻辑或计算只需修改属性的实现,而不需要修改外部调用的代码。

C#索引器

索引器定义:索引器的语法与属性相似但它使用this关键字来表示索引器本身,并且可以接收一个或多个索引参数,与普通的属性不同索引器并不需要指定名称而是用索引来代替,其特性如下所示:

1)无需方法名称:索引器通过索引访问数据不需要显式的方法名称,简化了代码的书写。

2)可以使用多个参数:索引器可以接受多个参数,用于多维数组或更复杂的数据结构。

3)像数组一样访问:定义索引器的类可以通过类似数组的语法来访问数据,这使得类更加灵活和易于使用。

4)灵活数据访问:可以在get和set访问器中添加自定义的逻辑,例如对输入数据进行验证、转换或计算。

在C#中索引器 是一种特殊的成员,它允许对象像数组一样使用索引来访问其元素,通过索引器可以通过索引来访问集合或对象的内部数据而不需要显式定义一个方法来处理数据访问,索引器提供了一种简洁且直观的方式来处理数据集合,其基本语法如下所示:

cs 复制代码
public class MyCollection
{
    private string[] items = new string[10];
    // 定义索引器
    public string this[int index]
    {
        get { return items[index]; }  // 获取元素
        set { items[index] = value; } // 设置元素
    }
}

索引器使用:一旦定义了索引器就可以像使用数组一样使用索引来访问类的对象,例如:

cs 复制代码
class Program
{
    static void Main()
    {
        // 创建 MyCollection 类的实例
        MyCollection collection = new MyCollection();
        // 使用索引器设置元素
        collection[0] = "Item 1";
        collection[1] = "Item 2";
        // 使用索引器获取元素
        Console.WriteLine(collection[0]);  // 输出 "Item 1"
        Console.WriteLine(collection[1]);  // 输出 "Item 2"
    }
}

多参数使用:索引器不仅可以接受单个参数还可以接受多个参数,这对于处理多维数组或需要多个索引的集合非常有用,例如:

cs 复制代码
public class Matrix
{
    private int[,] data = new int[3, 3];
    // 定义一个带两个索引的索引器
    public int this[int row, int col]
    {
        get { return data[row, col]; }  // 获取二维数组中的元素
        set { data[row, col] = value; } // 设置二维数组中的元素
    }
}

只读与只写:与普通的属性一样索引器的get和set访问器也可以是可选的,这意味着你可以创建只读或只写的索引器:

cs 复制代码
// 只读索引器
public class MyCollection
{
    private string[] items = new string[10];
    // 只读索引器
    public string this[int index]
    {
        get { return items[index]; }  // 仅允许读取
    }
}

// 只写索引器
public class MyCollection
{
    private string[] items = new string[10];
    // 只写索引器
    public string this[int index]
    {
        set { items[index] = value; }  // 仅允许写入
    }
}

虽然索引器与数组非常相似,但有几个关键的区别:

1)数组是C#内建的类型具有固定的大小和结构,而索引器可以用于自定义类型并且可以实现不同的访问规则。

2)数组通过[]访问而索引器也通过[]访问,但它是在类内部定义的允许自定义逻辑(例如验证索引或修改数据)。

C#委托

在C#中委托是一种类型安全的函数指针,封装了方法的引用允许将方法作为参数传递也可以动态地调用方法,从而支持事件驱动编程和回调机制。委托是一个引用类型,它定义了一个方法签名,可以用于存储指向该签名的方法,通过委托,你可以调用其他类中的方法,声明委托的语法如下:

cs 复制代码
// public delegate 返回类型 委托名(参数类型 参数名, ...);
public delegate <return type> <delegate-name> <parameter list>

委托使用:定义好委托类型后可以创建委托实例并将其与具体的方法绑定,通过委托可以调用绑定的方法:

cs 复制代码
using System;
class Program
{
    // 定义一个委托类型
    public delegate int MyDelegate(int x, int y);
    // 一个符合委托签名的方法
    public static int Add(int a, int b)
    {
        return a + b;
    }
    // 另一个符合委托签名的方法
    public static int Subtract(int a, int b)
    {
        return a - b;
    }
    static void Main()
    {
        // 创建委托实例并绑定方法
        MyDelegate delAdd = new MyDelegate(Add);
        MyDelegate delSub = new MyDelegate(Subtract);
        // 使用委托调用方法
        Console.WriteLine("Add: " + delAdd(10, 5));       // 输出 15
        Console.WriteLine("Subtract: " + delSub(10, 5));  // 输出 5
    }
}

委托多播:委托可以绑定多个方法并按顺序调用它们,这叫做多播委托,当委托实例被调用时它会顺序地调用所有绑定的方法:

cs 复制代码
using System;
delegate int NumberChanger(int n);
namespace DelegateAppl
{
   class TestDelegate
   {
      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;
      }
      static void Main(string[] args)
      {
         // 创建委托实例
         NumberChanger nc;
         NumberChanger nc1 = new NumberChanger(AddNum);
         NumberChanger nc2 = new NumberChanger(MultNum);
         nc = nc1;
         nc += nc2;
         // 调用多播
         nc(5);
         Console.WriteLine("Value of Num: {0}", getNum());
         Console.ReadKey();
      }
   }
}

移除委托:如果不再需要某个方法,可以通过 -= 运算符将该方法从委托链中移除。

cs 复制代码
public class Program
{
    public delegate void PrintMessage(string message);
    public static void PrintUpperCase(string message)
    {
        Console.WriteLine(message.ToUpper());
    }
    public static void PrintLowerCase(string message)
    {
        Console.WriteLine(message.ToLower());
    }
    public static void Main()
    {
        PrintMessage print = PrintUpperCase;
        print += PrintLowerCase;
        // 移除 PrintLowerCase 方法
        print -= PrintLowerCase;
        // 调用委托(只会调用 PrintUpperCase)
        print("Hello, C#");  // 输出: HELLO, C#
    }
}

委托类型:C#提供了几种常见的委托类型:

Action:代表不返回值的方法,可以接受最多16个参数。

cs 复制代码
Action<string> printMessage = Console.WriteLine;
printMessage("Hello");

Func:代表有返回值的方法,最多接受16个参数,第一个参数是输入参数,最后一个参数是返回值类型。

cs 复制代码
Func<int, int, int> add = (x, y) => x + y;
Console.WriteLine(add(3, 4));  // 输出 7

Predicate:代表返回bool值的方法,通常用于条件判断。

cs 复制代码
Predicate<int> isEven = x => x % 2 == 0;
Console.WriteLine(isEven(4));  // 输出 True

委托是C#中一个非常强大和灵活的特性,可以帮助实现事件驱动的编程、回调机制和函数式编程风格,不仅提供了代码重用的能力还提高了程序的模块化程度,理解和掌握委托的使用对于C#编程是非常重要的。

C#事件

在C#中事件是一种特殊的委托类型主要用于提供一种机制,让对象能够通知其他对象某些状态或行为的改变,事件通常与委托结合使用允许一个对象在发生某些操作时通知其它对象从而实现松耦合的通信。

C#中的事件是通过event关键字来声明的,通常情况下事件类型是一个委托类型,这个委托指定了事件触发时调用的方法签名,事件的定义语法如下所示:

cs 复制代码
// DelegateType 是一个委托类型,定义了事件触发时调用的方法签名。
// EventName 是事件的名称,可以用来触发事件。
public event DelegateType EventName;

下面是一个简单的示例,展示了如何定义和使用事件:

cs 复制代码
using System;
public class Publisher
{
    // 定义一个委托类型
    public delegate void Notify();  
    // 声明一个事件
    public event Notify OnNotify;
    // 触发事件的方法
    public void TriggerEvent()
    {
        Console.WriteLine("事件触发!");
        OnNotify?.Invoke();  // 使用 null 合并运算符,确保事件不为空
    }
}

public class Subscriber
{
    // 事件处理方法
    public void HandleEvent()
    {
        Console.WriteLine("事件已处理!");
    }
}

class Program
{
    static void Main()
    {
        // 创建发布者和订阅者对象
        Publisher publisher = new Publisher();
        Subscriber subscriber = new Subscriber();
        // 订阅事件:将事件与处理方法关联
        publisher.OnNotify += subscriber.HandleEvent;
        // 触发事件
        publisher.TriggerEvent();
    }
}

事件多播委托:C#中的事件是基于多播委托实现的,这意味着一个事件可以关联多个处理方法,当事件触发时所有订阅该事件的方法都会被依次调用:

cs 复制代码
using System;
public class Publisher
{
    // 定义委托类型
    public delegate void Notify();  
    // 声明事件
    public event Notify OnNotify;
    // 触发事件的方法
    public void TriggerEvent()
    {
        Console.WriteLine("事件触发!");
        OnNotify?.Invoke();  // 调用所有订阅的方法
    }
}

public class Subscriber
{
    public void HandleEventA()
    {
        Console.WriteLine("事件处理方法 A");
    }
    public void HandleEventB()
    {
        Console.WriteLine("事件处理方法 B");
    }
}

class Program
{
    static void Main()
    {
        Publisher publisher = new Publisher();
        Subscriber subscriber = new Subscriber();
        // 订阅多个处理方法
        publisher.OnNotify += subscriber.HandleEventA;
        publisher.OnNotify += subscriber.HandleEventB;
        // 触发事件
        publisher.TriggerEvent();
    }
}

总结:C#中的事件是一种基于委托的机制,通常用于发布-订阅模式使得对象之间的通信更加松耦合,通过事件发布者可以通知订阅者某个操作已发生而订阅者则可以响应这些通知并处理事件,事件和委托的结合使得C#在事件驱动编程中非常强大,特别是在GUI编程、异步操作和应用程序中的通知系统中。

相关推荐
窜天遁地大吗喽1 分钟前
abc 383 C (bfs 最短路 )D(唯一分解定理,欧拉筛)
c语言·开发语言·宽度优先
大梦百万秋1 小时前
解析Java中的Stream API:函数式编程与性能优化
开发语言·windows·python
Tinalee-电商API接口呀1 小时前
从零开始构建电商数据采集系统:步骤与策略
java·大数据·开发语言·人工智能·信息可视化·json
NeilNiu1 小时前
类的生命周期
java·开发语言
Rverdoser2 小时前
Spring Boot 配置Kafka
c#·linq
007php0072 小时前
go语言zero框架中config读取不到.env文件问题排查与解决方案
java·开发语言·后端·python·golang·aigc·ai编程
互联网动态分析4 小时前
PHP:构建动态网站的后端基石
开发语言·php
SomeB1oody5 小时前
【Rust自学】3.5. 控制流:if else
开发语言·后端·rust
Q之路7 小时前
C++之多态
开发语言·c++