所有反射相关的类,都在名称空间:
using System.Reflection;
要得到这个变量的类型信息:
Type typeInfo = zjq.GetType();当然也可以使用typeof:
Type typeInfo = typeof(Student);
#常见面试题:两者的区别?#
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)了父类方法,调用规则和多态一致:随对象。
为了避免字符串的拼写错误,(较高版本)C#引入了nameof(),传入一个类成员,就能够得到它的字符串格式的名字。这样上面的代码就可以写成:
typeInfo.GetProperty(nameof(Name))注意,nameof():
一个项目就会被编译成一个程序集(Assembly),所以能猜到这啥意思不:
object atai = typeof(Student).Assembly.CreateInstance("CSharp.Student");
程序集是.NET代码运行的基本单元,表现形式为可执行文件(.exe或.dll)。演示:在bin中查看和运行。
二进制的可执行文件,需要用特定的反编译工具才能够阅读。(复习)
ILDASM在Visual Studio安装时默认自动安装,可以启用:
Tool - Command Line - Developer Command Prompt
然后通过运行ildasm命令自动打开
然后通过:文件 - 打开 一个编译过后的.dll文件,就能看到如下界面:
这就是一个程序集被反编译(不是反射)之后的内容,包括:
.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……这些都是非常基础非常底层的指令。同学们打个照面认识一下即可,不用深究。
.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#语言的源代码。
可以将IL再反编译成C#代码,在.NET闭源时一度非常流行。
可直接通过VS的Extensions Manage在其Marketplace下载。(略)
微软的一个可快捷查询.NET source code的网站。
可以在调试时选择从微软官方/github服务器下载响应的源代码,参与调试,非常方便!
(在VS2022中)需要做的也非常简单,在Tools - Options中设置:
这样,击中断点之后,按F11,就会弹出提示:
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] 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的整数次方,会出现一些“预期以外”的结果。
Flags本质上是一个继承了Attribute的类。F12查看其定义:
[AttributeUsage(AttributeTargets.Enum, Inherited = false)] public class FlagsAttribute : Attribute
在FlagsAttribute上又使用了特性AttributeUsage,它指示了FlagsAttribute只能应用于枚举(演示:标记于其他地方报错)
再次转到AttributeUsage的定义:
public sealed class AttributeUsageAttribute : Attribute
可以看到里面有一个有参构造函数和若干属性。对照AttributeUsage的使用,我们就可以知道:
可以标记某个元素已经过时。(对应Java的@Deprecated)
默认情况下标记为过时的元素还可以使用,但是会在编译时给出警告:
[Obsolete] void learn(){ }
但如果使用Obsolete的有参构造函数,可以指定该元素无法使用。强行使用该元素会导致编译错误:
波浪线的颜色为绿色,表示警告[Obsolete("不能再使用了", true)] internal void learn(){ }
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会在整个程序集(项目)中遍历,找到带有特定标签(特性)的类和方法,予以相应的处理。
注意这个类里面的两个方法都被贴上了特性:
NUnit是依据特性而不是方法名来确定如何调用这些方法的,所以Tests的类名和其中的方法名都可以修改。
启动测试:
然后在Test1上点击右键,就可以Run(运行)或者Debug(调试)这个测试方法了。演示:SetUp方法在每一个Test方法调用前调用
测试方法中现在可以使用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);
直接由Assert引导的简单比较方法:
区分:
还可以由Assert.That引导,通常可用于数组(集合)的复杂测试
int[] array = { 1, 2, 1 };
Assert.That(true, Is.All.EqualTo(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());
#体会#
一般来说,都是第三方框架类库开发方:
开发人员,只需要按文档使用特性即可。
多快好省!前端后端,线上线下,名师精讲
更多了解 加: