Using Reflect and Type
Introduction
This tutorial demonstrates how to use reflection. The Reflect
class allows code to view and modify the structure of data types at runtime. The Type
class contains functions that let us determine variable types and instantiate objects by name at runtime. Both classes are made up of static methods.
Variable Type Checks
This example does a few type checks on variables of different types. We check to see if some variables are objects or functions. Then we get variable types as strings. Then we get the class names of the variables that are objects.
class TypeCheckEx { public function new() {} public static function func() {} public static function main() { // set some variables var obj = new TypeCheckEx(); var anon = {one:1, two:2.2, three: "three"}; var one = 1; var str = "string"; // detect if a variable is an object trace("object check:"); trace(" obj is an object: " + Reflect.isObject(obj)); trace(" anon is an object: " + Reflect.isObject(anon)); trace(" 1 is an object: " + Reflect.isObject(one)); // detect if a variable is a function trace("function check:"); trace(" str is a function: " + Reflect.isFunction(str)); trace(" func is a function: " + Reflect.isFunction(func)); // get type trace("type lookup:"); trace(" one is a: " + Type.typeof(one)); trace(" anon is a: " + Type.typeof(anon)); trace(" func is a: " + Type.typeof(func)); trace(" str is a: " + Type.typeof(str)); trace(" obj is a: " + Type.typeof(obj)); // get class name trace("class name lookup:"); trace(" str is a: " + Type.getClassName(Type.getClass(str))); trace(" obj is a: " + Type.getClassName(Type.getClass(obj))); } }
Compile and run with:
haxe -neko ex1.n -main TypeCheckEx neko ex1.n
The output should be:
TypeCheckEx.hx:18: object check: TypeCheckEx.hx:19: obj is an object: true TypeCheckEx.hx:20: anon is an object: true TypeCheckEx.hx:21: 1 is an object: false TypeCheckEx.hx:24: function check: TypeCheckEx.hx:25: str is a function: false TypeCheckEx.hx:26: func is a function: true TypeCheckEx.hx:29: type lookup: TypeCheckEx.hx:30: one is a: TInt TypeCheckEx.hx:31: anon is a: TObject TypeCheckEx.hx:32: func is a: TFunction TypeCheckEx.hx:33: str is a: TClass({ ... }) TypeCheckEx.hx:34: obj is a: TClass({ ... }) TypeCheckEx.hx:37: class name lookup: TypeCheckEx.hx:38: str is a: String TypeCheckEx.hx:39: obj is a: TypeCheckEx
See ValueType
for the list of names returned by Type.typeof
.
TEnum
and TClass
contain type parameters and function prototypes which were removed to simplify the output above.
More Variable Type Checks
Haxe offers even more tools for checking types. This example demonstrates the usage of the type
and Std.is
functions. The type
function outputs the type of the given variable to the console at compile time. This output is formatted like a trace statement. Std.is
returns true if the given variable is of the given type, or is a subclass of that type. The type is specified using the actual class name, not the ValueType
value.
import Type; class TypeCheckEx2 { public function new() {} public static function main() { var a : Int = 1; // type explicitly set var b = 2.2; // type inferred var c : String = "str"; var d = new TypeCheckEx2(); // these output to stdout during compile type(a); type(b); type(c); type(d); // a is an int and float trace("a is an Int: " + Std.is(a, Int)); trace("a is a Float: " + Std.is(a, Float)); // b is only a float trace("b is an Int: " + Std.is(b, Int)); trace("b is a Float: " + Std.is(b, Float)); // c is a string trace("c is a String: " + Std.is(c, String)); // d is a TypeCheckEx2 trace("b is a TypeCheckEx2: " + Std.is(d, TypeCheckEx2)); } }
Compile with:
haxe -neko ex2.n -main TypeCheckEx2
The output from compile should be:
TypeCheckEx2.hx:16: characters 9-10 : Int TypeCheckEx2.hx:17: characters 9-10 : Float TypeCheckEx2.hx:18: characters 9-10 : String TypeCheckEx2.hx:19: characters 9-10 : TypeCheckEx2
Run with:
neko ex2.n
The output should be:
TypeCheckEx2.hx:22: a is an Int: true TypeCheckEx2.hx:23: a is a Float: true TypeCheckEx2.hx:26: b is an Int: false TypeCheckEx2.hx:27: b is a Float: true TypeCheckEx2.hx:30: c is a String: true TypeCheckEx2.hx:33: b is a TypeCheckEx2: true
Note that a
is both an Int
and a Float
. This is because Int
is a subclass of Float
.
Anonymous Types
In this example, we create an object with anonymous type, look through its fields and copy it. Note that Reflect.fields
returns a list of field names. To get the field values, we must call Reflect.field
with the field name.
The example goes on to modify the original object (capitalize it) and print both the original and the copy. Since Reflect.copy
doesn't make deep copies, our modification of the original object's four.one
field also affects the copy. This happens because both variable's four
field point to the same object.
class AnonEx { public static function main() { // create an object with an anonymous type var anon = {one:1, two:2.2, three: "three", four:{one:"one"}}; // detect if an object has a specified field // note that notSet is not seen trace("field check:"); trace(" anon has field one: " + Reflect.hasField(anon, "one")); trace(" anon has field five: " + Reflect.hasField(anon, "five")); // print fields from the anonymous object trace("anonymous type fields:"); for( ff in Reflect.fields(anon) ) trace(" anon." + ff + " = " + Reflect.field(anon, ff)); // copy it var anonCopy = Reflect.copy(anon); // modify the original anon.two = 2.2222; // does not affect copy anon.three = "THREE"; // does not affect copy anon.four.one = "ONE"; // affects copy // print the original trace("modified type fields:"); for( ff in Reflect.fields(anon) ) trace(" anon." + ff + " = " + Reflect.field(anon, ff)); // print the copy trace("anonymous copy type fields:"); for( ff in Reflect.fields(anonCopy) ) trace(" anon." + ff + " = " + Reflect.field(anonCopy, ff)); } }
Compile and run with:
haxe -neko ex3.n -main AnonEx neko ex3.n
The output should be:
AnonEx.hx:10: field check: AnonEx.hx:11: anon has field one: true AnonEx.hx:13: anon has field five: false AnonEx.hx:17: anonymous type fields: AnonEx.hx:19: anon.four = { one => one } AnonEx.hx:19: anon.one = 1 AnonEx.hx:19: anon.two = 2.2 AnonEx.hx:19: anon.three = three AnonEx.hx:30: modified type fields: AnonEx.hx:32: anon.four = { one => ONE } AnonEx.hx:32: anon.one = 1 AnonEx.hx:32: anon.two = 2.2222 AnonEx.hx:32: anon.three = THREE AnonEx.hx:35: anonymous copy type fields: AnonEx.hx:37: anon.four = { one => ONE } AnonEx.hx:37: anon.one = 1 AnonEx.hx:37: anon.two = 2.2 AnonEx.hx:37: anon.three = three
Classes
This example instantiates an object using the class name as a string literal. This is necessary if the desired type is not known until runtime. Then the example accesses the object's fields and methods by name using string
s. Then it removes a field and adds another to the object.
Note that Reflect.fields
only returns fields that have been set, whereas Type.getInstanceFields
returns all fields and methods. Also note that Reflect.fields
allows access to private fields.
The result of printing the value of a method is "#function:n" where n
is the number of arguments it takes.
class ObjEx { // a private field private var privateNum : Int; // a public property public var publicString(default,null) : String; // reflection ignores this public var notSet : String; // initialize fields public function new() { privateNum = 2; publicString = "car"; } // an example method. adds two numbers public function sum(a,b) { return a+b; } // an example method public function getHi() { return "Hi"; } public static function main() { // set some variables var obj = new ObjEx(); var str = "string"; var sum = obj.sum; // create class by name trace("\ncreate class by name:"); var obj2 = Type.createInstance(Type.resolveClass("ObjEx"),[]); trace(" obj2.publicString : " + obj2.publicString); trace(" obj2.getHi() : " + obj2.getHi()); // detect if an object has a specified field // note that notSet is not seen trace("field check:"); trace(" obj has sum field: " + Reflect.hasField(obj, "notSet")); trace(" obj has publicString field: " + Reflect.hasField(obj, "publicString")); // print all fields and methods trace("all class fields:"); var allFields = Type.getInstanceFields(Type.resolveClass("ObjEx")); for( ff in allFields ) trace(" obj." + ff + " = " + Reflect.field(obj, ff)); // print all methods trace("methods:"); var isFunction = function(name) { return Reflect.isFunction(Reflect.field(obj,name)); } var functions = Lambda.filter(allFields, isFunction); for( ff in functions ) trace(" obj." + ff + "()"); // print data fields // note that notSet is skipped, and the private field is visible trace("data fields:"); for( ff in Reflect.fields(obj) ) trace(" obj." + ff + " = " + Reflect.field(obj, ff)); // delete a field. create another one Reflect.deleteField(obj, "publicString"); Reflect.setField(obj, "newString", "boat"); // print fields again trace("fields after modifying:"); for( ff in Reflect.fields(obj) ) trace(" obj." + ff + " = " + Reflect.field(obj, ff)); // call a method // note that arguments cannot be optional when called this way trace("call methods:"); trace(" obj.sum(1,2): " + Reflect.callMethod(obj, Reflect.field(obj,"sum"), [1,2])); trace(" obj.getHi(): " + Reflect.callMethod(obj, Reflect.field(obj,"getHi"), [])); } }
Compile and run with:
haxe -neko ex4.n -main ObjEx neko ex4.n
The output should be:
ObjEx.hx:39: create class by name: ObjEx.hx:41: obj2.publicString : car ObjEx.hx:42: obj2.getHi() : Hi ObjEx.hx:46: field check: ObjEx.hx:47: obj has sum field: false ObjEx.hx:49: obj has publicString field: true ObjEx.hx:53: all class fields: ObjEx.hx:56: obj.sum = #function:2 ObjEx.hx:56: obj.privateNum = 2 ObjEx.hx:56: obj.getHi = #function:0 ObjEx.hx:56: obj.publicString = car ObjEx.hx:56: obj.notSet = null ObjEx.hx:59: methods: ObjEx.hx:64: obj.sum() ObjEx.hx:64: obj.getHi() ObjEx.hx:68: data fields: ObjEx.hx:70: obj.privateNum = 2 ObjEx.hx:70: obj.publicString = car ObjEx.hx:77: fields after modifying: ObjEx.hx:79: obj.newString = boat ObjEx.hx:79: obj.privateNum = 2 ObjEx.hx:83: call methods: ObjEx.hx:84: obj.sum(1,2): 3 ObjEx.hx:86: obj.getHi(): Hi
VarArgs
This example shows how to create a function that takes a variable number of arguments. In the example, we call the sub
function twice. The first time, we pass in an Array
of Dynamic
values, as the function's definition specifies. Then we get a vararg handle to the function (varArgSub
), and call it passing in each argument separately. The output of each call is identical.
The values of the arguments we use are a variety of types. In the example we are just printing them, so their types don't matter, but if we were doing more with them it is a good idea to check their types before accessing them to make sure they're what the function expects. Since they're declared Dynamic
the compiler isn't doing any type checking for us.
class VarArgsEx { public static function main() { // call sub normally var input : Array<Dynamic> = [1,2,3,[4,5,6],"cat"]; trace("sub called normally"); sub(input); // call with var arg var varArgSub = Reflect.makeVarArgs(sub); trace("sub called with var arg"); varArgSub(1,2,3,[4,5,6],"cat"); } private static function sub(val:Array<Dynamic>) : Dynamic { for( ii in val ) trace(" param: " + ii); } }
Compile and run with:
haxe -neko varargs.n -main VarArgsEx neko varargs.n
The output should be:
VarArgsEx.hx:7: sub called normally VarArgsEx.hx:19: param: 1 VarArgsEx.hx:19: param: 2 VarArgsEx.hx:19: param: 3 VarArgsEx.hx:19: param: [4, 5, 6] VarArgsEx.hx:19: param: cat VarArgsEx.hx:12: sub called with var arg VarArgsEx.hx:19: param: 1 VarArgsEx.hx:19: param: 2 VarArgsEx.hx:19: param: 3 VarArgsEx.hx:19: param: [4, 5, 6] VarArgsEx.hx:19: param: cat
Type Inference
We use type inference in these examples to improve code readability, but we could have declared each variable's type explicitly. The following are alternate ways to define variables from the above examples that specify the types explicitly.
From TypeCheckEx, obj
could be defined as:
var obj : TypeCheckEx = new TypeCheckEx();
anon
could be defined as:
var anon : {one:Int, two:Float, three:String} = {one:1, two:2.2, three: "three"};
one
could be defined as:
var one : Int = 1;
str
could be defined as:
var str : String = "string";
From ObjEx, the sum
method could be defined as:
public function sum(a:Int, b:Int) : Int { return a+b; }