- Mastering Dart
- Sergey Akopkokhyants
- 1288字
- 2021-08-05 17:19:20
Reflection
Introspection is the ability of a program to discover and use its own structure. Reflection is the ability of a program to use introspection to examine and modify the structure and behavior of the program at runtime. You can use reflection to dynamically create an instance of a type or get the type from an existing object and invoke its methods or access its fields and properties. This makes your code more dynamic and can be written against known interfaces so that the actual classes can be instantiated using reflection. Another purpose of reflection is to create development and debugging tools, and it is also used for meta-programming.
There are two different approaches to implementing reflection:
- The first approach is that the information about reflection is tightly integrated with the language and exists as part of the program's structure. Access to program-based reflection is available by a property or method.
- The second approach is based on the separation of reflection information and program structure. Reflection information is separated inside a distinct Mirror object that binds to the real program member.
Dart reflection follows the second approach with Mirrors. You can find more information about the concept of Mirrors in the original paper written by Gilad Bracha at of Mirrors:
- Mirrors are separate from the main code and cannot be exploited for malicious purposes
- As reflection is not part of the code, the resulting code is smaller
- There are no method-naming conflicts between the reflection API and inspected classes
- It is possible to implement many different Mirrors with different levels of reflection privileges
- It is possible to use Mirrors in command-line and web applications
Let's try Mirrors and see what we can do with them. We will continue to create a library to run our tests.
Introspection in action
We will demonstrate the use of Mirrors with something simple such as introspection. We will need a universal code that can retrieve the information about any object or class in our program to discover its structure and possibly manipulate it with properties and call methods. For this, we've prepared the TypeInspector
class. Let's take a look at the code. We've imported the dart:mirrors
library here to add the introspection ability to our code:
library inspector; import 'dart:mirrors'; import 'test.dart'; class TypeInspector { ClassMirror _classMirror; // Create type inspector for [type]. TypeInspector(Type type) { _classMirror = reflectClass(type); }
The ClassMirror
class contains all the information about the observing type. We perform the actual introspection with the reflectClass
function of Mirrors and return a distinct Mirror object as the result. Then, we call the getAnnotatedMethods
method and specify the name of the annotation
that we are interested in. This will return a list of MethodMirror
that will contain methods annotated with specified parameters. One by one, we step through all the instance members and call the private _isMethodAnnotated
method. If the result of the execution of the _isMethodAnnotated
method is successful, then we add the discovering method to the result
list of found MethodMirror's
, as shown in the following code:
// Return list of method mirrors assigned by [annotation]. List<MethodMirror> getAnnotatedMethods(String annotation) { List<MethodMirror> result = []; // Get all methods _classMirror.instanceMembers.forEach( (Symbol name, MethodMirror method) { if (_isMethodAnnotated(method, annotation)) { result.add(method); } }); return result; }
The first argument of _isMethodAnnotated
has the metadata
property that keeps a list of annotations. The second argument of this method is the annotation
name that we would like to find. The inst
variable holds a reference to the original object in the reflectee
property. We pass through all the method's metadata to exclude some of them annotated with the Test
class and marked with include equals false
. All other method's annotations should be compared to the annotation name, as follows:
// Check is [method] annotated with [annotation]. bool _isMethodAnnotated(MethodMirror method, String annotation) { return method.metadata.any( (InstanceMirror inst) { // For [Test] class we check include condition if (inst.reflectee is Test && !(inst.reflectee as Test).include) { // Test must be exclude return false; } // Literal compare of reflectee and annotation return inst.reflectee.toString() == annotation; }); } }
Dart Mirrors have the following three main functions for introspection:
reflect
: This function is used to introspect an instance that is passed as a parameter and saves the result inInstanceMirror
orClosureMirror
. For the first one, we can call methods, functions, or get and set fields of thereflectee
property. For the second one, we can execute the closure.reflectClass
: This function reflects the class declaration and returnsClassMirror
. It holds full information about the type passed as a parameter.reflectType
: This function returnsTypeMirror
and reflects a class,typedef
, function type, or type variable.
Let's take a look at the main code:
library test.framework; import 'type_inspector.dart'; import 'test_case.dart'; main() { TypeInspector inspector = new TypeInspector(TestCase); List methods = inspector.getAnnotatedMethods('test'); print(methods); }
Firstly, we created an instance of our TypeInspector
class and passed the testable class, in our case, TestCase
. Then, we called getAnnotatedMethods
from inspector
with the name of the annotation, test
. Here is the result of the execution:
[
MethodMirror on 'testStart', MethodMirror on 'testStop']
The inspector
method found the methods testStart
and testStop
and ignored testWarmUp
of the TestCase
class as per our requirements.
Reflection in action
We have seen how introspection helps us find methods marked with annotations. Now we need to call each marked method to run the actual tests. We will do that using reflection. Let's make a MethodInvoker
class to show reflection in action:
library executor; import 'dart:mirrors'; class MethodInvoker implements Function { // Invoke the method call(MethodMirror method) { ClassMirror classMirror = method.owner as ClassMirror; // Create an instance of class InstanceMirror inst = classMirror.newInstance(new Symbol(''), []); // Invoke method of instance inst.invoke(method.simpleName, []); } }
As the MethodInvoker
class implements the Function
interface and has the call
method, we can call instance it as if it was a function. In order to call the method, we must first instantiate a class. Each MethodMirror
method has the owner
property, which points to the owner object in the hierarchy. The owner of MethodMirror
in our case is ClassMirror
. In the preceding code, we created a new instance of the class with an empty constructor and then we invoked the method of inst
by name. In both cases, the second parameter was an empty list of method parameters.
Now, we introduce MethodInvoker
to the main code. In addition to TypeInspector
, we create the instance of MethodInvoker
. One by one, we step through the methods and send each of them to invoker
. We print Success
only if no exceptions occur. To prevent the program from terminating if any of the tests failed, we wrap invoker
in the try-catch block, as shown in the following code:
library test.framework; import 'type_inspector.dart'; import 'method_invoker.dart'; import 'engine_case.dart'; main() { TypeInspector inspector = new TypeInspector(TestCase); List methods = inspector.getAnnotatedMethods(test); MethodInvoker invoker = new MethodInvoker(); methods.forEach((method) { try { invoker(method); print('Success ${method.simpleName}'); } on Exception catch(ex) { print(ex); } on Error catch(ex) { print("$ex : ${ex.stackTrace}"); } }); }
As a result, we will get the following code:
Success Symbol("testStart") Success Symbol("testStop")
To prove that the program will not terminate in the case of an exception in the tests, we will change the code in TestCase
to break it, as follows:
// Start engine @test testStart() { engine.start(); // !!! Broken for reason if (engine.started) throw new Exception("Engine must start"); }
When we run the program, the code for testStart
fails, but the program continues executing until all the tests are finished, as shown in the following code:
Exception: Engine must start Success Symbol("testStop")
And now our test library is ready for use. It uses introspection and reflection to observe and invoke marked methods of any class.