• (转)c#的反射,迷糊呀!!


     反射的概述 
       
      反射appDomain 的程序集 
       
      反射单个程序集 
       
      利用反射获取类型信息 
       
      设置反射类型的成员 
       
      通过反射创建类型的实例 
       
      反射类型的接口 
       
      反射的性能 
       
      反射的概述 
       
      反射的定义:审查元数据并收集关于它的类型信息的能力。元数据(编译以后的最基本数据单元)就是一大堆的表,当编译程序集或者模块时,编译器会创建一个类定义表,一个字段定义表,和一个方法定义表等,。System.reflection命名空间包含的几个类,允许你反射(解析)这些元数据表的代码 
       
      和反射相关的命名空间(我们就是通过这几个命名空间访问反射信息): 
       
      System.Reflection.MemberInfo 
       
       System.Reflection.EventInfo 
       
       System.Reflection.FieldInfo 
       
       System.Reflection.MethodBase 
       
       System.Reflection.ConstructorInfo 
       
       System.Reflection.MethodInfo 
       
       System.Reflection.PropertyInfo 
       
       System.Type 
       
       System.Reflection.Assembly 
       
      反射的层次模型: 
       
       
      注:层次间都是一对多的关系 
       
      反射的作用: 
       
      1. 可以使用反射动态地创建类型的实例,将类型绑定到现有对象,或从现 有对象中获取类型 
       
      2. 应用程序需要在运行时从某个特定的程序集中载入一个特定的类型,以便实现某个任务时可以用到反射。 
       
      3. 反射主要应用与类库,这些类库需要知道一个类型的定义,以便提供更多的功能。 
       
      应用要点: 
       
      1. 现实应用程序中很少有应用程序需要使用反射类型 
       
      2. 使用反射动态绑定需要牺牲性能 
       
      3. 有些元数据信息是不能通过反射获取的 
       
      4. 某些反射类型是专门为那些clr 开发编译器的开发使用的,所以你要意识到不是所有的反射类型都是适合每个人的。 
       
       
       
      反射appDomain 的程序集 
       
       当你需要反射AppDomain 中包含的所有程序集,示例如下: 
       static void Main 
       
       { 
       
       //通过GetAssemblies 调用appDomain的所有程序集 
       
      foreach (Assembly assem in Appdomain.currentDomain.GetAssemblies()) 
       
      { 
       
       //反射当前程序集的信息 
       
       reflector.ReflectOnAssembly(assem) 
       
      } 
       
      } 
       
      说明:调用AppDomain 对象的GetAssemblies 方法 将返回一个由System.Reflection.Assembly元素组成的数组。 
       
      反射单个程序集 
       
      上面的方法讲的是反射AppDomain的所有程序集,我们可以显示的调用其中的一个程序集,system.reflecton.assembly 类型提供了下面三种方法: 
       
      1. Load 方法:极力推荐的一种方法,Load 方法带有一个程序集标志并载入它,Load 将引起CLR把策略应用到程序集上,先后在全局程序集缓冲区,应用程序基目录和私有路径下面查找该程序集,如果找不到该程序集系统抛出异常 
       
      2. LoadFrom 方法:传递一个程序集文件的路径名(包括扩展名),CLR会载入您指定的这个程序集,传递的这个参数不能包含任何关于版本号的信息,区域性,和公钥信息,如果在指定路径找不到程序集抛出异常。 
       
      3. LoadWithPartialName:永远不要使用这个方法,因为应用程序不能确定再在载入的程序集的版本。该方法的唯一用途是帮助那些在.Net框架的测试环节使用.net 框架提供的某种行为的客户,这个方法将最终被抛弃不用。 
       
      注意:system.AppDomain 也提供了一种Load 方法,他和Assembly的静态Load 方法不一样,AppDomain的load 方法是一种实例方法,返回的是一个对程序集的引用,Assembly的静态Load 方发将程序集按值封装发回给发出调用的AppDomain.尽量避免使用AppDomain的load 方法 
       
       
       
      利用反射获取类型信息 
       
      前面讲完了关于程序集的反射,下面在讲一下反射层次模型中的第三个层次,类型反射 
       
      一个简单的利用反射获取类型信息的例子: 
       
      using system; 
       
      using sytem.reflection; 
       
      class reflecting 
       
      { 
       
       static void Main(string[]args) 
       
      { 
       
       reflecting reflect=new reflecting();//定义一个新的自身类 
       
       //调用一个reflecting.exe程序集 
       
       assembly myAssembly =assembly.loadfrom(“reflecting.exe”) 
       
       reflect.getreflectioninfo(myAssembly);//获取反射信息 
       
      } 
       
      //定义一个获取反射内容的方法 
       
      void getreflectioninfo(assembly myassembly) 
       
      { 
       
       type[] typearr=myassemby.Gettypes();//获取类型 
       
       foreach (type type in typearr)//针对每个类型获取详细信息 
       
       { 
       
       //获取类型的结构信息 
       
       constructorinfo[] myconstructors=type.GetConstructors; 
       
       //获取类型的字段信息 
       
       fieldinfo[] myfields=type.GetFiedls() 
       
       //获取方法信息 
       
       MethodInfo myMethodInfo=type.GetMethods(); 
       
       //获取属性信息 
       
       propertyInfo[] myproperties=type.GetProperties 
       
       //获取事件信息 
       
       EventInfo[] Myevents=type.GetEvents; 
       
       
       
      } 
       
      } 
       
      } 
       
      其它几种获取type对象的方法: 
       
      1. System.type 参数为字符串类型,该字符串必须指定类型的完整名称(包括其命名空间) 
       
      2. System.type 提供了两个实例方法:GetNestedType,GetNestedTypes 
       
      3. Syetem.Reflection.Assembly 类型提供的实例方法是:GetType,GetTypes,GetExporedTypes 
       
      4. System.Reflection.Moudle 提供了这些实例方法:GetType,GetTypes,FindTypes 
       
      设置反射类型的成员 
       
       反射类型的成员就是反射层次模型中最下面的一层数据。我们可以通过type对象的GetMembers 方法取得一个类型的成员。如果我们使用的是不带参数的GetMembers,它只返回该类型的公共定义的静态变量和实例成员,我们也可以通过使用带参数的GetMembers通过参数设置来返回指定的类型成员。具体参数参考msdn 中system.reflection.bindingflags 枚举类型的详细说明。 
       
      例如: 
       
       
       
      //设置需要返回的类型的成员内容 
       
      bindingFlags bf=bingdingFlags.DeclaredOnly|bingdingFlags.Nonpublic|BingdingFlags.Public; 
       
      foreach (MemberInfo mi int t.getmembers(bf)) 
       
      { 
       
       writeline(mi.membertype) //输出指定的类型成员 
       
      } 
       
      通过反射创建类型的实例 
       
      通过反射可以获取程序集的类型,我们就可以根据获得的程序集类型来创建该类型新的实例,这也是前面提到的在运行时创建对象实现晚绑定的功能 
       
      我们可以通过下面的几个方法实现: 
       
      1. System.Activator 的CreateInstance方法。该方法返回新对象的引用。具体使用方法参见msnd 
       
      2. System.Activator 的createInstanceFrom 与上一个方法类似,不过需要指定类型及其程序集 
       
      3. System.Appdomain 的方法:createInstance,CreateInstanceAndUnwrap,CreateInstranceFrom和CreateInstraceFromAndUnwrap 
       
      4. System.type的InvokeMember实例方法:这个方法返回一个与传入参数相符的构造函数,并构造该类型。 
       
      5. System.reflection.constructinfo 的Invoke实例方法 
       
      反射类型的接口 
       
      如果你想要获得一个类型继承的所有接口集合,可以调用Type的FindInterfaces GetInterface或者GetInterfaces。所有这些方法只能返回该类型直接继承的接口,他们不会返回从一个接口继承下来的接口。要想返回接口的基础接口必须再次调用上述方法。 
       
      反射的性能: 
       
      使用反射来调用类型或者触发方法,或者访问一个字段或者属性时clr 需 要做更多的工作:校验参数,检查权限等等,所以速度是非常慢的。所以尽量不要使用反射进行编程,对于打算编写一个动态构造类型(晚绑定)的应用程序,可以采取以下的几种方式进行代替: 
       
      1. 通过类的继承关系。让该类型从一个编译时可知的基础类型派生出来,在运行时生成该类 型的一个实例,将对其的引用放到其基础类型的一个变量中,然后调用该基础类型的虚方法。 
       
      2. 通过接口实现。在运行时,构建该类型的一个实例,将对其的引用放到其接口类型的一个变量中,然后调用该接口定义的虚方法。 
       
      3.通过委托实现。让该类型实现一个方法,其名称和原型都与一个在编译时就已知的委托相符。在运行时先构造该类型的实例,然后在用该方法的对象及名称构造出该委托的实例,接着通过委托调用你想要的方法。这个方法相对与前面两个方法所作的工作要多一些,效率更低一些 


    -----------------------------------------------------------------------------------------------------------------------------------------------------

    反射技术
    我经常在博客园看到运用反射技术的***,可是遗憾自己一直不知道什么是反射,急忙找些资料学习学习。
    程序集包含模块,而模块包含类型,类型又包含成员。反射则提供了封装程序集、模块和类型的对象。您可以使用反射动态地创建类型的实例,将类型绑定到现有对象,或从现有对象中获取类型。然后,可以调用类型的方法或访问其字段和属性。反射通常具有以下用途:
    使用 Assembly 定义和加载程序集,加载在程序集清单中列出的模块,以及从此程序集中查找类型并创建该类型的实例。
    使用 Module 了解如下的类似信息:包含模块的程序集以及模块中的类等。您还可以获取在模块上定义的所有全局方法或其他特定的非全局方法。
    使用 ConstructorInfo 了解如下的类似信息:构造函数的名称、参数、访问修饰符(如 public 或 private)和实现详细信息(如 abstract 或 virtual)等。使用 Type 的 GetConstructors 或 GetConstructor 方法来调用特定的构造函数。
    使用 MethodInfo 来了解如下的类似信息:方法的名称、返回类型、参数、访问修饰符(如 public 或 private)和实现详细信息(如 abstract 或 virtual)等。使用 Type 的 GetMethods 或 GetMethod 方法来调用特定的方法。
    使用 FieldInfo 来了解如下的类似信息:字段的名称、访问修饰符(如 public 或 private)和实现详细信息(如 static)等;并获取或设置字段值。
    使用 EventInfo 来了解如下的类似信息:事件的名称、事件处理程序数据类型、自定义属性、声明类型和反射类型等;并添加或移除事件处理程序。
    使用 PropertyInfo 来了解如下的类似信息:属性的名称、数据类型、声明类型、反射类型和只读或可写状态等;并获取或设置属性值。
    使用 ParameterInfo 来了解如下的类似信息:参数的名称、数据类型、参数是输入参数还是输出参数,以及参数在方法签名中的位置等。
    举例说明:

    using System;

    using System.Reflection;



    namespace ReflectionExample

    {

         
    class Class1

         
    {

             [STAThread]

             
    static void Main(string[] args)

             
    {

                  Console.WriteLine(
    "列出程序集中所有类型");

                  Assembly a 
    = Assembly.LoadFrom("ReflectionExample.exe");

                  Type[] mytypes 
    = a.GetTypes();

                  
    foreach(Type t in mytypes)

                  
    {

                       Console.WriteLine(t.Name);

                  }


                  Console.WriteLine(
    "列出HelloWorld中的所有方法");

                  Type ht 
    = typeof(HelloWorld);

                  MethodInfo[] mif 
    = ht.GetMethods();

                  
    foreach(MethodInfo mf in mif)

                  
    {

                       Console.WriteLine(mf.Name);

                  }


                  Console.WriteLine(
    "实例化HelloWorld,并调用HelloWorld方法");

                  Object obj 
    = Activator.CreateInstance(ht);

                  
    string[] s = {"wssmax"};

                  Object objName 
    = Activator.CreateInstance(ht,s);

                  MethodInfo msayhello 
    = ht.GetMethod("SayHello");

                  msayhello.Invoke(obj,
    null);

                  msayhello.Invoke(objName,
    null);

                  Console.ReadLine();

             }


         }


    }




    using System;



    namespace ReflectionExample

    {

         
    public class HelloWorld

         
    {

             
    string myName;

             
    public HelloWorld(string name)

             
    {

                  myName 
    = name;

             }


             
    public HelloWorld():this(null)

             
    {}

             
    public string Name

             
    {

                  
    get{return myName;}

             }


             
    public void SayHello()

             
    {

                  
    if(myName == null)

                       Console.WriteLine(
    "Hello World");

                  
    else

                       Console.WriteLine(
    "Hello "+myName);

             }


         }


    }


    得到double类的信息:
    using System;
    using System.Text;
    using System.Windows.Forms;
    using System.Reflection;

    namespace Wrox.ProCSharp.TypeView
    {
        
    class MainClass
        
    {
            
    static void Main()
            
    {
                
    // modify this line to retrieve details of any
                
    // other data type
                Type t = typeof(double);
       
                AnalyzeType(t);
                MessageBox.Show(OutputText.ToString(), 
    "Analysis of type " + t.Name);
                Console.ReadLine();
            }


            
    static void AnalyzeType(Type t)
            
    {
                AddToOutput(
    "Type Name:  " + t.Name);
                AddToOutput(
    "Full Name:  " + t.FullName);
                AddToOutput(
    "Namespace:  " + t.Namespace);
                Type tBase 
    = t.BaseType;
                
    if (tBase != null)
                    AddToOutput(
    "Base Type:" + tBase.Name);
                Type tUnderlyingSystem 
    = t.UnderlyingSystemType;
                
    if (tUnderlyingSystem != null)
                    AddToOutput(
    "UnderlyingSystem Type:" + tUnderlyingSystem.Name);

                AddToOutput(
    "\nPUBLIC MEMBERS:");
                MemberInfo [] Members 
    = t.GetMembers();
                
    foreach (MemberInfo NextMember in Members)
                
    {
                    AddToOutput(NextMember.DeclaringType 
    + " " + NextMember.MemberType +
                        
    " " + NextMember.Name);
                }

            }


            
    static void AddToOutput(string Text)
            
    {
                OutputText.Append(
    "\n" + Text);
            }


            
    static StringBuilder OutputText = new StringBuilder(500);
        }

    }
    posted on 2006-01-04 10:01 梦在天涯 阅读(251) 评论(1)  编辑 收藏 收藏至365Key 所属分类: C#/.NET

    FeedBack:
    # re: c#的反射
    2006-04-19 12:16 | 梦在天涯
    关于反射

    提纲:
    1、什么是反射
    2、命名空间与装配件的关系
    3、运行期得到类型信息有什么用
    4、如何使用反射获取类型
    5、如何根据类型来动态创建对象
    6、如何获取方法以及动态调用方法
    7、动态创建委托


    1、什么是反射
    Reflection,中文翻译为反射。
    这是.Net中获取运行时类型信息的方式,.Net的应用程序由几个部分:‘程序集(Assembly)’、‘模块(Module)’、‘类型(class)’组成,而反射提供一种编程的方式,让程序员可以在程序运行期获得这几个组成部分的相关信息,例如:

    Assembly类可以获得正在运行的装配件信息,也可以动态的加载装配件,以及在装配件中查找类型信息,并创建该类型的实例。
    Type类可以获得对象的类型信息,此信息包含对象的所有要素:方法、构造器、属性等等,通过Type类可以得到这些要素的信息,并且调用之。
    MethodInfo包含方法的信息,通过这个类可以得到方法的名称、参数、返回值等,并且可以调用之。
    诸如此类,还有FieldInfo、EventInfo等等,这些类都包含在System.Reflection命名空间下。

    2、命名空间与装配件的关系
    很多人对这个概念可能还是很不清晰,对于合格的.Net程序员,有必要对这点进行澄清。
    命名空间类似与Java的包,但又不完全等同,因为Java的包必须按照目录结构来放置,命名空间则不需要。

    装配件是.Net应用程序执行的最小单位,编译出来的.dll、.exe都是装配件。

    装配件和命名空间的关系不是一一对应,也不互相包含,一个装配件里面可以有多个命名空间,一个命名空间也可以在多个装配件中存在,这样说可能有点模糊,举个例子:
    装配件A:
    namespace N1
    {
    public class AC1 {…}
    public class AC2 {…}
    }
    namespace N2
    {
    public class AC3 {…}
    public class AC4{…}
    }
    装配件B:
    namespace N1
    {
    public class BC1 {…}
    public class BC2 {…}
    }
    namespace N2
    {
    public class BC3 {…}
    public class BC4{…}
    }

    这两个装配件中都有N1和N2两个命名空间,而且各声明了两个类,这样是完全可以的,然后我们在一个应用程序中引用装配件A,那么在这个应用程序中,我们能看到N1下面的类为AC1和AC2,N2下面的类为AC3和AC4。
    接着我们去掉对A的引用,加上对B的引用,那么我们在这个应用程序下能看到的N1下面的类变成了BC1和BC2,N2下面也一样。
    如果我们同时引用这两个装配件,那么N1下面我们就能看到四个类:AC1、AC2、BC1和BC2。

    到这里,我们可以清楚一个概念了,命名空间只是说明一个类型是那个族的,比如有人是汉族、有人是回族;而装配件表明一个类型住在哪里,比如有人住在北京、有人住在上海;那么北京有汉族人,也有回族人,上海有汉族人,也有回族人,这是不矛盾的。

    上面我们说了,装配件是一个类型居住的地方,那么在一个程序中要使用一个类,就必须告诉编译器这个类住在哪儿,编译器才能找到它,也就是说必须引用该装配件。
    那么如果在编写程序的时候,也许不确定这个类在哪里,仅仅只是知道它的名称,就不能使用了吗?答案是可以,这就是反射了,就是在程序运行的时候提供该类型的地址,而去找到它。
    有兴趣的话,接着往下看吧。

    3、运行期得到类型信息有什么用
    有人也许疑问,既然在开发时就能够写好代码,干嘛还放到运行期去做,不光繁琐,而且效率也受影响。
    这就是个见仁见智的问题了,就跟早绑定和晚绑定一样,应用到不同的场合。有的人反对晚绑定,理由是损耗效率,但是很多人在享受虚函数带来的好处的时侯还没有意识到他已经用上了晚绑定。这个问题说开去,不是三言两语能讲清楚的,所以就点到为止了。
    我的看法是,晚绑定能够带来很多设计上的便利,合适的使用能够大大提高程序的复用性和灵活性,但是任何东西都有两面性,使用的时侯,需要再三衡量。

    接着说,运行期得到类型信息到底有什么用呢?
    还是举个例子来说明,很多软件开发者喜欢在自己的软件中留下一些接口,其他人可以编写一些插件来扩充软件的功能,比如我有一个媒体播放器,我希望以后可以很方便的扩展识别的格式,那么我声明一个接口:
    public interface IMediaFormat
    {
    string Extension {get;}
    Decoder GetDecoder();
    }
    这个接口中包含一个Extension属性,这个属性返回支持的扩展名,另一个方法返回一个解码器的对象(这里我假设了一个Decoder的类,这个类提供把文件流解码的功能,扩展插件可以派生之),通过解码器对象我就可以解释文件流。
    那么我规定所有的解码插件都必须派生一个解码器,并且实现这个接口,在GetDecoder方法中返回解码器对象,并且将其类型的名称配置到我的配置文件里面。
    这样的话,我就不需要在开发播放器的时侯知道将来扩展的格式的类型,只需要从配置文件中获取现在所有解码器的类型名称,而动态的创建媒体格式的对象,将其转换为IMediaFormat接口来使用。

    这就是一个反射的典型应用。

    4、如何使用反射获取类型
    首先我们来看如何获得类型信息。
    获得类型信息有两种方法,一种是得到实例对象
    这个时侯我仅仅是得到这个实例对象,得到的方式也许是一个object的引用,也许是一个接口的引用,但是我并不知道它的确切类型,我需要了解,那么就可以通过调用System.Object上声明的方法GetType来获取实例对象的类型对象,比如在某个方法内,我需要判断传递进来的参数是否实现了某个接口,如果实现了,则调用该接口的一个方法:

    public void Process( object processObj )
    {
    Type t = processsObj.GetType();
    if( t.GetInterface(“ITest”) !=null )

    }

    另外一种获取类型的方法是通过Type.GetType以及Assembly.GetType方法,如:
    Type t = Type.GetType(“System.String”);
    需要注意的是,前面我们讲到了命名空间和装配件的关系,要查找一个类,必须指定它所在的装配件,或者在已经获得的Assembly实例上面调用GetType。
    本装配件中类型可以只写类型名称,另一个例外是mscorlib.dll,这个装配件中声明的类型也可以省略装配件名称(.Net装配件编译的时候,默认都引用了mscorlib.dll,除非在编译的时候明确指定不引用它),比如:
    System.String是在mscorlib.dll中声明的,上面的Type t = Type.GetType(“System.String”)是正确的
    System.Data.DataTable是在System.Data.dll中声明的,那么:
    Type.GetType(“System.Data.DataTable”)就只能得到空引用。
    必须:
    Type t = Type.GetType("System.Data.DataTable,System.Data,Version=1.0.3300.0, Culture=neutral, PublicKeyToken=b77a5c561934e089");
    这样才可以,大家可以看下面这个帖子:
    http://expert.csdn.net/Expert/topic/2210/2210762.xml?temp=.1919977
    qqchen的回答很精彩


    5、如何根据类型来动态创建对象
    System.Activator提供了方法来根据类型动态创建对象,比如创建一个DataTable:

    Type t = Type.GetType("System.Data.DataTable,System.Data,Version=1.0.3300.0, Culture=neutral, PublicKeyToken=b77a5c561934e089");

    DataTable table = (DataTable)Activator.CreateInstance(t);

    例二:根据有参数的构造器创建对象
    namespace TestSpace {
    public class TestClass
    {
    private string _value;
    public TestClass(string value) {
    _value=value;
    }
    }
    }

    Type t = Type.GetType(“TestSpace.TestClass”);
    Object[] constructParms = new object[] {“hello”}; //构造器参数
    TestClass obj = (TestClass)Activator.CreateInstance(t,constructParms);

    把参数按照顺序放入一个Object数组中即可

    6、如何获取方法以及动态调用方法
    namespace TestSpace
    {
    public class TestClass {
    private string _value;
    public TestClass() {
    }
    public TestClass(string value) {
    _value = value;
    }

    public string GetValue( string prefix ) {
    if( _value==null )
    return "NULL";
    else
    return prefix+" : "+_value;
    }

    public string Value {
    set {
    _value=value;
    }
    get {
    if( _value==null )
    return "NULL";
    else
    return _value;
    }
    }
    }
    }

    上面是一个简单的类,包含一个有参数的构造器,一个GetValue的方法,一个Value属性,我们可以通过方法的名称来得到方法并且调用之,如:

    //获取类型信息
    Type t = Type.GetType("TestSpace.TestClass");
    //构造器的参数
    object[] constuctParms = new object[]{"timmy"};
    //根据类型创建对象
    object dObj = Activator.CreateInstance(t,constuctParms);
    //获取方法的信息
    MethodInfo method = t.GetMethod("GetValue");
    //调用方法的一些标志位,这里的含义是Public并且是实例方法,这也是默认的值
    BindingFlags flag = BindingFlags.Public | BindingFlags.Instance;
    //GetValue方法的参数
    object[] parameters = new object[]{"Hello"};
    //调用方法,用一个object接收返回值
    object returnValue = method.Invoke(dObj,flag,Type.DefaultBinder,parameters,null);

    属性与方法的调用大同小异,大家也可以参考MSDN

    7、动态创建委托
    委托是C#中实现事件的基础,有时候不可避免的要动态的创建委托,实际上委托也是一种类型:System.Delegate,所有的委托都是从这个类派生的
    System.Delegate提供了一些静态方法来动态创建一个委托,比如一个委托:

    namespace TestSpace {
    delegate string TestDelegate(string value);
    public class TestClass {
    public TestClass() {
    }
    public void GetValue(string value) {
    return value;
    }
    }
    }

    使用示例:
    TestClass obj = new TestClass();

    //获取类型,实际上这里也可以直接用typeof来获取类型
    Type t = Type.GetType(“TestSpace.TestClass”);
    //创建代理,传入类型、创建代理的对象以及方法名称
    TestDelegate method = (TestDelegate)Delegate.CreateDelegate(t,obj,”GetValue”);

    String returnValue = method(“hello”); 
  • 相关阅读:
    对象的强、软、弱和虚引用
    spark运行模式之一:Spark的local模式安装部署
    Spark Tungsten in-heap / off-heap 内存管理机制--待整理
    sparkContext之一:sparkContext的初始化分析
    mysql分区表之四:分区表性能
    服务的升级和降级
    怎样编写高质量的 Java 代码
    Java之代理(jdk静态代理,jdk动态代理,cglib动态代理,aop,aspectj)
    Spark Streaming之六:Transformations 普通的转换操作
    Spark Streaming之五:Window窗体相关操作
  • 原文地址:https://www.cnblogs.com/skyblue/p/821268.html
Copyright © 2020-2023  润新知