CHAPTER 21
Julian supports reflection and introspection on runtime types. Using these APIs can help create highly extensible application frameworks which do not depend on in-source typing.
Before inspecting a type, we need get a hold of it. A Type instance can be obtained in three ways:
// Approach 1: through hardcoded type identifier
Type typ1 = typeof(Machine);
// Approach 2: through runtime type of an object
Tool inst = new Tool();
Type typ2 = inst.getType();
// Approach 3: with an arbitrary name
Type typ3 = Type.load("Vehicle.Car");
In approach 1, if the type has not been loaded into the runtime, the typeof
operator will trigger the type loading (along with its dependencies). This thus has the same side effect as variable declaration for a specific type. Should the loading fail, the operation will fault and throw a ReflectedInvocationException with the inner cause being the real ClassLoadingException.
With the handle on a Type, we can start exploring its metadata. Among the most used methods might be getConstructors(), getMethods() and getFields(), which return the entire set of members under the respective category. For user-defined types, those among returned include the private members on this type and inherited members from its ancestor types. On the other hand, the system types shipped with Julian runtime do not return private members in general.
For each metadata category on a Type:
If a user wants to opt out of exposing a certain member to reflection API, she may easily achieve so by using Reflected attribute, setting visible=false
. This attribute is applicable to constructors, methods and fields, but not a type itself. This means you cannot really hide your type in runtime.
To get constructors of a type, call getConstructors(), which returns all the constructors regardless of the visibility, unless the type is part of system modules. Obviously, this method won't return any constructor for interface type and certain special class types such as Enum and Attribute.
The constructors can then be invoked to create new instance of this type. It's usual practice to enclose the call with a try-catch
block. If the call fails, a ReflectedInvocationException will be thrown. If the failure was caused by the logic of constructor itself, the original exception will be found as the inner cause.
try {
MyClass obj = ctor.invoke(new var[]{ });
...
} catch (System.Reflection.ReflectedInvocationException refl){
... // Error handling
}
Among all the reflection APIs, Method is the most complex one, mainly due to its calling behavior with regards to dynamic dispatching. First, let's get methods from a type:
Type t = typeof(MyClass);
Method[] mtds = t.getMethods();
The methods returned in that array include all members which are visible from the type's internal perspective. What this means is that:
All methods directly defined on this class, regardless of its visibility, are included.
All methods inherited by this class, which can be either public or protected, are also included.
A method is always called with an argument array, even if the method takes no arguments at all. For instance method, the first argument must be the object against which the method is invoked, and a set of rules apply as to what type the object might be of. In fact, with an object of some type different than the method's defining type, it's likely that we end up with another method getting invoked instead of this one represented by the Method instance. To understand the behavior of dynamic dispatching, let's start by introducing a couple of terms:
getMethod()
or getMethods()
API, usually)Consider the following scenario where the child class overrides one method from the parent while inheriting another:
class Parent {
void fun1(){}
void fun2(){}
}
class Child : Parent {
void fun1(){} // Override Parent.fun1()
}
Method[] methods = typeof(Child).getMethods()[]; // methods.length == 2
What we would get from getMethods()
contains two methods: Child.fun1()
and Parent.fun2()
.
The real complication comes when we call the method:
If runtime type is defining type, the method is called against the object. This is quite straightforward.
If runtime type is a child type of defining type, and if the method is inheritable (public/protected), then the corresponding method defined on the runtime type will be called.
If runtime type is a parent type of defining type, and if the method is inheritable (public/protected), then the corresponding method, should it be found on the runtime type, will be called.
In all other cases the call will fail.
These can be illustrated by an example.
class Professional {
void work(){
Console.println("Doing professional work...");
}
}
class Attorney : Professional {
void work(){
Console.println("Doing litigation...");
}
}
class DivorceLawyer : Attorney {
void work(){
Console.println("Settling alimony...");
}
}
Method method = typeof(Attorney).getMethods()[0];
Professional professional = new Professional();
Professional attorney = new Attorney();
Professional divLawyer = new DivorceLawyer();
method.call(new var[]{ professional }); // Doing professional work...
method.call(new var[]{ attorney }); // Doing litigation...
method.call(new var[]{ divLawyer }); // Settling alimony...
In the example above, the method we retrieved from Attorney
is indeed the implementation of work()
method on that type, which prints Doing litigation...
, but when we call it with instances of other types on the class hierarchy, the dynamic dispatching kicks in and the corresponding method that is defined on each runtime type is called instead. In fact, what call() does is exactly to simulate the behavior of calling an instance method in source code, where polymorphism applies based on the runtime type.
Sometimes, you may want to opt out of dynamic dispatching and just call the method as is. Such capability is also what reflection API offers. By calling callExact, the very method implementation this Method instance represents will be invoked. It's natural to think that only an object of strictly matching type can be passed in as 'this' object for callExact
, but Julian allows a little more than that: passing along an object whose type is a child type of defining type is also allowed. In that case, you are forcing a parent method to be called against a child object.
class Professional {
void work(){
Console.println("Doing professional work...");
}
}
class Doctor : Professional {
void work(){
Console.println("Practicing with medicine...");
}
}
Method method = typeof(Professional).getMethods()[0];
Professional doctor = new Doctor();
method.call(new var[]{ doctor }); // Doing professional work...
To comprehend the implication of calling-exact a method on a child object, you may imagine that as if we were somehow calling super.work()
from outside the child type (Doctor). This means the method's accessibility is confined within the super type:
class Professional {
private int income;
void work(int inc){
income += inc;
}
int getIncome(){
return income;
}
}
class Doctor : Professional {
private int income;
void work(int inc){
income += inc;
}
int getIncome(){
return income;
}
}
Method method = typeof(Professional).getMethods()[0];
Professional doctor = new Doctor();
method.call(new var[]{ doctor, 10 }); // affecting private field 'income' on Professional
doctor.getIncome(); // still 0
It's of importance to know that Method is not executable by itself. You cannot just call it with function-call syntax. To get an executable from it, it's necessary to bind it to an object as the invocation target, i.e. 'this' object.
Method method = typeof(Professional).getMethods("work")[0];
Professional prof = new Professional();
Function fun = method.bind(prof);
fun(10);
// In this case, the code above has same effect as
Function fun = prof.work;
fun(10);
However, Julian doesn't guarantee referential equivalence between a Function produced by method binding and the Function obtained directly from the target object. In one example, if the method is overloaded, what you got from property access on that object will actually be a method group object that can dispatch to one of the underlying methods based on argument types. The reflection API, on the other hand, will always produce Function bound with a single method.
To get fields defined on a type, call getFields(). Since fields cannot be overloaded, one may also call getField(string) to get the exact field with the specified name.
A field can be accessed in two ways: getting value from it, and setting value to it. In doing so, it's required to pass along the instance on which the field is to be accessed, as long as the field is at instance scope. If it's a static field, the argument is only technically required for signature mapping but will be disregarded in execution.
class MyClass {
int i;
static int si;
}
MyClass mc = new MyClass();
mc.i = 5;
MyClass.si = 10;
Type typ = typeof(MyClass);
Field field1 = typ.getField("i");
field1.get(mc); // 5
field.set(mc, 7);
field1.get(mc); // 7
Field field2 = typ.getField("si");
field2.get(null); // 10. Here we passed null. In fact you can pass along anything. This value will not be used since the field is static.
Like instance methods, instance fields can also be inherited, so some complexity arises when it comes to accessing to a field, as represented by Field class, on an object of some type other than the defining type from which the Field instance has been obtained. In general, these rules applies, where the we reuse the definition for 'runtime type' and 'defining type' as in previous section on Method.
If runtime type is defining type, access the defined field on that object.
If runtime type is a child type of defining type, and if the field is inheritable (public/protected), then the corresponding field defined on the runtime type will be accessed.
If runtime type is a parent type of defining type, and if the field is inheritable (public/protected), then the corresponding field, should it be found on the runtime type, will be accessed.
In all other cases the access will fail.
In general, the idea is to make sure that under no circumstances will the caller accidentally access to a field that the Field instance is not representing. You may worry, for example, in the case of 2, what if the runtime type has a field of same name defined with private accessibility. This in fact will not happen, as Julian doesn't allow lowering visibility in inherited classes.
Attributes are declaratively associated with type or its members at the definition. Upon type loading, an instance of the specified Attribute class annotated on a target will be created, initialized and retained by the type system. Reflection API is the way to retrieve these attributes during runtime.
import System.Reflection;
attribute Authorship {
string name;
int year;
int[] versions;
}
[Authorship(
name="Asimov",
versions=new int[]{1951, 1952, 1953}
)]
class FoundationSeries {
... ...
}
Type t = typeof(FoundationSeries);
Attribute[] attrs = t.getAttributes();
Authorship auth = (Authorship) attrs[0];
string name = auth.name; // "Asimov"
int[] versions = auth.versions; // 1951, 1952, 1953
The retrieval of attributes on class members are similar. Let's simulate a unit test framework:
import System.Reflection;
[AttributeType(allowMultiple=false, target=AttributeTarget.METHOD)]
attribute UnitTest {
string author;
bool enabled;
}
class MyTests {
[UnitTest(author="Allen", enabled=true)]
public void test1(){}
[UnitTest(author="Kraun", enabled=false)]
public void test2(){}
}
Type t = typeof(MyTests);
Method[] methods = t.getMethods();
for(Method m : methods){
Attribute[] attrs = m.getAttributes();
for(Attribute a : attrs){
if (a is UnitTest) {
UnitTest ut = (UnitTest)a;
Console.println(m.getName() + ": owned by " + ut.author + ", enabled = " + ut.enabled);
}
}
}
Note that the members of an attribute are implicitly constant. So attempt to set a value to it will result in exception. However, since one-dimensional array is also allowed to be used as an attribute member's type, one can bypass this restriction by mutating an element in the array.
The module system is also exposed through reflection API. One can find a module by calling Module.find(). This will trigger a search of the target module as if the module name is encountered in an import
statement. Recall the effect that an import
statement has on the runtime: it will scan the files under that module to load and preserve the minimum info about the types contained within, but neither load any of those types nor perform full semantic check. This means successfully finding a module doesn't mean the types defined within are all clear of admission.
With a hold onto the module, one can proceed to list the types. Note at this stage we can only get access to the basic info about those types. The user must call TypeInfo.load() to load that type into the runtime.
import System.Reflection;
Module mod = Module.find("MyNamespace.MyModule", false); // similar in effect to having "import MyNamespace.MyModule;" at the top of this file
TypeInfo[] types = mod.getTypes();
for(TypeInfo ti : types){
if (ti.getSimpleName() == "MyClass") {
type = ti.load(); // Actually load MyNamespace.MyModule.MyClass - will throw exception if the definition is unsound.
break;
}
}
In symmetry, calling Type.getModule() will return the module info. It's just since the type is already loaded, the module retrieval becomes trivial since it must have been preserved into the runtime at an earlier timing.