C#反射

C#反射

反射初步认识

作为一名C#小白,在学习C#的过程中,遇到了不太懂的内容,专门整理了一下在网上搜集到的资料。

反射的定义

反射提供了封装程序集、模块和类型的对象(Type类型)。可以使用反射动态创建类型的实例,将类型绑定到现有对象,或从现有对象获取类型并调用其方法或访问其字段和属性。如果代码中使用了特性,可以使用反射对他们进行访问。

  1. 元数据的动态查询;
  2. 绑定与执行;
  3. 动态代码生成;

使用场景

  • 需要访问程序元数据的特性
  • 检查和实例化程序集中的类型
  • 在运行时构建新类型。使用System.Reflection.Emit中的类
  • 执行后期绑定,访问在运行时创建的类型的方法

反射用到的命名空间

  • System.Reflection
  • System.Type
  • System.Reflection.Assembly

反射包括

  1. 程序集反射
  2. 类型反射
  3. 接口反射

利用反射实现延迟加载程序集

所有对程序集的引用都是在编译时进行的,因此效率会比较高。但是在某些特定的情况下,我们需要对程序集进行延迟加载,即对程序集的引用由编译时推移到运行时,反射是一个很好的选择。

using System;
using System.Reflection;

class Test
{
     public static void Main(string[] args)
     {
          string assemblyName=args[0];
          string typeName=args[1];
          string fieldName1=args[2]; 
          string fieldName2=args[3];
          string methodName=args[4];
          Assembly assembly=Assembly.Load(assemblyName);  //手动加载程序集
          Type type=assembly.GetType(typeName);           //获取程序集中的类型
          
          //查询
          MemberInfo[] mis=type.GetMembers();             //获取类型中的成员信息
          for(int i=0;i<mis.Length;i++)
          {
               Console.WriteLine(mis[i]);
              }          
          
              object obj=Activator.CreateInstance(type);  //创建对象实例
              
              //查询字段
              FieldInfo field1=type.GetField(fieldName1);
              FieldInfo field2=type.GetField(fieldName2);
              field1.SetValue(obj,100);   //实例成员必须依附于对象实例才能赋值
              field2.SetValue(obj,200);
              
              //查询方法
              MethodInfo method=type.GetMethod(methodName);
              
              //调用方法
              method.Invoke(obj,null);   //实例方法必须依附于对象实例才能执行
         }
    }
    

利用接口提高反射效率

那么怎么样才能提高反射的效率了,很简单,我们需要明确约定,也就是说只有满足了约定信息的类型才能被加载进来,也因此我们对类型成员的处理必须是满足了这一约定的,这样双方都有了一个共同的约定。那么我们用什么来实现这一约定了,当然需要用到接口了。
我们对上面的例子进行改进,通过对Point类型的观察,我们可以得到这样一个接口,我们先看接口的实现,并将其编译为IPoint.dll文件。

using System;
public interface IPoint
{
     public int X{set;get;}
     public int Y{set;get;}
     
     void Print();
}

下面我们来实现Point类,并对其进行编译,注意:在编译时确保对IPoint.dll的引用。

using System;
public class Point :IPoint
{
     private int x;
     private int y;
     
     public int X
     {
         set{this.x=value;}
         get{return x;}
     }
         
     public int Y
     {
         set{this.y=value;}
         get{return y;}
     }
             
     public void Print()
     {
          Console.WriteLine("[{0},{1}]",this.X,this.Y);
     }
}

下面我们再来看Reflect类的实现,在对其进行编译时同样需要对IPoint.dll进行引用。

using System;
using System.Reflection;

class Test
{
     public static void Main(string[] args)
     {
          string assemblyName=args[0];
          string typeName=args[1];
         
          Assembly assembly=Assembly.Load(assemblyName);  //手动加载程序集
          Type type=assembly.GetType(typeName);           //获取程序集中的类型
                
              IPoint obj=(IPoint)Activator.CreateInstance(type);  //通过接口创建对象实例
              obj.X=100;
              obj.Y=200;
              obj.Print();          
      }
}

最后运行编译后的Reflect.exe文件,运行时同样需要传入程序集信息和类型信息。这里我们看到Point程序集和Reflect程序集在编译时都对IPoint程序集进行了引用,因此在Reflect程序集中,虽然在编译时并没有Point类型信息,但是有IPoint接口信息,因此我们通过这个接口很方便的实现了我们需要的操作,只要未来加载进来的类型是实现了IPoint接口的就可以了,这样在运行时就不需要进行大量的校验工作了,这些工作都还原到了编译时,因此使用接口来实现反射也大大提高了反射的性能。

System.Type.GetType()和Object.GetType()与typeof比较

//运算符,获得某一类型的 System.Type 对象。
Type t = typeof(int);

//方法,获取当前实例的类型。
int i = 10;
Console.WriteLine(i.GetType());
//区别
Typeof()是运算符而GetType是方法
GetType()是基类System.Object的方法,因此只有建立一个实例之后才能被调用(也就是创建实例)
Typeof()的参数只能是lint,string,类,且不能是实例
得到结果的区别
(1)Typeof():得到一个class的Type
(2)GetType():得到一个class实例的Type

System.Type.GetType()的使用

Type type = System.Type.GetType(“ConsoleApplication1.child”);
Type type1 = System.Type.GetType(“System.Int32”);
Object.GetType()的小案例

public class Student
{
        public Student()
        {
        
        }
        public virtual string Id { get; set; }
        public virtual string StudentNo { get; set; }
        public virtual string Address { get; set; }
}

public class StudentDTO
{
       public StudentDTO()
       {

       }
       public virtual string Id { get; set; }
       public virtual string StudentNo { get; set; }
       public virtual int TeacherId { get; set; }
}
	//对student对象赋值
    Student student = new Student();
    student.Id = Guid.NewGuid().ToString();
    student.Name = "张三";
    student.Address = "福建";
	//将student的值赋予studentdto
    StudentDTO studentDTO = new StudentDTO();
    studentDTO.Id = student.Id;
    studentDTO.Name = student.Name;

改进:若是student的属性过多,那么可以通过此方法减少许多代码

foreach (var item in student.GetType().GetProperties())    //返回Student的所有公共属性
 {
       var value = item.GetValue(student, null);   //返回属性值    
       var setobj = studentDTO.GetType().GetProperty(item.Name);   //搜索具有指定属性名称的公共属性
       if (value != null && setobj != null)
       {
              setobj.SetValue(studentDTO, value, null);
       }
}

Assembly.Load、LoadFrom与LoadFile

  1. Assembly.Load()

简介

Load()方法接收一个String或AssemblyName类型作为参数,这个参数实际上是需要加载的程序集的强名称(名称,版本,语言,公钥标记)。例如.NET 2.0中的FileIOPermission类,它的强名称是:

System.Security.Permissions.FileIOPermission, mscorlib, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089

对于弱命名的程序集,则只会有程序集名称,而不会有版本,语言和公钥标记。如 TestClassLibrary

细节

  • CLR内部普遍使用了Load()方法来加载程序集,在Load()方法的内部,CLR首先会应用这个程序集的版本绑定重定向策略,接着在GAC中查找目标程序集。如果GAC中没有找到,则会在应用程序目录和子目录中寻找(应用配置文件的codebase所指定的位置)。
  • 如果希望加载弱命名程序集,Load()方法就不会去GAC中查找。
  • 当Load()找到目标程序集时,就会加载它,并返回一个相应Assembly对象的引用。
  • 当没有找到程序集时,会抛出System.IO.FileNotFoundException异常。
  • 当存在特定CPU架构的程序集时,CLR会优先加载当前架构的程序集(例如x86版本优先于IL中立版本)
  • 如果希望强迫加载某个架构版本的程序集,需要在强名称中加以指定。ProcessorArchitecture可以为x86 IA64 AMD64或MSIL,当然还有None
  • Load方法与Win32函数中的LoadLibrary方法等价
  1. Assembly.LoadFrom()

简介

LoadFrom()方法可以从指定文件中加载程序集,通过查找程序集的AssemblyRef元数据表,得知所有引用和需要的程序集,然后在内部调用Load()方法进行加载。

Assembly.LoadFrom(@“C:\ABC\Test.dll”);

细节

  • LoadFrom()首先会打开程序集文件,通过GetAssemblyName方法得到程序集名称,然后关闭文件,最后将得到的AssemblyName对象传入Load()方法中
  • 随后,Load()方法会再次打开这个文件进行加载。所以,LoadFrom()加载一个程序集时,会多次打开文件,造成了效率低下的现象(与Load相比)。
  • 由于内部调用了Load(),所以LoadFrom()方法还是会应用版本绑定重定向策略,也会在GAC和各个指定位置中进行查找。
  • LoadFrom()会直接返回Load()的结果——一个Assembly对象的引用。
  • 如果目标程序集已经加载过,LoadFrom()不会重新进行加载。
  • LoadFrom支持从一个URL加载程序集(如"http://www.abc.com/test.dll"),这个程序集会被下载到用户缓存文件夹中。
  • 从URL加载程序集时,如果计算机未联网,LoadFrom会抛出一个异常。如果IE被设置为“脱机工作”,则不会抛出异常,转而从缓存中寻找已下载的文件。
  1. Assembly.LoadFile()

简介

LoadFile()从一个指定文件中加载程序集,它和LoadFrom()的不同之处在于LoadFile()不会加载目标程序集所引用和依赖的其他程序集。您需要自己控制并显示加载所有依赖的程序集

细节

  • LoadFile()不会解析任何依赖
  • LoadFile()可以多次加载同一程序集
  • 显式加载依赖程序集的方法是,注册AppDomain的AssemblyResolve事件

对于反射的总结,我想从以下几个方面展开,首先是反射程序集,模块,类的成员以及成员的一些信息;接下来就是动态调用类的成员方法;第三个方面就动态产生程序集,模块和类以及类的成员。好了,现在就让我们从反射各种信息开始吧

在C#中,我们要使用反射,首先要搞清楚以下命名空间中几个类的关系:

System.Reflection命名空间

(1) AppDomain:应用程序域,可以将其理解为一组程序集的逻辑容器

(2) Assembly:程序集类

(3) Module:模块类

(4) Type:使用反射得到类型信息的最核心的类

他们之间是一种从属关系,也就是说,一个AppDomain可以包含N个Assembly,一个Assembly可以包含N个Module,而一个Module可以包含N个Type.

AppDomain这个类我们等下再来讲解。我们先关注Assembly个类

在程序中,如果我们要动态加载一个程序集怎么办呢?有几种方式可以使用,分别是Load、LoadFrom和LoadWithPartialName三个Assembly的静态方法.

先来讲解Assembly.Load方法,该方法会有多个重载版本,其中一个就是提供程序集的详细信息,即程序集的标识,包括程序集的名称,版本,区域信息,公有密钥标记,全部都是以一个字符串的形式提供,例如:"MyAssembly,Version=1.0.0.0,culture=zh-CN,PublicKeyToken=47887f89771bc57f”.

那么,使用Assembly.Load加载程序集的顺序是怎样的呢?首先它会去全局程序集缓存查找,然后到应用程序的根目录查找,最后会到应用程序的私有路径查找。

当然,如果你使用的是弱命名程序集,也即只给出程序集的名称,那么这个时候,CLR将不会在程序集上应用任何安全或者部署策略,而且Load也不会到全局缓存程序集中查找程序集。

测试加载弱命名程序集的例子如下:

(1) 新建一个控制台应用程序的工程,同时勾选创建解决方案

(2) 在解决方案中新建一个类库的项目,随便写一个类和一个方法

(3) 在控制台项目中,首先不添加引用,直接在Main方法中添加如下代码:

Assembly assembly = Assembly.Load(“MyAssembly”);

if (assembly != null)

{ Console.WriteLine(“加载成功”); }

执行程序,会抛出异常,说找不到该程序集。什么原因呢?因为我们使用的是弱命名程序集,Load方法不会去全局程序集缓存中查找,而该应用程序目录下又没有该程序集,所以程序找不到。这个时候,我们把程序稍微改一下,不用添加代码,只需添加对MyAssembly的引用,重新运行程序,加载成功了。

接下来,我们就要看看Load怎么加载强命名程序集了,这个步骤稍微有些复杂。还是刚才的项目,找到MyAssembly.dll程序集所在的目录,一般在bin"Debug目录下

(1)生成密钥对文件 sn –k MyAssemblyKey.keys

你也可以自己随便起一个密钥对文件名

(2)生成公钥文件

sn –p MyAssemblyKey.keys MyAssemblyPublicKey.PublicKey

注:查看公钥命令:sn –tp MyAssemblyPublicKey.PublicKey

(3)创建强命名程序集。

很简单,只需要在声明命名空间的那句代码上加上如下特性:

[assembly:AssemblyKeyFileAttribute(@”D:"Test"MyAssemblyKey.keys”)]

(4) 编译项目

(5) 将程序集添加到程序集全局缓存

gacutil –i MyAssembly.dll

这个时候,转到加载程序集的项目中,将Load方法中的参数改为”程序集名,Version=版本,culture=区域信息,PublicKeyToken=公钥“,然后再去掉对程序集的引用,我们会发现,程序运行成功。表明Load到全局缓存区查找到了该程序集。

使用Load方法加载程序集,特别是强命名程序集,能在程序集上应用安全和部署策略,推荐使用该方法动态加载程序集,至于LoadFrom和LoadWithPartialName。

首先我们还是来看看LoadFrom方法,这个方法的原理是这样的:我们如果要使用它来动态加载程序集,必须告诉它程序集的路径,也即在哪个目录下面,CLR会去加载与你指定的路径完全匹配的程序集。记住,当我们指定程序集路径时,不能包括任何关于程序集强命名的信息,所以,CLR不会在我们指定的程序集文件上应用任何策略,而且也不会去任何其他的地方搜索程序集,简言之,它就是指哪打哪,呵呵。

例如:你有个程序集在D:/Test/MyAssembly.dll,你要用Assembly.LoadFrom加载该程序集,代码就如下:

Assembly assembly = Assembly.LoadFrom(@”D:/Test/MyAssembly.dll”);

对于,LoadWithParitalName方法,推荐大家最好不要使用它,因为程序无法确定最终要去加载哪个程序集的版本,所以我们这里只是简单的介绍一下它的工作原理:你可以传递一个程序集标识给它,包括程序集名称,至于其他信息是可选的(区域信息,公有密钥等),该方法执行时,会首先检查应用程序中配置文件的qualifyAssembly节点,如果存在,则把该部分名称的程序集替换成完全的程序集标识,如果不存在,则使用程序集名称先到应用程序根目录下查找,然后是私有目录,没有找到的话,就到程序集全局缓存中查找。简单过程如下:

   应用程序根目录 -> 应用程序私有目录 -> 程序集全局缓存. 

Assembly.Load()方法,Assembly.LoadFrom()方法,Assembly.LoadFile()方法的区别!

1,Assembly.Load()

这个方法通过程序集的长名称(包括程序集名,版本信息,语言文化,公钥标记)来加载程序集的,会加载此程序集引用的其他程序集,一般情况下都应该优先使用 这个方法,他的执行效率比LoadFrom要高很多,而且不会造成重复加载的问题(原因在第2点上说明)

使用这个方法的时候, CLR会应用一定的策略来查找程序集,实际上CLR按如下的顺序来定位程序集:

⑴如果程序集有强名称,在首先在全局程序集缓(GAC)中查找程序集。

⑵如果程序集的强名称没有正确指定或GAC中找不到,那么通过配置文件中的元素指定的URL来查找

⑶如果没有指定强名称或是在GAC中找不到,CLR会探测特定的文件夹:

假设你的应用程序目录是C:\AppDir,元素中的privatePath指定了一个路径Path1,你要定位的程序集是AssemblyName.dll则CLR将按照如下顺序定位程序集

      C:\AppDir\AssemblyName.dll

      C:\AppDir\AssemblyName\AssemblyName.dll

      C:\AppDir\Path1\AssemblyName.dll

      C:\AppDir\Path1\AssemblyName\AssemblyName.dll

如果以上方法不能找到程序集,会发生编译错误,如果是动态加载程序集,会在运行时抛出异常!

2,Assembly.LoadFrom()

这个方法从指定的路径来加载程序集,实际上这个方法被调用的时候,CLR会打开这个文件,获取其中的程序集版本,语言文化,公钥标记等信息,把他们传递给 Load方法,接着,Load方法采用上面的策略来查找程序集。如果找到了程序集,会和LoadFrom方法中指定的路径做比较,如果路径相同,该程序集会被认为是应用程序的一部分,如果路径不同或Load方法没有找到程序集,那该程序集只是被作为一个“数据文件”来加载,不会被认为是应用程序的一部分。 这就是在第1点中提到的Load方法比LoadFrom方法的执行效率高的原因。另外,由于可能把程序集作为“数据文件”来加载,所以使用 LoadFrom从不同路径加载相同程序集的时候会导致重复加载。当然这个方法会加载此程序集引用的其他程序集。

3,Assembly.LoadFile()

这个方法是从指定的文件来加载程序集,和上面方法的不同之处是这个方法不会加载此程序集引用的其他程序集!

结论:一般大家应该优先选择Load方法来加载程序集,如果遇到需要使用LoadFrom方法的时候,最好改变设计而用Load方法来代替!

另:Assembly.LoadFile 与 Assembly.LoadFrom的区别

1、Assembly.LoadFile只载入相应的dll文件,比如Assembly.LoadFile(“abc.dll”),则载入abc.dll,假如abc.dll中引用了def.dll的话,def.dll并不会被载入。

Assembly.LoadFrom则不一样,它会载入dll文件及其引用的其他dll,比如上面的例子,def.dll也会被载入。

2、用Assembly.LoadFrom载入一个Assembly时,会先检查前面是否已经载入过相同名字的Assembly,比如abc.dll有两个版本(版本1在目录1下,版本2放在目录2下),程序一开始时载入了版本1,当使用Assembly.LoadFrom(“2\abc.dll”)载入版本2时,不能载入,而是返回版本1。Assembly.LoadFile的话则不会做这样的检查,比如上面的例子换成Assembly.LoadFile的话,则能正确载入版本2。

LoadFile:加载指定路径上的程序集文件的内容。LoadFrom: 根据程序集的文件名加载程序集文件的内容。

区别:

LoadFile 方法用来来加载和检查具有相同标识但位于不同路径中的程序集.但不会加载程序的依赖项。

LoadFrom 不能用于加载标识相同但路径不同的程序集。

GetMembers、GetField和GetMethod

public class RefClass
{
         private int _test3;
         private int _test1 { get; set; }
         protected int Test2 { get; set; }
         public int Test3 { get; set; }
         private static void Show2()
         {
         }

          public static void Show3()
          {
          }

  		 public void Show()
         {

         }
}
static void Main(string[] args)
{
       Type t = typeof(RefClass);
       Func<MemberTypes, String> getType = (x) =>
       {
                  switch (x)
                  {
                      case MemberTypes.Field:
                      {
                             return "字段";
                      }
                     case MemberTypes.Method:
                     {
                            return "方法";
                     }
                    case MemberTypes.Property:
                    {
                             return "属性";
                    }
                    default:
                    {
                             return "未知";
                    }
                }
             };
             MemberInfo[] minfos = t.GetMembers(BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.Public | BindingFlags.DeclaredOnly | BindingFlags.Static );
             foreach (MemberInfo minfo in minfos)
            {
                 Console.WriteLine(minfo.Name + ";类型:" + getType(minfo.MemberType));
             }
             Console.ReadKey();
         }

在这里插入图片描述

1			static void Main(string[] args)
 2         {
 3             Type t = typeof(RefClass);
 4             FieldInfo[] finfos = t.GetFields(BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly);
 5             foreach (FieldInfo finfo in finfos)
 6             {
 7                 Console.WriteLine("字段名称:{0}  字段类型:{1} ", finfo.Name, finfo.FieldType.ToString());
 8             }
 9             Console.ReadKey();
10         }

在这里插入图片描述

1         static void Main(string[] args)
 2         {
 3             Type t = typeof(RefClass);
 4             RefClass rc = new RefClass();
 5             rc.Test3 = 3;
 6             FieldInfo[] finfos = t.GetFields(BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly);
 7             foreach (FieldInfo finfo in finfos)
 8             {
 9                 Console.WriteLine("字段名称:{0}  字段类型:{1} rc中的值为:{2}", finfo.Name, finfo.FieldType.ToString(), finfo.GetValue(rc));
10             }
11             Console.ReadKey();
12         }

在这里插入图片描述

1         static void Main(string[] args)
 2         {
 3             Type t = typeof(RefClass);
 4             RefClass rc = new RefClass();
 5             rc.Test3 = 3;
 6             FieldInfo[] finfos = t.GetFields(BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly);
 7             foreach (FieldInfo finfo in finfos)
 8             {
 9                 finfo.SetValue(rc, 100);
10                 Console.WriteLine("字段名称:{0}  字段类型:{1} rc中的值为:{2}", finfo.Name, finfo.FieldType.ToString(), finfo.GetValue(rc));
11             }
12             Console.ReadKey();
13         }

在这里插入图片描述

 1         static void Main(string[] args)
 2         {
 3             Type t = typeof(RefClass);
 4             RefClass rc = new RefClass();
 5             rc.Test3 = 3;
 6             PropertyInfo[] finfos = t.GetProperties(BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly);
 7             foreach (PropertyInfo finfo in finfos)
 8             {
 9                 MethodInfo getinfo = finfo.GetGetMethod(true);
10                 Console.WriteLine("get方法的名称{0}  返回值类型:{1}  参数数量:{2}  MSIL代码长度:{3} 局部变量数量:{4}", getinfo.Name, getinfo.ReturnType.ToString(),
11                     getinfo.GetParameters().Count(),
12                     getinfo.GetMethodBody().GetILAsByteArray().Length, 
13                     getinfo.GetMethodBody().LocalVariables.Count);
14 
15                 MethodInfo setinfo = finfo.GetSetMethod(true);
16                 Console.WriteLine("get方法的名称{0}  返回值类型:{1}  参数数量:{2}  MSIL代码长度:{3} 局部变量数量:{4}", setinfo.Name, setinfo.ReturnType.ToString(),
17                     setinfo.GetParameters().Count(),
18                     setinfo.GetMethodBody().GetILAsByteArray().Length,
19                     setinfo.GetMethodBody().LocalVariables.Count);
20 
21                 setinfo.Invoke(rc, new object[] { 123 });
22                 object obj = getinfo.Invoke(rc, null);
23                 Console.WriteLine("方法名:{0}  内部值:{1}", finfo.Name, obj);
24             }
25             Console.ReadKey();
26         }

在这里插入图片描述

1         public class RefClass
 2         {
 3             private int _test3;
 4             private int _test1 { get; set; }
 5             protected int Test2 { get; set; }
 6             public int Test3 { get; set; }
 7 
 8             private static void Show2()
 9             {
10 
11             }
12 
13             public static string Show3(string s)
14             {
15                 int b;
16                 int c;
17                 return s;
18             }
19 
20             public string Show(string s)
21             {
22                 string a;
23                 return s;
24             }
25         }
1         static void Main(string[] args)
 2         {
 3             Type t = typeof(RefClass);
 4             RefClass rc = new RefClass();
 5             rc.Test3 = 3;
 6             MethodInfo[] finfos = t.GetMethods(BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly | BindingFlags.Static );
 7             foreach (MethodInfo finfo in finfos)
 8             {
 9                 if (finfo.GetParameters().Count() > 0 && finfo.GetParameters()[0].ParameterType == typeof(string) )
10                 {
11                     object obj = finfo.Invoke(rc, new[] { "123" });
12                     MethodBody mbody = finfo.GetMethodBody();
13                     Console.WriteLine("拥有参数的方法名:{0}  返回值类型:{1}  参数1类型:{2}  参数1名称:{3}  方法调用后返回的值:{4}",
14                         finfo.Name,
15                         finfo.ReturnType.ToString(),
16                         finfo.GetParameters()[0].ParameterType.ToString(),
17                         finfo.GetParameters()[0].Name,
18                         obj.ToString());
19                 }
20                 else
21                 {
22                     MethodBody mbody = finfo.GetMethodBody();
23                     Console.WriteLine("没有参数的方法名:{0}  返回值类型:{1}",
24                         finfo.Name,
25                         finfo.ReturnType.ToString());
26                 }
27             }
28             Console.ReadKey();
29         }

在这里插入图片描述

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值