【C#】反射、特性和动态编程 --《C#本质论》

一、反射

反射是指对程序集重的元数据进行检查的过程。当C#被编译成CIL时,它会维持关于代码的大部分元数据(以前代码被编译成机器语言时,会丢弃元数据)。

利用反射可以枚举出程序集中的所有类型,找出满足特定条件的那些。

1、反射可以做的事

1.1 访问程序集中的元数据

读取类型的元数据,关键在于获得System.Type的一个实例,它代表目标类型实例。System.Type提供了获取类型信息的所有方法。获得对类型的Type的引用主要通过object.GetType() 或typeof()

        //object.GetType():获取一个对象实例
        //静态类无法使用,因为静态类无法实例化
        DateTime dateTime = new DateTime();
        Type type = dateTime.GetType();
        foreach (var item in type.GetProperties())
        {
            Console.WriteLine(item.Name);
        }

        //Enum.Parse():获取标识了一个枚举的type对象,将一个字符串转换成特定的枚举值
        ThreadPriorityLevel level = (ThreadPriorityLevel)Enum.Parse(typeof(ThreadPriorityLevel), "Idle");

1.2 使用元数据在运行时动态调用类型的成员

demo没怎么看懂,先放着

2、泛型类型上的反射

CLR2.0新增了一些列方法来判断给定类型是否支持泛型参数和泛型实参:

Type type;
type = typeof(Nullable<>);
Console.WriteLine(type.IsGenericType); //指示类型是否为泛型
Console.WriteLine(type.ContainsGenericParameters);  //指示类型是否包含泛型实参

type = typeof(Nullable);
Console.WriteLine(type.ContainsGenericParameters);
Console.WriteLine(type.IsGenericType);

从泛型获取泛型实参: 

        Stack stack = new Stack();
        Type type=stack.GetType();

        //调用GetGenericArguments()可获得一个由System.Type实例构成的一个数组,实例顺序是它们被声明的顺序
        foreach (var item in type.GetGenericArguments())
        {
            Console.Write(item.Name+"  "+ item.FullName);
        }

 输出:Int32  System.Int32 

3、nameof操作符

nameof操作符生成一个常量字符串来包含被指定为实参的任何程序元素的非限定名称。由于这个行动是在编译时发生,所以nameof技术上说不是反射。但它接收的是有关程序集及其结构的数据

使用nameof而不是直接写字符串有两方面好处:

  • C#编译器确保nameof操作符的实参是有效程序元素,这样,如果程序元素名称发生变化,或者出现拼写错误,编译时就能知道出错
  • 相较于字符字面值,IDE工具使用nameof操作符能工作的更好,例如“查找所有引用",工具能找到nameof表达式中提到的程序元素,但字符串字面值就不行。"重命名"也是。
public class Person:INotifyPropertyChanged
{
    public event PropertyChangedEventHandler? PropertyChanged;

    private string name = "";

    public string Name 
    { 
        get { return name; }
        set
        {
            if(value!=name)
            {
                name = value;
                PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Name));
            }
        }
    }
}

二、特性

特性是将额外数据关联到属性(以及其他构造)的一种方式。

除了修饰属性,特性还可修饰类、接口、结构、枚举、委托、事件、方法、构造方法、字段、参数、返回值、程序集、类型参数和模块。

程序集的特性用于添加有关程序集的额外元数据。

[assembly: AssemblyTitle("Meter")]
[assembly: AssemblyDescription("Copyright © 2016, All Rights Reserved.")]
[assembly: AssemblyConfiguration("")] //为程序集指定生成配置 debug或release
[assembly: AssemblyCompany("Shenzhen iTest Technology Co., LTD.")]
[assembly: AssemblyProduct("Meter")]
[assembly: AssemblyCopyright("Copyright © iTest 2016")] //版权
[assembly: AssemblyTrademark("Meter")] //商标
[assembly: AssemblyCulture("")] //区域性
[assembly: AssemblyVersion("3.0.0.61")]
[assembly: AssemblyFileVersion("3.0.0.61")]

自定义特性需继承Attribute

查找特性需要给定一个PropertyInfo对象(通过反射来获取),调用GetCustomAttributes(),返回一个object数组,该数组可成功转换为Attribute数组。

为构造应用特性时,通常只有常量值和typeof表达式才允许被作为实参。

如特性有必须的属性值,要提供只能取值的属性(赋值设为私有)

1、System.AttributeUsageAttribute

System.AttributeUsageAttribute可以限制特性能修饰哪些构造,只要特性被不恰当使用,就会导致编译错误

[AttributeUsage(AttributeTargets.Property|AttributeTargets.Field, AllowMultiple = false)]
internal class SwitchCommandAttribute: Attribute
{
}

 AttributeTargets提供了“运行时”允许特性修饰的所有目标的列表

【C#】反射、特性和动态编程 --《C#本质论》_第1张图片

2、具名参数

AttributeUsageAttribute中的AllowMultiple 时具名参数。具名参数在特性构造函数调用中设置特定的公共属性和字段——即使构造函数不包括对应参数。

对具名参数的赋值只能放在构造函数的最后一部分进行,任何显示声明的构造函数参数都必须在它之前完成赋值。

有了具名参数后,就可以直接对特性的数据进行赋值,不必为特性属性的每一种组合都提供对应的构造函数。

3、预定义特性

预定义特性会影响编译器的行为,有时会导致编译器报错。且,预定义特性没有运行时代码,而是由编译器内建了对它的支持

3.1、System.ConditionalAttribute

指示编译器应忽略方法调用或属性,除非定义了指定的条件编译符号。可应用与类和无返回值且不包含out参数的方法。

被ConditionalAttribute修饰的构造仍会进行编译并包含在目标程序集中,具体是否调用取决于调用者所在程序集中的预处理器标识符。

ConditionalAttribute修饰自定义特性时,只有在调用程序集中定义了条件字符串,才能通过反射来获取该自定义特性。

#define CONDITION_A

using System;
using System.Diagnostics;
using System.Reflection;

public class Program
{
    public static void Main(string[] args)
    {
        Console.WriteLine("Begin...");
        MethodA();
        MethodB();
        Console.WriteLine("End...");
    }

    [Conditional("CONDITION_A")]
    private static void MethodA()
    {
        Console.WriteLine("MethodA executing...");
    }

    [Conditional("CONDITION_B")]
    private static void MethodB()
    {
        Console.WriteLine("MethodB executing...");
    }
}

Begin...
MethodA executing...
End...

3.2、System.ObsoleteAttribute

用于编制代码版本,向调用者指出一个特定的成员或类型已过时。调用被ObsoleteAttribute修饰的成员,会使编译器显示警告(也可选择报错)

ObsoleteAttribute特性还提供另外两个构造函数,一个是ObsoleteAttribute(string? message),能在编译器生成的报告过时的消息上附加额外的信息,譬如告诉用户用什么来替代已过时的代码。另一个是ObsoleteAttribute(string? message, bool error),bool参数指定是否强制将警告视为错误。

【C#】反射、特性和动态编程 --《C#本质论》_第2张图片

 3.3、与序列化相关的特性

(4条消息) 【C#】序列化_阿月浑子2021的博客-CSDN博客

版本控制(使用BinaryFormatter进行序列化)

文档这样的对象可能用程序集的一个版本序列化,以后用另一个版本反序列化。但哪怕只是添加一个新字段,对原始文档进行反序列化的时候都会抛出异常,容易造成版本不兼容问题。(上面那个XmlSerializer 序列化的例子我试了一下,增减字段后读原始文件并没有报错)

为避免这个问题,.Net Framework 2.0及其后续版本添加了一个System.Runtime.Serialization.OptionalFieldAttribute。如需向后兼容,就必须使用OptionalFieldAttribute来修饰序列化的字段——包括私有字段。如果以后的某个版本规定该字段是必须而非可选,就不必添加OptionalFieldAttribute修饰。

三、使用动态对象进行编程

C#4.0动态对象的初始实现中,提供了一下4种绑定方式:

  • 针对底层CLR类型使用反射
  • 调用自定义IDynamicMetaObjectProvider,使一个DynamicMetaObjectProvider变得可用
  • 通过COM的IUnknown和IDispatch接口来调用
  • 调用由动态语言(比如IronPython)定义的类型

(以上四句,不明觉历)

1、使用dynamic调用反射

反射的关键功能之一就是动态查找和调用特定类型的成员,这要求在执行时识别成员名或其他特征,比如特性。

C#4.0新增的动态对象提供了更简单的方法来通过反射调用成员,该技术的限制在于,编译时要知道成员名和签名(参数个数,以及指定的参数是否和签名类型兼容)

        dynamic data = "Hello!";
        Console.WriteLine(data);
        
        data=(double)data.Length;
        data *= 3.5;
        Console.WriteLine(data);

Hello!
21 

将data声明为dynamic类型,并直接在它上面调用方法,编译时不会检查指定成员是否可用,甚至不会检查dynamic对象的基础类型是什么。 

2、dynamic的原则和行为

dynamic类型实际上是一个object,事实上,如果没有任何调用,dynamic类型的声明和object没有区别。但一旦调用它的成员,区别显露出来了。

为调用成员,编译器要声明System.Runtime.CompilerServices.CallSite类型的一个变量。

另外还会动态定义一个方法,该方法需要传递CallSite site,object dynamcTarget 和string reuslt三个参数。其中,site是调用点本身,dynamcTarget 是要在上面调用方法的object,result是ToString()方法调用的基础类型的返回值。

得到CallSIte实例后调用CallSite.Target()来调用实际的成员。

dynamic 的一些特征:

  1. dynamic是通知编译器生成代码的指令。将类型指定为dynamic,相当于从概念上“包装”了原始类型,这样便不会发生编译时验证。在运行时调用一个成员时,“包装器”会解释调用,并相应调度(或拒绝)它。
  2. 任何能转换成object的类型都能转换成dynamic
  3. 从dynamic到一个替代类型的成功转换要依赖基础类型的支持
  4. dynamic类型的基础类型在每次赋值时都可能改变。dynamic涉及一个解释机制,要先编译再执行基础类型的代码。所以,可将基础类型实例更换为一个完全不同的类型,这会造成另一个解释调用位置,需要再调用前编译它。
  5. 验证基础类型上是否存在指定签名要推迟到运行时才进行。
  6. 任何dynamic成员调用都返回dynamic对象,但在dynamic对象上调用GetType()可揭示出dynamic实例的基础类型。
  7. 用dynamic实现的反射不支持扩展方法。

你可能感兴趣的:(C#,c#,开发语言)