ArticleDirectory
- Reflection in. net
- Check type and members
- Dynamic call code
- Combined
- Efficient serialization type Processing
Is the clear componentization goal lost by sharing too many types of information between libraries? You may need efficient and strongly typed data storage, but if you need to update your database architecture after every development of the object model, it will be very costly, so are you more willing to deduce its type architecture during runtime? Do you need to deliver components that can accept any user object and process them in an intelligent way? Do you want library callers to show you their types programmatically?
If you find that you are struggling to maintain a strongly typed data structure while looking to maximize runtime flexibility, you may be willing to consider reflection and how it improves your software. In this column, I will discuss MicrosoftThe system. Reflection namespace in. NET Framework and how it helps your development experience. I will start with some simple examples and finally explain how to deal with serialization in the real world. In this process, I will show how reflection and codedom work together to effectively process runtime data.
Before in-depth exploration of system. reflection, I would like to discuss general reflection programming first. First, reflection can be defined as any function provided by a programming system.ProgramPersonnel can check and operate without having to know their identity or formal structure in advanceCodeEntity. This part contains a lot of content. I will describe it one by one.
First, what does reflection provide? What can you do with it? I tend to divide a typical reflection-centered task into two types: Check and operation. Check the objects and types to be analyzed to collect structured information about their definitions and behaviors. Except for some basic provisions, this is usually done without prior knowledge of them. (For example, in. NET Framework, everything inherits from system. object, and an object type reference is usually the general starting point of reflection .)
The operation dynamically calls the code by checking the collected information, creates a new instance of the discovered type, or even easily restructures the type and object dynamically. One important point to be pointed out is that for most systems, the operation types and objects during runtime are comparedSource codeIf you perform the same operation statically, the performance will be reduced. Because of the dynamic characteristics of reflection, this is a necessary trade-off, but there are a lot of tips and best practices to optimize the performance of reflection (more in-depth information on optimizing the use of reflection, see msdn.microsoft.com/msdnmag/issues/05/07/reflection ).
So what is the goal of reflection? What are the actual checks and operations of programmers? In my definition of reflection, I used the new term "code entity" to emphasize the fact that, from the programmer's perspective, reflection technology sometimes blur the boundaries between traditional objects and types. For example, a typical reflection-centric task may be:
- Starts from the handle of object O, and obtains the handle of its related definition (type T) Using Reflection.
- Check type T to obtain the handle of its method M.
- Call method M of another object o' (also type T.
Please note that I am moving from an instance to its underlying type, from this type to a method, then, use the handle of this method to call it on another instance-obviously this is not implemented by using the traditional C # Programming Technology in the source code. After discussing system. Reflection of. NET Framework, I will explain this situation through a specific example.
SomeProgramming LanguageYou can use the syntax to provide reflection, while other platforms and frameworks (such as. NET Framework) use it as the system library. Regardless of the method in which reflection is provided, the possibility of using reflection technology in a given situation is quite complex. The ability of a programming system to provide reflection depends on a number of factors: Do programmers use the functions of programming languages to express their concepts? Does the compiler embed enough structured information (metadata) in the output to facilitate future interpretation? Is there a runtime subsystem or host interpreter to digest the metadata? Does the platform library display this interpretation result in a useful way for programmers?
If you think of a complex, object-oriented system, but the code shows a simple, C-style function, and there is no formal data structure, obviously, your program cannot dynamically infer that the pointer of a variable V1 points to an object instance of a certain type T. After all, type T is a concept in your mind and never appears explicitly in your programming statements. However, if you use a more flexible object-oriented language (such as C #) to express the abstract structure of a program and directly introduce the concept of type T, then the compiler will convert your idea into a form that can be understood by appropriate logic in the future, just like the Common Language Runtime (CLR) or a dynamic language interpreter provides the same.
Is reflection completely dynamic and runtime technology? In short, this is not the case. Reflection is often available and useful to developers throughout the development and execution cycle. Some programming languages are implemented through independent compilers, which directly convert advanced code into commands that can be recognized by machines. The output file only contains compiled input, and does not support the logic used to accept opaque objects and dynamically analyze their definitions at runtime. This is exactly the case for many traditional C compilers. Because almost no logic is supported in the target executable file, you cannot perform much dynamic reflection. However, the compiler will provide static reflection from time to time-for example, the commonly used typeof operator allows programmers to check type identifiers during compilation.
Another completely different scenario is that an interpreted programming language is always executed through the main process (the scripting language generally belongs to this type ). Because the complete definition of the program is available (as the input source code) and combined with the complete language implementation (as the interpreter itself ), therefore, all technologies required to support self-analysis are in place. This dynamic language frequently provides comprehensive reflection capabilities, as well as a rich set of tools for dynamic analysis and operation procedures.
. NET Framework Clr and its bearer language such as C # are intermediate forms. The compiler is used to convert source code into Il and metadata. Although the latter is lower-level or less "logical" than the source code, it still retains a lot of abstract structure and type information. Once the CLR starts and carries this program, the system. Reflection library of the base class library (BCL) can use this information and return information about the object type, type member, and member signature. In addition, it supports calling, including later binding calls.
Reflection in. net
To use reflection for programming with. NET Framework, you can use the system. Reflection namespace. This namespace provides classes that encapsulate many runtime concepts, such as assemblies, modules, types, methods, constructors, fields, and attributes. The table in Figure 1 shows how the classes in system. Reflection correspond to the corresponding items when the concept is running.
Figure 1 system. Reflection class
Language Components |
Corresponding. Net class |
Assembly |
System. reflection. Assembly |
Module |
System. reflection. Module |
Abstract Member |
System. reflection. memberinfo (all of the following base classes) |
Type |
System. Type |
Attribute |
System. reflection. propertyinfo |
Field |
System. reflection. fieldinfo |
Event |
System. reflection. eventinfo |
Abstract Method |
System. reflection. methodbase (all of the following base classes) |
Method |
System. reflection. methodinfo |
Constructor |
System. reflection. constructorinfo |
Although important, system. reflection. Assembly and system. reflection. module are mainly used to locate new code and load it to runtime. In this column, I will not discuss these parts for the moment, and assume that all the relevant code has been loaded.
To check and operate on loaded code, the typical mode is system. type. Generally, you get a system. Type instance of the runtime type of interest (through object. GetType ). Then you can use various methods of system. type to explore the definition of the type in system. Reflection and obtain instances of other classes. For example, if you are interested in a specific method and want to obtain a system. reflection. methodinfo instance for this method (possibly through type. getmethod ). Similarly, if you are interested in a field and want to obtain a system. reflection. fieldinfo instance for this field (possibly through type. getfield ).
Once you get all the necessary reflection instance objects, you can continue to follow the inspection or operation steps as needed. During the check, you use various descriptive attributes in the reflection class to obtain the information you need (is this a common type? Is this an instance method ?). You can call and execute Methods dynamically, create new objects by calling constructor, and so on.
Check type and members
Let's jump to some code and explore how to use basic reflection for inspection. I will focus on Type Analysis. Starting from an object, I will retrieve its type and then examine several interesting members (see figure 2 ).
Figure 2 search object types and members
Code Using system; using system. reflection; // use reflection to enumerate some basic properties of a type... namespace example1 {class myclass {private int myfield = 0; Public void mymethod1 () {return;} public int mymethod2 (int I) {return I ;} public int myproperty {get {return myfield ;}} class program {static void main (string [] ARGs) {console. writeline ("Reflection demo Example 1"); myclass MC = new myclass (); Type T = MC. getType (); console. writeline ("type name: {0}", T. name); foreach (methodinfo m in T. getmethods () console. writeline ("method name: {0}", M. name); foreach (propertyinfo P in T. getproperties () console. writeline ("property name: {0}", p. name );}}}
Output Reflection demo Example 1 type name: myclassmethod name: mymethod1method name: mymethod2method name: get_mypropertymethod name: gettypemethod name: tostringmethod name: descrismethod name: gethashcodeproperty name: myproperty
|
The first thing to note is that in the class definition, the length of the method is much larger than I expected. Where do these additional methods come from? Anyone proficient in the. NET Framework object hierarchy will recognize these methods inherited from the generic base class object itself. (In fact, I first used object. GetType to retrieve its type .) In addition, you can see the getter function of the attribute. What should I do if you only need functions explicitly defined by myclass? In other words, how do you hide inherited functions? Or do you only need to explicitly define Instance functions?
Watch msdn onlineYou will find that you are willing to use the second getmethods overload method, which accepts the bindingflags parameter. By combining different values from the bindingflags enumeration, you can let the function only return the required method subset. Replace the getmethods call:
Getmethods (bindingflags. instance | bindingflags. declaredonly | bindingflags. Public)
The result is that you get the following output (note that there are no static helper functions and functions inherited from system. object ).
Reflection demo Example 1 type name: myclassmethod name: mymethod1method name: mymethod2method name: get_mypropertyproperty name: myproperty
What if you know the type name (fully qualified) and member in advance? How do you convert an enumeration type to a retrieval type? The example in Figure 3 shows how to use the string text describing the type information through object. GetType and type. getmethod to retrieve the corresponding items of the actual code. With the code in the first two examples, you have the basic components that can implement the primitive browser. You can find a runtime object by name, and then enumerate its various related attributes.
Figure 3: String retrieval type and methodinfo
Using system; using system. reflection; // use reflection to retrieve references to a type and method via namenamespace example2 {class myclass {public void mymethod1 () {return;} public void mymethod2 () {return ;}} class program {static void main (string [] ARGs) {console. writeline ("reflection demo Example 2"); // note that we must use the fully qualified name... type T = type. getType ("example2.myclass"); methodinfo M = T. getmethod ("mymethod1"); console. writeline ("type name: {0}", T. name); console. writeline ("method name: {0}", M. name );}}} |
Dynamic call code
So far, I have obtained the handle (such as type and method) of runtime objects for descriptive purposes only, such as output their names. But how can we do more? How do I actually call a method? Figure 4 shows how to obtain the methodinfo of a type of member and then use methodinfo. invoke to dynamically call this method.
Figure 4 dynamic call Method
using system; using system. reflection; // use reflection to retrieve a methodinfo for an // instance method and invoke it upon jsonobject instancesnamespace example3 {class myclass {private int id =-1; public myclass (int id) {This. id = ID;} public void mymethod2 (Object p) {console. writeline ("mymethod2 is being invoked on Object with" + "ID {0} with parameter {1 }... ", id. tostring (), P. tostring () ;}} class program {static void main (string [] ARGs) {console. writeline ("reflection demo Example 3"); myclass mc1 = new myclass (1); myclass MC2 = new myclass (2); Type T = mc1.gettype (); methodinfo method = T. getmethod ("mymethod2"); For (INT I = 1; I <= 5; I ++) method. invoke (MC2, new object [] {I}) ;}} |
The main points of this example are: first, retrieve a system. Type instance from a myclass, mc1 instance, and then retrieve a methodinfo instance from this type. Finally, when methodinfo is called, it is passed as the first parameter of the call and bound to another myclass (MC2) instance.
As mentioned above, for the difference between the type and object usage that you expect to see in the source code, this example blur the difference. Logically, you retrieve the handle of a method and call this method, just as it belongs to a different object. This may be easy for programmers who are familiar with functional programming languages, but for programmers who are only familiar with C #, separating object implementation and Object Instantiation may not be so intuitive.
Combined
Now I have discussed the basic principles of checking and calling. Next I will combine them with specific examples. Imagine that you want to deliver a library with a static helper function that must process objects. However, you have no idea about the types of these objects during design! This depends on the instruction of the function caller and how he wants to extract meaningful information from these objects. The function accepts an object set and a string descriptor of a method. It then traverses the set, calls the methods of each object, and aggregates the returned values with some functions (see figure 5 ).
Figure 5 extract information from the object
Using system; using system. collections. generic; using system. reflection; namespace example4 {class program {static void main (string [] ARGs) {// prepare some objects for our function to process object [] objs = new object [] {New intreturner (1), new intreturner (2), new intreturner (3 ), new sonofintreturner (4), new sonofintreturner (5), new sonofintreturner (6),}; console. writeline ("attempting to comput E average, "+" passing in array with {0} elements. ", objs. length); int average = computeaverage (objs, "getinteger"); console. writeline ("found an average of {0 }! ", Average);} public static int computeaverage (ienumerable <Object> objs, string methodname) {int sum = 0, Count = 0; Type firsttype = NULL; methodinfo firstmethod = NULL; foreach (Object o in objs) {If (firstmethod = NULL) {firsttype = O. getType (); firstmethod = firsttype. getmethod (methodname);} sum + = (INT) firstmethod. invoke (O, null); count ++;} // note that we use integer division here (not Floating Point) if (COUNT = 0) return 0; return sum/count ;}} class intreturner {protected int value =-1; Public intreturner (int I) {value = I;} Public Virtual int getinteger () {console. writeline ("getinteger called on instance of intreturner," I'm returning {0 }! ", Value); Return Value ;}} class sonofintreturner: intreturner {public sonofintreturner (int I): Base (I) {} public override int getinteger () {console. writeline ("getinteger called on instance of sonofintreturner," I'm returning {0 }! ", This. value); Return Value ;}} class enemyofintreturner {protected int value =-1; Public enemyofintreturner (int I) {value = I;} Public Virtual int getinteger () {console. writeline ("getinteger called on instance of enemyofintreturner," "I'm returning {0 }! ", Value); Return Value ;}}} |
In this example, I want to declare some constraints. First, the string parameter description method (which must be implemented by the underlying type of each object) does not accept any parameters and returns an integer. The Code traverses the object set, calls the specified method, and gradually calculates the average value of all values. Finally, because this is not a production code, I don't have to worry about parameter verification or integer overflow during summation.
When browsing the sample code, we can see that the protocol between the main function and the static helper computeaverage does not depend on any type information except the general base class of the object itself. In other words, you can completely change the type and structure of the object being transferred, but as long as you can always use a string to describe a method and return an integer, computeaverage can work normally!
A key issue to be noted is related to methodinfo (general reflection) hidden in the last example. Note: In the foreach loop of computeaverage, the Code captures only one methodinfo from the first object in the set, and then binds it to call all subsequent objects. As shown in the code, it runs well-this is a simple example of methodinfo caching. However, there is a fundamental limitation here. A methodinfo instance can only be called by instances of the same level of objects it retrieves. This operation can be performed only when instances of intreturner and sonofintreturner (inherited from intreturner) are passed in.
The sample code already contains the class named enemyofintreturner, which implements the same basic protocol as the other two classes, but does not share any common sharing types. In other words, this interface is logically equivalent, but does not overlap at the type level. To explore the use of methodinfo in this case, try to add other objects to the set, get an instance through "New enemyofintreturner (10)", and run the example again. You may encounter an exception stating that methodinfo cannot be used to call the specified object because it is completely irrelevant to the original type when obtaining methodinfo (even if the method name is equivalent to the basic protocol ). To make your code reach the production level, you must be prepared for this situation.
One possible solution is to analyze the types of all input objects by yourself and retain the explanations of the shared type level (if any. If the type of the next object is different from that of any known type, you need to obtain and store a new methodinfo. Another solution is to capture targetexception and obtain a new methodinfo instance. The two solutions mentioned here have their own advantages and disadvantages. Joel pobar wrote an excellent article about the methodinfo buffer and the reflection performance I strongly recommend for the 2007 issue.
If you want to add reflection to an application or framework in this example, you can add more flexibility for future customization or scalability. It is undeniable that reflection may be cumbersome compared to the equivalent logic in the local programming language. If you feel that for you or your customers, adding reflection-based post-binding to code is too troublesome (after all, they need to describe their types and code in some way in your framework ), you may only need moderate flexibility to strike a balance.
Efficient serialization type Processing
Now we have explained the basic principles of. Net reflection through several examples. Let's take a look at the situation in the real world. If your software interacts with other systems through web services or remote technology outside of other processes, you may have encountered serialization problems. Serialization is essentially to convert an active, memory-occupied object into a data format suitable for online transmission or disk storage.
.. NET Framework. XML. the serialization namespace provides a powerful serialization engine with xmlserializer. It can use any hosted object and convert it to XML (XML data can also be converted back to typed object instances in the future, this process is called deserialization ). The xmlserializer class is a powerful enterprise-ready software snippet. If you are faced with serialization problems in your project, it will be your first choice. But for the purpose of teaching, we will discuss how to implement serialization (or other similar runtime type processing instances ).
Imagine: You are delivering a framework that uses any user type object instance and converts it to a certain smart data format. For example, assume that there is a resident memory object with the address type as follows:
(Pseudo code) Class address {addressid ID; string Street, city; statetype state; zipcodetype zipcode ;}
How to generate appropriate data representation for future use? A simple text Presentation may solve this problem:
Address: 123 Street: 1 Microsoft way city: Redmond state: wa ZIP: 98052
If you fully understand the formal data types to be converted (for example, when writing your own code) in advance, the process becomes very simple:
Foreach (address a in Addresslist) {console. writeline ("Address: {0}",. ID); console. writeline ("\ tstreet: {0}",. street );... // and so on}
However, if you do not know the data type that you will encounter during running, the situation will become very interesting. How do you compile general framework code like this?
Myframework. translateobject (Object input, myoutputwriter output)
First, you need to determine which types of members are useful for serialization. Possible scenarios include capturing only specific types of members, such as primitive system types, or providing a mechanism for Type authors to explain which members need to be serialized, for example, use custom attributes as tags on type members ). You can only capture members of specific types, such as primitive system types, or the type Author can explain which members need to be serialized (the possible method is to use custom attributes as tags on the Type members ).
Once you have logged the members of the data structure to be converted, you need to write the logic to enumerate and retrieve them from the input objects. Reflection carries on a heavy task here, allowing you to query both the data structure and data values.
For simplicity, we design a lightweight conversion engine, get an object, get all its public attribute values, convert them to strings by directly calling tostring, and then serialize these values. For a given object named "input,AlgorithmIt is roughly as follows:
- Call input. GetType to retrieve the system. Type instance. This instance describes the underlying structure of input.
- Use type. getproperties and the appropriate bindingflags parameters to retrieve the public attributes as propertyinfo instances.
- Use propertyinfo. Name and propertyinfo. getvalue to retrieve attributes as key-value pairs.
- Call object. tostring on each value to convert it (in the basic mode) to the string format.
- Pack the object type name, attribute name, and string value set into the correct serialization format.
This algorithm obviously simplifies things and also captures the essentials of getting the runtime data structure and converting it into self-descriptive data. But here is a problem: performance. As mentioned earlier, reflection costs a lot for type processing and value retrieval. In this example, I perform a complete Type Analysis in each provided instance.
If you can capture or retain your understanding of the type structure in some way, you can easily retrieve it in the future and effectively process new instances of this type; in other words, step #3 in the example Algorithm? The good news is that using the features in the. NET Framework is completely possible. Once you understand the data structure of the type, you can use codedom to dynamically generate code bound to the data structure. You can generate a helper Assembly that contains the helper class and a method that references the input type and directly accesses its attributes (similar to any other attribute in the managed code ), therefore, type checks only affect the performance once.
Now I will revise this algorithm. New Type:
- Obtain the system. Type instance corresponding to this type.
- Use various system. Type accessors to retrieve architectures (or at least retrieve a subset of architectures useful for serialization), such as attribute names and field names.
- Use schema information to generate a helper assembly (through codedom) that is linked to the new type and processes instances effectively.
- Use code in the Helper Program to extract instance data.
- Serialize data as needed.
For all input data of the given type, you can jump forward to step #4. This can greatly improve the performance compared to explicitly checking each instance.
I developed a basic serialization library named simpleserialization, which implements this Algorithm Using Reflection and codedom (which can be downloaded in this column. The main component is a class named simpleserializer, which is constructed by a system. Type instance. In the constructor, the new simpleserializer instance analyzes the given type and uses the helper class to generate a temporary assembly. The helper class is closely bound to the given data type, and the processing method for the instance is as if you have fully understood the type before writing the code.
The simpleserializer class has the following layout:
Class simpleserializer {public class simpleserializer (type datatype); Public void serialize (Object input, simpledatawriter writer );}
Simple and amazing! The constructor undertakes the heaviest task: it uses reflection to analyze the type structure and uses codedom to generate a helper assembly. The simpledatawriter class is only a data receiver used to clarify common serialization modes.
To serialize a simple address class instance, use the following pseudo code to complete the task:
Simpleserializer myserializer = new simpleserializer (typeof (Address); simpledatawriter writer = new simpledatawriter (); myserializer. serialize (addressinstance, writer );
End
We strongly recommend that you try the sample code yourself, especially the simpleserialization library. I added comments to some interesting parts of simpleserializer, hoping to help. Of course, if you need to strictly serialize the product code, you must rely on the technology provided in. NET Framework (such as xmlserializer ). However, if you find that you need to use any type of data at runtime and can efficiently process them, I hope you will use my simpleserialization library as your own solution.
We are grateful for the guidance and feedback from CLR developers weitao SU (reflection) and Pete sheill (codedom.