Are clear component goals frustrated by sharing too many types of information between libraries? Perhaps you need efficient, strongly typed data storage, but if you need to update your database schema every time the object model is developed, it can be costly, so would you prefer to infer its type schema at run time? Do you need to deliver components that accept arbitrary user objects and process them in some intelligent way? Do you want the caller of the library to be able to programmatically explain their type to you?
If you find yourself struggling to maintain a strongly typed data structure while looking to maximize run-time flexibility, you'll probably be willing to consider reflection and how it can improve your software. In this column, I'll explore the System.Reflection namespaces in the Microsoft. NET Framework and how it can help your development experience. I'll start with a few simple examples and end with a list of how to handle the real-world serialization situation. In this process, I show how reflection and CodeDom work together to effectively handle run-time data.
Before delving into the System.Reflection, I would like to discuss general reflection programming first. First, reflection can be defined as any functionality provided by a programming system that enables programmers to examine and manipulate code entities without having to know their identity or formal structure in advance. This part is a lot of content, I will expand the description.
First, what does reflection offer? What can you do with it? I tend to divide typical reflection-centric tasks into two categories: check and operate. Examine the objects and types that need to be parsed to gather structured information about their definitions and behavior. In addition to some basic provisions, this is usually done without prior knowledge of them. (for example, in the. NET Framework, everything inherits from System.Object, and a reference to an object type is usually a general starting point for reflection.) )
Operations use dynamically calling code by examining the information gathered, creating new instances of discovered types, or even easily dynamically restructuring types and objects. One important point to point out is that for most systems, manipulating types and objects at run time can result in slower performance than static equivalent operations in the source code. This is a necessary trade-off due to the dynamic nature of the reflection, but there are many techniques and best practices to optimize the performance of reflection.
So, what is the target of reflection? What does the programmer actually check and manipulate? In my definition of reflection, I used the new term "code entity" to emphasize the fact that, from the programmer's point of view, reflection technology sometimes blurs the boundaries between traditional objects and types. For example, a typical reflection-centric task might be:
Starts with the handle of the object O and uses reflection to obtain a handle to its related definition (type T).
Check the type T to get the handle of its method M.
Method M that invokes another object O ' (same type T).
Notice that I am moving from one instance to its underlying type, from this type to a method, and then using the handle of this method to use it on another instance-obviously this is not possible with traditional C # programming techniques in the source code. After exploring the system.reflection of the. NET Framework, I will explain the situation again with a specific example.
Some programming languages themselves provide reflection through syntax, while other platforms and frameworks (such as the. NET framework) Use it as a system library. Regardless of how the reflection is provided, the likelihood of using reflection techniques in a given situation is quite complex. The ability of a programming system to provide reflection depends on a number of factors: Does the programmer make good use of the functionality of the programming language to express his concepts? Does the compiler embed enough structured information (metadata) in the output to facilitate future interpretation? Is there a run-time subsystem or host interpreter to digest the metadata? Does the platform library show the results of this interpretation in a way that is useful to programmers?
If your mind is imagining a complex, object-oriented type of system, but in the code is a simple, C-language style of functions, and there is no formal data structure, then obviously your program can not dynamically infer that a variable v1 pointer to a type T object instance. Because after all, type T is the concept in your mind, and it never appears explicitly in your programming statements. But if you use a more flexible object-oriented language, such as C #, to express the abstract structure of a program, and introduce the concept of type T directly, the compiler converts your idea into a form that can be understood later in the appropriate logic, just as the common language runtime (CLR) Or some kind of dynamic language interpreter provides.
Is reflection completely dynamic, run-time technology? Simply put, it's not. Throughout the development and execution cycle, reflection is available and useful to developers in many cases. Some programming languages are implemented by a standalone compiler that converts advanced code directly to instructions that the machine can recognize. The output file includes only compiled input, and the runtime has no support logic to accept opaque objects and dynamically parse their definitions. This is the case for many traditional C compilers. Because there is little support logic in the target executable, you cannot perform too many dynamic reflections, while the compiler provides static reflection from time-for example, the universally used typeof operator allows programmers to check the type identity at compile time.
Another completely different scenario is that the interpretive programming language is always executed through the main process (the scripting language usually belongs to this category). Because the complete definition of a program is available (as input source code) and combined with a complete language implementation (as the interpreter itself), all the technologies needed to support the self-assessment are in place. This dynamic language frequently provides comprehensive reflection and a set of rich tools for dynamic analysis and operating procedures.
The. NET Framework CLR and its hosting language, such as C #, are in the middle form. The compiler is used to convert the source code to IL and metadata, which is lower or less "logical" than the source code, but still retains a lot of abstract structure and type information. Once the CLR starts and hosts this program, the System.Reflection library of the base Class library (BCL) can use this information and return information about object types, type members, member signatures, and so on. In addition, it can also support calls, including late-bound calls.
reflection in. NET
To use reflection when you are programming with the. NET Framework, you can use the System.Reflection namespace. This namespace provides classes that encapsulate a number of run-time concepts, such as assemblies, modules, types, methods, constructors, fields, and properties. The table in Figure 1 shows how the classes in System.Reflection correspond to the corresponding items in the conceptual runtime.
Although important, System.Reflection.Assembly and System.Reflection.Module are primarily used to locate new code and load it into the runtime. In this column, I'm not talking about these parts, and assume all the relevant code is loaded.
To check and manipulate the loaded code, the typical pattern is mainly system.type. Typically, you start with a System.Type instance of a focused Run-time category (via Object.GetType). You can then use the various methods of System.Type to explore the definition of the type in System.Reflection and get an instance of the other class. For example, if you are interested in a particular method and want to obtain a System.Reflection.MethodInfo instance of this method (possibly through Type.getmethod). Similarly, if you are interested in a field and want to get a System.Reflection.FieldInfo instance of this field (possibly through Type.getfield).
Once you have all the necessary reflection instance objects, you can follow the steps of the check or action as needed to continue. When checking, you use a variety of descriptive attributes in your reflection class to get the information you need (is this a common type?). Is this an instance method? )。 Action, you can dynamically call and Execute methods, create new objects by calling the constructor, and so on.
checking types and members
Let's jump to some of the code and explore how to use basic reflection for checking. I'll focus on type analysis. Starting with an object, I'll retrieve its type and then examine a few interesting members.
The first thing to note is that in the class definition, at first glance it appears that the method is much more space than I expected. Where do these extra methods come from? Anyone who is proficient in the. NET Framework object hierarchy will recognize these methods inherited from the universal base class object itself. (In fact, I first used the Object.GetType to retrieve its type.) In addition, you can see the Getter function for the property. Now, what if you just need to MyClass functions that you explicitly define? In other words, how do you hide inherited functions? Or maybe you just need an explicitly defined instance function?
If you look at MSDN online, you'll find that everyone is willing to use the GetMethods second overloaded method, which accepts BindingFlags parameters. By combining different values from the BindingFlags enumeration, you can have the function return only the subset of methods that you want. Replace the GetMethods call, replacing it with:
GetMethods (BindingFlags.Instance | BindingFlags.DeclaredOnly | BindingFlags.Public)
As a result, you get the following output (note that there are no static helper functions and inherited functions from System.Object).
The following are the referenced contents:
Reflection Demo Example 1 Type Name:myclass Method Name:mymethod1 Method Name:mymethod2 Method Name:get_myproperty Property Name:myproperty |
What if you knew the type name (fully qualified) and the member in advance? How do you complete a transformation from an enumerated type to a retrieval type? With the code in the first two examples, you already have the basic components that enable you to implement a primitive class browser. By name you can find a run-time entity, and then enumerate its various related properties.
Dynamic Calling code
So far, I've gotten the handles of Run-time objects, such as types and methods, for descriptive purposes only, such as outputting their names. But how do you do more? How do you actually call a method?
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 that type. Finally, when MethodInfo is invoked, it is bound to another MyClass (MC2) instance by passing it as the first argument of the call.
As mentioned earlier, this example blurs the distinction between the type and object usage that you expect to see in your source code. Logically, you retrieve a handle to a method, and then call the method as if it belonged to a different object. This may be easy for programmers familiar with functional programming languages, but for programmers who are familiar with C #, separating object implementations and object instantiations may be less intuitive.
grouped together
Now that I've explored the basics of checking and calling, I'll put them together in concrete examples. Imagine that you want to deliver a library with a static helper function that must handle the object. But at design time, you have no concept of the types of these objects! This depends on the function caller's instructions to see how he wants to extract meaningful information from these objects. function will accept a collection of objects, and a string descriptor for a method. It then iterates through the collection, calls each object's method, and returns the value with some function aggregation.
For this example, I want to declare some constraints. First, the method described by the string parameter (which must be implemented by the underlying type of each object) will not accept any arguments and will return an integer. The code iterates through the collection of objects, calls the specified method, and calculates the average of all the values gradually. Finally, because this is not a production code, I don't have to worry about parameter validation or integer overflow when summing.
As you browse through the sample code, you can see that the protocol between the primary function and the static helper Computeaverage does not rely on any type of information except for the common base class of the object itself. In other words, you can completely change the type and structure of the object you are transferring, but as long as you can always use a string to describe a method, and the method returns an integer, the computeaverage can work correctly!
A key issue to note is related to the MethodInfo (General reflection) hidden in the last example. Note that in the Computeaverage foreach Loop, the code crawls only one MethodInfo from the first object in the collection and then binds the call for all subsequent objects. As the code shows, it works well-this is a simple example of the MethodInfo cache. But there is a fundamental limitation here. The MethodInfo instance can only be invoked by it to retrieve an instance of an object of the same level type. This can be done because an instance of Intreturner and Sonofintreturner (inherited from Intreturner) is passed in.
In the example code, a class named Enemyofintreturner was already included, which implements the same basic protocol as the other two classes, but does not share any common shared types. In other words, the interface is logically equivalent, but there is no overlap at the type level. To explore the use of MethodInfo in this case, try adding other objects to the collection, get an instance through "new Enemyofintreturner (10)", and run the example again. You will encounter an exception stating that MethodInfo cannot be used to invoke the specified object because it has nothing to do with the original type when the MethodInfo is obtained (even if the method name and base protocol are equivalent). To get your code up to production level, you need to be prepared for the situation.
One possible solution would be to analyze the types of all incoming objects by themselves, preserving the interpretation of the type hierarchy (if any) that they share. If the type of the next object is different from any known type hierarchy, you need to acquire and store a new MethodInfo. Another solution is to capture the targetexception and retrieve a MethodInfo instance again. Both of the solutions mentioned here have their pros and cons. Joel Pobar has written an excellent article about the MethodInfo buffer and the reflective performance I strongly recommend for this magazine's 20,075-month period.
You want this example to demonstrate adding reflection to an application or framework that can add more flexibility for future customizations or extensibility. It is undeniable that using reflection can be tedious compared to the equivalent logic in native programming languages. If you feel that it is too cumbersome for you or your customers to add late binding based on reflection to your code (after all, they need some way to explain their type and code in your framework), then you may need only modest flexibility to achieve some sort of balance.
efficient type handling for serialization
Now that we've covered the basics of. NET reflection through several examples, let's take a look at the real-world situation. If your software interacts with other systems through WEB services or other out-of-process remote technologies, you are likely to have encountered serialization problems. Serialization essentially transforms an active, memory-consuming object into a data format suitable for online transmission or disk storage.
The System.Xml.Serialization namespaces in the. NET Framework provide a powerful serialization engine with XmlSerializer, which can use arbitrary managed objects and convert them to XML (XML Data is converted back to a typed object instance, a process called deserialization. The XmlSerializer class is a powerful, enterprise-ready piece of software that will be your first choice if you are faced with serialization issues in your project. But for teaching purposes, let's explore how to implement serialization (or other similar run-time type processing instances).
Scenario: You're delivering a framework that uses an object instance of any user type and converts it to some kind of smart data format. For example, suppose you have an object that resides in memory, and the type is the address shown below:
The following are the referenced contents:
(pseudocode) Class Address { ADDRESSID ID; String Street, City; Statetype State; Zipcodetype ZipCode; } |
How do I generate the appropriate data representation for later use? Perhaps a simple text rendering will solve this problem:
The following are the referenced contents:
Address:123 Street:1 Microsoft Way City:redmond State:wa zip:98052 |
If you fully understand the formal data types that you need to transform (for example, when you write your own code), things can be very simple:
The following are the referenced contents:
foreach (Address A in AddressList) { Console.WriteLine ("Address:{0}", a.id); Console.WriteLine ("\tstreet:{0}", A.street); ...//And so on } |
However, it can be very interesting if you do not know beforehand what type of data you will encounter at run time. How do you write generic framework code like this?
Myframework.translateobject (object input, Myoutputwriter output)
First, you need to decide which types of members are useful for serialization. Possible scenarios include capturing only certain types of members, such as primitive system types, or providing a mechanism for type authors to describe which members need to be serialized, such as using custom attributes as tokens on type members. You can only capture a specific type of member, such as a primitive system type, or a type author can explain which members need to be serialized (possible by using a custom attribute on a type member as a token).
Once you have a clear record of the data structure members that need to be converted, you then need to write logic that enumerates and retrieves them from the incoming objects. Reflection has a heavy workload here, allowing you to query data structures and query the values.
For simplicity's sake, let's design a lightweight conversion engine, get an object, get all its public property values, convert them to strings by calling ToString directly, and then serialize the values. For a given object named "Input", the algorithm is roughly as follows:
Call input. GetType to retrieve the System.Type instance, which describes the underlying structure of input.
Using type.getproperties and appropriate BindingFlags parameters, the public property is retrieved as a PropertyInfo instance.
Using Propertyinfo.name and Propertyinfo.getvalue, properties are retrieved as key-value pairs.
Call object.tostring on each value to convert it (by basic means) to string format.
Package the name and property name of the object type, and the collection of string values into the correct serialization format.
This algorithm obviously simplifies things and also captures the gist of getting the run-time data structures and translating them into self-describing data. But here's a problem: performance. As mentioned earlier, reflection is expensive for both type processing and value retrieval. In this example, I perform a complete type analysis in an instance of each of the provided types.
If you can capture or retain your understanding of a type structure in some way so that you do not have to easily retrieve it later, and effectively handle a new instance of that type, in other words, step forward to the example algorithm #3? The good news is that leveraging the functionality in the. NET Framework is entirely possible. Once you understand the data structure of a type, you can use CodeDom to dynamically generate code that is bound to that structure. You can build a helper assembly that contains helper classes and methods that reference incoming types and directly access their properties (similar to any other property in managed code), so type checking can only have an impact on performance.
Now I'm going to fix this algorithm. New type:
Gets the System.Type instance corresponding to the type.
Use various System.Type accessors to retrieve the schema (or at least to retrieve a subset of the schemas that are useful for serialization), such as property names, field names, and so on.
Generates a helper assembly (through CodeDom) using schema information, which is linked to the new type and effectively handles the instance.
Use code in the helper assembly to extract the instance data.
Serialize the data as needed.
For all incoming data of a given type, you can skip forward to step #4, which can be a huge performance boost compared to explicitly checking each instance.
I developed a basic serialization library called Simpleserialization, which implements this algorithm with reflection and CodeDom (downloadable in this column). The main component is a class called Simpleserializer, which is constructed by a user with a System.Type instance. In the constructor, the new Simpleserializer instance analyzes the given type and generates a temporary assembly using the helper class. The helper class is tightly bound to the given data type, and the instance is handled in the same way as if you were writing the code in a completely prior understanding of the type.
The following are the referenced contents:
The Simpleserializer class has the following layout: Class Simpleserializer { public class Simpleserializer (Type dataType); public void Serialize (object input, simpledatawriter writer); } |
Simply stunning! The constructor takes on the most onerous task: it uses reflection to parse the type structure and then generates helper assemblies with CodeDom. The Simpledatawriter class is just a data sink used to clarify common serialization patterns.
To serialize a simple instance of the address class, complete the task with the following pseudocode:
The following are the referenced contents:
Simpleserializer myserializer = new Simpleserializer (typeof (Address)); Simpledatawriter writer = new Simpledatawriter (); Myserializer.serialize (addressinstance, writer); |
End
It is highly recommended that you try the sample code yourself, especially the Simpleserialization library. I've added notes to some of the interesting parts of Simpleserializer, hoping to help. Of course, if you need to make strict serialization in your product code, you do rely on the technologies provided in the. NET Framework, such as XmlSerializer. But if you find that you need to use any type at run time and handle them efficiently, I want you to use my simpleserialization library as your own solution.