大多数人,都低估了编程学习的难度,而高估了自己的学习能力和毅力。
当前系列: C#语法 修改讲义
复习:J&C:反射 / 特性(注释)/ 单元测试工具


反射(Reflection)

所有反射相关的类,都在名称空间:

using System.Reflection;

Type类

要得到这个变量的类型信息:

Type typeInfo = zjq.GetType();
当然也可以使用typeof:
Type typeInfo = typeof(Student);

#常见面试题:两者的区别?#

  • typeof是静态的、编译时确定的
  • GetType()是动态的、运行时确定的
    Person atai = new Student();
    if (Console.ReadLine() == "P")
    {
        atai = new Person();
    }//else nothing
    Type type =  atai.GetType();
    Console.WriteLine(type.Name);

还可以通过类(的全)名获得:

Type type1 = Type.GetType("CSharp.Student");

而且,同一个类型,无论通过何种方式获得的Type对象,其实都是同一个对象:

Type type2 = new Student().GetType();
Console.WriteLine(type1 == type2);

使用这个Type对象,可以获取

类成员信息

构造函数

Type.EmptyTypes为空数组,表示取无参构造函数:

object student = 
    typeInfo.GetConstructor(Type.EmptyTypes)
    .Invoke(null);
使用invoke()调用方法:构造函数也算是方法。

属性

通过属性名直接获取:

typeInfo.GetProperty("Score").SetValue(student, 98.5);

第一个参数指定要赋值的对象,第二个参数是给属性的值。

即使set为private也行:(理解访问修饰符“防君子不防小人”)
public double Score { get; private set; }
但如果属性本身是private的,就狗带了:
private double Score { get; set; }

这时候需要BindingFlags枚举,注意位运算符:|

typeInfo.GetProperty("Score", 
    BindingFlags.NonPublic | BindingFlags.Instance)
    .SetValue(student, 98.5);

方法

typeInfo.GetMethod("Learn").Invoke(student, null);

invoke()的第二个参数代表方法参数

其他字段等:略

继承和多态

继承和多态,在反射中仍然起作用。

class Student : Person
{
    public override void Eat()
    {
        Console.WriteLine("student eat...");
    }
}
class Person
{
    public string Name { get; set; }

    public virtual void Eat()
    {
        Console.WriteLine("peason eat...");
    }
}
首先,子类类型信息对象,就直接继承(包含)着她父类成员,可以由子类对象调用其父类成员:
//typeInfo和student都由Student产生
typeInfo.GetProperty("Name").SetValue(student, "atai");

但如果父类成员是protected的

protected string Name { get; set; }
,需要声明BindFlags:
typeInfo.GetProperty("Name", BindingFlags.NonPublic | BindingFlags.Instance)
    .SetValue(student, "atai");

注意:如果父类成员是private的,子类对象就完全拿不到!(区分自己的private和父类的private)

子类覆盖(override)了父类方法,调用规则和多态一致:随对象。

nameof()

为了避免字符串的拼写错误,(较高版本)C#引入了nameof(),传入一个类成员,就能够得到它的字符串格式的名字。这样上面的代码就可以写成:

typeInfo.GetProperty(nameof(Name))
注意,nameof()
  1. 是编译时完成的,所以不用担心性能问题
  2. 获取的只是“完全限定的名称”(非全名)



.NET程序集

一个项目就会被编译成一个程序集(Assembly),所以能猜到这啥意思不:

object atai = typeof(Student).Assembly.CreateInstance("CSharp.Student");

程序集是.NET代码运行的基本单元,表现形式为可执行文件(.exe或.dll)。演示:在bin中查看和运行。

ILDASM

二进制的可执行文件,需要用特定的反编译工具才能够阅读。复习

ILDASM在Visual Studio安装时默认自动安装,可以启用:

Tool - Command Line - Developer Command Prompt

然后通过运行ildasm命令自动打开

然后通过:文件 - 打开 一个编译过后的.dll文件,就能看到如下界面:

这就是一个程序集被反编译(不是反射)之后的内容,包括:

  • Manifest(清单):列明了该程序集的基本信息(比如版本号)和应包含的内容(比如依赖项、文件资源等)
  • IL:中间语言格式的代码。这里被整理成了类(.class)、构造函数(.ctor)和方法(Main),双击Main方法,我们还可以看到如下代码:
    .method private hidebysig static void  Main(string[] args) cil managed
    {
      .entrypoint
      // 代码大小       13 (0xd)
      .maxstack  8
      IL_0000:  nop
      IL_0001:  ldstr      "Hello World!"
      IL_0006:  call       void [System.Console]System.Console::WriteLine(string)
      IL_000b:  nop
      IL_000c:  ret
    } // end of method Program::Main

这是一种和汇编语言比较类似的语言,nop/ldstr/call……这些都是非常基础非常底层的指令。同学们打个照面认识一下即可,不用深究。

metadata(元数据)

.NET的程序集是自描述的。

大家注意到了没有,我们在F12转到.NET类库成员的定义时,只能看到方法的签名,看不到方法的实现,而且tab上还有一个标志from metadata:

所以这时候你看到的不是源代码,而是引用的.dll文件中的元数据。比如在这个文件的头部已经写明了.dll文件的路径:

#region Assembly System.Runtime, Version=4.2.1.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50 // C:\Program Files\dotnet\sdk\NuGetFallbackFolder\microsoft.netcore.app\2.1.0\ref\netcoreapp2.1\System.Runtime.dl #endregion

所以,这是.NET一个非常有趣的地方:当它把源代码编译成.dll文件的时候,它还同时生成了程序集的二进制描述性信息:元数据(metadata),并直接存放在.dll文件中。程序集的所定义和引用的有成员信息都包括在内,包括程序集信息、类名、类成员和类的继承实现等等。

但是注意,metadata中没有方法的具体实现。换言之,metadata只记录声明,实现还要靠IL。

源代码

无论是ILDASM,还是metadata,都不能查看/反编译出C#语言的源代码。

ILSpy

可以将IL再反编译成C#代码,在.NET闭源时一度非常流行。

可直接通过VS的Extensions Manage在其Marketplace下载。(略)

Reference Source

微软的一个可快捷查询.NET source code的网站。

Source Link

可以在调试时选择从微软官方/github服务器下载响应的源代码,参与调试,非常方便!

(在VS2022中)需要做的也非常简单,在Tools - Options中设置:

  • Debugging中勾选:Enable Source Link support

  • Debugging - General中取消勾选:Enable Just My Code

  • Debugging - Symbols中勾选:

这样,击中断点之后,按F11,就会弹出提示:


特性(Attribute)

Java中称之为注解(annotation)

自定义

使用特性之前首先要定义/声明,实际上它是一个继承自Attribute的类。

这个类可以有构造函数和(简单类型的)属性,比如:

    class OnlineAttribute : Attribute
    {
        //可以无参也可以有参
        public OnlineAttribute(){}
        public OnlineAttribute(int version)
        {
            Version = version;
        }

        //还可以使用属性
        public int Version { get; set; }
    }

然后,就可以使用这个Attribute了。声明特性(类)的时候,我们通常以Attribute为后缀,但在使用的时候可以省略。

比如,可以像方法一样加圆括号,里面接受构造函数参数、给属性赋值等

    ///用于类,获取时将: 
    ///1. 调用OnlineAttribute的无参构造函数
    ///2. 给OnlineAttribute对象的Version属性赋值
    [Online(Version = 3)]
    internal class Student
    {
        [Online(2)]  //用于方法,获取时将调用OnlineAttribute的有参构造函数
        internal void learn(){ }

        //[Online]  //不能用于属性
        public string Name { get; set; }
    }
在.NET运行时可以检查/获取目标元素上有无特性,是什么特性:
            Attribute attribute = OnlineAttribute.GetCustomAttribute(
                typeof(Student),             //Student类上的
                typeof(OnlineAttribute)      //OnlineAttribute特性
            );
            //将基类的Attribute对象强转为子类
            Console.WriteLine(((OnlineAttribute)attribute).Version);

注意:除非(使用反射)显式的获取,特性不会被实例化。

拿到这些信息又能干嘛呢?想干嘛就干嘛,^_^

实际上,使用Attribute就好像给类(不是对象)打了一个“标签”。 我们可以在程序运行时通过反射来查找有特定标签的类,读取标签的内容给予区别对待。

Flags

我们可以对比一下有Flags标记和没有Flags标记的区别:

  [Flags]
    enum Role
    {
        Student = 1,
        TeacherAssist = 2,
        TeamLeader = 4,
        DormitoryHead = 8
    }

    Console.WriteLine(Role.Student | Role.TeacherAssist);
    //没有Flag,输出:3
    //有Flag,  输出:Student, TeacherAssist
实际上这就是因为Enum重写了Object的ToString()方法,在其中判断枚举上是否标记了FlagsAttribute:
if (!eT.IsDefined(typeof(System.FlagsAttribute), false)) // Not marked with Flags

注意:Flags仅在ToString()时输出“更有意义”的字符串,不保证枚举值都是2的整数次方。如果枚举值不是2的整数次方,会出现一些“预期以外”的结果。

AttributeUsage

Flags本质上是一个继承了Attribute的类。F12查看其定义:

    [AttributeUsage(AttributeTargets.Enum, Inherited = false)]
    public class FlagsAttribute : Attribute

在FlagsAttribute上又使用了特性AttributeUsage,它指示了FlagsAttribute只能应用于枚举(演示:标记于其他地方报错)

再次转到AttributeUsage的定义:

public sealed class AttributeUsageAttribute : Attribute

可以看到里面有一个有参构造函数和若干属性。对照AttributeUsage的使用,我们就可以知道:

  • AttributeTargets.Enum是其构造函数的参数:指定FlagsAttribute只能使用在枚举(Enum)上。说明:
    • 特性可以被使用于任何目标元素,包括:类、类成员、enum、delegate、assembly……但是,具体某个特性,应被使用在那种目标元素,是由声明特性时的AttributeUsage指定的。
    • 一个目标元素可以被附着多个Attribute
  • Inherited是其属性,Inherited=false说明该特性无法被子类继承

Obsolete

可以标记某个元素已经过时。(对应Java的@Deprecated

默认情况下标记为过时的元素还可以使用,但是会在编译时给出警告:

        [Obsolete]
        void learn(){ }

但如果使用Obsolete的有参构造函数,可以指定该元素无法使用。强行使用该元素会导致编译错误:

波浪线的颜色为绿色,表示警告
        [Obsolete("不能再使用了", true)]
        internal void learn(){ }


NUnit

NUnit最开始是模仿JUnit的一个第三方(非微软)的开源软件。

PS:但现在Visual Studio已经直接集成了NUnit,说明微软在开源和社区支持的路上确实是一路狂奔,因为这是一个和微软自己的MSTest Test和Unit Test直接竞争的单元测试框架。微软确实已经从“什么都要自己有”向“借用(不仅是借鉴)乃至大力支持一切优质开源项目”华丽转身。

在solution上右键添加项目,选择NUnit Test Project,输入项目名称,点击OK:

新建的单元测试项目包含一个默认的类文件:UnitTest1.cs,其中首先使用了using:

using NUnit.Framework;

因为NUnit的所有成员(类和方法等)都在NUnit.Framework命名空间(及其dll)之下。

然后有一个类:

    public class Tests
    {
        [SetUp]
        public void Setup()
        {
        }

        [Test]
        public void Test1()
        {
            Assert.Pass();
        }
    }

@想一想@:这个项目和Console Project不一样,它没有没有Main()函数作为入口,怎么运行呢?

这就需要用到反射了:NUnit会在整个程序集(项目)中遍历,找到带有特定标签(特性)的类和方法,予以相应的处理。

SetUp和Test

注意这个类里面的两个方法都被贴上了特性:

  • SetUp:被标记的方法将会在每一个测试方法被调用前调用
  • Test:被标记的方法会被依次调用

NUnit是依据特性而不是方法名来确定如何调用这些方法的,所以Tests的类名和其中的方法名都可以修改。

启动测试:

  • 快捷键Ctrl+E+T,或者
  • VS的菜单栏上,依次:Test-Windows-Test Explore
打开测试窗口:

然后在Test1上点击右键,就可以Run(运行)或者Debug(调试)这个测试方法了。演示:SetUp方法在每一个Test方法调用前调用

Assert(断言)

测试方法中现在可以使用Assert的各种方法,最常用的是Assert.AreEqual(),比较传入的两个参数:

        [Test]
        public void Test1()
        {
            Assert.AreEqual(5, 3 + 2);
        }

        [Test]
        public void Test2()
        {
            Assert.AreEqual(8, 3 + 2);
        }

前面一个参数代表你期望获得的值,后面一个参数代表实际获得的值。如果两个值相等,测试通过;否则会抛出AssertException异常。

一个方法里可以有多条Assert语句,只有方法里所有Assert语句全部通过,方法才算通过测试。方法通过,用绿色√表示;否则,用红色×标识。

点击未通过的方法,可以看到其详细信息:

尤其是StackTrace,是我们定位未通过Assert的有力工具。

更多的时候,我们是就其他项目中的对象方法的测试。所以,首先需要添加项目引用;然后实例化一个对象:

Student atai = new Student(60);
测试方法的返回值是否符合预期:
Assert.AreEqual(10, atai.Learn());
如果方法没有返回值,就比较所影响的对象属性:
atai.Learn();
Assert.AreEqual(70, atai.Score);

其他

Attributes

  • OneTimeSetUp:在第一个Setup之前被调用,一个测试类只调用一次
  • TearDown:在Test方法之后被调用
  • OneTimeTearDown:在最后一个TearDown之后被调用,一个测试类只调用一次

Assert

直接由Assert引导的简单比较方法:

  • 大于/小于(或等于):Greater()/Less()(OrEqual)
  • 真假:True/False:
  • 为Null/非数字/空:IsNull/IsNaN/IsEmpty

区分:

  • AreSame():严格要求同一个对象
  • AreEqual():同一个对象或者值相等都行

还可以由Assert.That引导,通常可用于数组(集合)的复杂测试

int[] array = { 1, 2, 1 };
  • 数组中的所有元素都为1:
    Assert.That(true, Is.All.EqualTo(1));
  • 数值中至少有一个元素为1:
    Assert.That(1, Is.AnyOf(array));
    这样会有问题,但是这样:
    Assert.That(1, Is.AnyOf(1, 2, 1));
    或者这样:
    object[] array = { 1, 2, 1 };
    又是OK的,结合AnyOf方法的定义:
    public static AnyOfConstraint AnyOf(params object[] expected)
    @想一想@:为什么?为了避免这种“麻烦”,还可以写成:
    Assert.That(array, Has.Member(1));

还可以测试方法是否能抛出异常:

Assert.Throws<ArgumentException>(() => atai.Learn());


#体会#

一般来说,都是第三方框架类库开发方:

  • 声明/定义/写好某些Attribute,
  • 通过反射调用/获取Attribute等

开发人员,只需要按文档使用特性即可。


作业

见:J&C:反射 / 特性(注释)/ 单元测试工具

学习笔记
源栈学历
今天学习不努力,明天努力找工作

作业

觉得很 ,不要忘记分享哟!

任何问题,都可以直接加 QQ群:273534701

在当前系列 C#语法 中继续学习:

多快好省!前端后端,线上线下,名师精讲

  • 先学习,后付费;
  • 不满意,不要钱。
  • 编程培训班,我就选源栈

更多了解 加:

QQ群:273534701

答疑解惑,远程debug……

B站 源栈-小九 的直播间

写代码要保持微笑 (๑•̀ㅂ•́)و✧

公众号:源栈一起帮

二维码