From Wikibooks, open books for an open world
Reflection allows programmers to inspect types and invoke methods of objects at runtime without knowing their data type at compile time.
At first glance, reflection seems to go against the spirit of ML as it is inherently not type-safe, so typing errors using reflection are not discovered until runtime. However, .NET's typing philosophy is best stated as static typing where possible, dynamic typing when needed, where reflection serves to bring in the most desirable behaviors of dynamic typing into the static typing world. In fact, dynamic typing can be a huge time saver, often promotes the design of more expressive APIs, and allows code to be refactored much further than possible with static typing.
This section is intended as a cursory overview of reflection, not a comprehensive tutorial.
There are a variety of ways to inspect the type of an object. The most direct way is calling the .GetType()
method (inherited from System.Object
) on any non-null object:
> "hello world".GetType();; val it : System.Type = System.String {Assembly = mscorlib, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089; AssemblyQualifiedName = "System.String, mscorlib, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089"; Attributes = AutoLayout, AnsiClass, Class, Public, Sealed, Serializable, BeforeFieldInit; BaseType = System.Object; ContainsGenericParameters = false; DeclaringMethod = ?; DeclaringType = null; FullName = "System.String"; GUID = 296afbff-1b0b-3ff5-9d6c-4e7e599f8b57; GenericParameterAttributes = ?; GenericParameterPosition = ?; ...
Its also possible to get type information without an actual object using the built-in typeof
method:
> typeof<System.IO.File>;; val it : System.Type = System.IO.File {Assembly = mscorlib, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089; AssemblyQualifiedName = "System.IO.File, mscorlib, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089"; Attributes = AutoLayout, AnsiClass, Class, Public, Abstract, Sealed, BeforeFieldInit; BaseType = System.Object; ContainsGenericParameters = false; DeclaringMethod = ?; DeclaringType = null; FullName = "System.IO.File"; ...
object.GetType
and typeof
return an instance of System.Type
, which has a variety of useful properties such as:
val Name : string
val GetConstructors : unit -> ConstructorInfo array
val GetMembers : unit -> MemberInfo array
val InvokeMember : (name : string, invokeAttr : BindingFlags, binder : Binder, target : obj, args : obj) -> obj
The following program will print out the properties of any object passed into it:
type Car(make : string, model : string, year : int) = member this.Make = make member this.Model = model member this.Year = year member this.WheelCount = 4 type Cat() = let mutable age = 3 let mutable name = System.String.Empty member this.Purr() = printfn "Purrr" member this.Age with get() = age and set(v) = age <- v member this.Name with get() = name and set(v) = name <- v let printProperties x = let t = x.GetType() let properties = t.GetProperties() printfn "-----------" printfn "%s" t.FullName properties |> Array.iter (fun prop -> if prop.CanRead then let value = prop.GetValue(x, null) printfn "%s: %O" prop.Name value else printfn "%s: ?" prop.Name) let carInstance = new Car("Ford", "Focus", 2009) let catInstance = let temp = new Cat() temp.Name <- "Mittens" temp printProperties carInstance printProperties catInstance
This program outputs the following:
----------- Program+Car WheelCount: 4 Year: 2009 Model: Focus Make: Ford ----------- Program+Cat Name: Mittens Age: 3
In addition to discovering types, we can dynamically invoke methods and set properties:
let dynamicSet x propName propValue = let property = x.GetType().GetProperty(propName) property.SetValue(x, propValue, null)
Reflection is particularly remarkable in that it can read/write private fields, even on objects which appear to be immutable. In particular, we can explore and manipulate the underlying properties of an F# list:
> open System.Reflection let x = [1;2;3;4;5] let lastNode = x.Tail.Tail.Tail.Tail;; val x : int list = [1; 2; 3; 4; 5] val lastNode : int list = [5] > lastNode.GetType().GetFields(BindingFlags.NonPublic ||| BindingFlags.Instance) |> Array.map (fun field -> field.Name);; val it : string array = [|"__Head"; "__Tail"|] > let tailField = lastNode.GetType().GetField("__Tail", BindingFlags.NonPublic ||| BindingFlags.Instance);; val tailField : FieldInfo = Microsoft.FSharp.Collections.FSharpList`1[System.Int32] __Tail > tailField.SetValue(lastNode, x);; (* circular list *) val it : unit = () > x |> Seq.take 20 |> Seq.to_list;; val it : int list = [1; 2; 3; 4; 5; 1; 2; 3; 4; 5; 1; 2; 3; 4; 5; 1; 2; 3; 4; 5]
The example above mutates the list in place and to produce a circularly linked list. In .NET, "immutable" doesn't really mean immutable and private members are mostly an illusion.
While .NET's built-in reflection API is useful, the F# compiler performs a lot of magic which makes built-in types like unions, tuples, functions, and other built-in types appear strange using vanilla reflection. The Microsoft.FSharp.Reflection namespace provides a wrapper for exploring F# types.
open System.Reflection open Microsoft.FSharp.Reflection let explore x = let t = x.GetType() if FSharpType.IsTuple(t) then let fields = FSharpValue.GetTupleFields(x) |> Array.map string |> fun strings -> System.String.Join(", ", strings) printfn "Tuple: (%s)" fields elif FSharpType.IsUnion(t) then let union, fields = FSharpValue.GetUnionFields(x, t) printfn "Union: %s(%A)" union.Name fields else printfn "Got another type"
Using fsi:
> explore (Some("Hello world"));; Union: Some([|"Hello world"|]) val it : unit = () > explore (7, "Hello world");; Tuple: (7, Hello world) val it : unit = () > explore (Some("Hello world"));; Union: Some([|"Hello world"|]) val it : unit = () > explore [1;2;3;4];; Union: Cons([|1; [2; 3; 4]|]) val it : unit = () > explore "Hello world";; Got another type
.NET attributes and reflection go hand-in-hand. Attributes allow programmers to decorate classes, methods, members, and other source code with metadata used at runtime. Many .NET classes use attributes to annotate code in a variety of ways; it is only possible to access and interpret attributes through reflection. This section will provide a brief overview of attributes. Readers interested in a more complete overview are encouraged to read MSDN's Extending Metadata With Attributes series.
Attributes are defined using [<AttributeName>]
, a notation already seen in a variety of places in previous chapters of this book. The .NET framework includes a number of built-in attributes, including:
We can create custom attributes by defining a new type which inherits from System.Attribute
:
type MyAttribute(text : string) = inherit System.Attribute() do printfn "MyAttribute created. Text: %s" text member this.Text = text [<MyAttribute("Hello world")>] type MyClass() = member this.SomeProperty = "This is a property"
We can access attribute using reflection:
> let x = new MyClass();; val x : MyClass > x.GetType().GetCustomAttributes(true);; MyAttribute created. Text: Hello world val it : obj [] = [|System.SerializableAttribute {TypeId = System.SerializableAttribute;}; FSI_0028+MyAttribute {Text = "Hello world"; TypeId = FSI_0028+MyAttribute;}; Microsoft.FSharp.Core.CompilationMappingAttribute {SequenceNumber = 0; SourceConstructFlags = ObjectType; TypeId = Microsoft.FSharp.Core.CompilationMappingAttribute; VariantNumber = 0;}|]
The MyAttribute
class has the side-effect of printing to the console on instantiation, demonstrating that MyAttribute
does not get constructed when instances of MyClass
are created.
Attributes are often used to decorate classes with any kind of ad-hoc functionality. For example, let's say we wanted to control whether single or multiple instances of classes are created based on an attribute:
open System open System.Collections.Generic [<AttributeUsage(AttributeTargets.Class)>] type ConstructionAttribute(singleInstance : bool) = inherit Attribute() member this.IsSingleton = singleInstance let singletons = Dictionary<System.Type,obj>() let make<'a>() : 'a = let newInstance() = Activator.CreateInstance<'a>() let attributes = typeof<'a>.GetCustomAttributes(typeof<ConstructionAttribute>, true) let singleInstance = if attributes.Length > 0 then let constructionAttribute = attributes.[0] :?> ConstructionAttribute constructionAttribute.IsSingleton else false if singleInstance then match singletons.TryGetValue(typeof<'a>) with | true, v -> v :?> 'a | _ -> let instance = newInstance() singletons.Add(typeof<'a>, instance) instance else newInstance() [<ConstructionAttribute(true)>] type SingleOnly() = do printfn "SingleOnly constructor" [<ConstructionAttribute(false)>] type NewAlways() = do printfn "NewAlways constructor" let x = make<SingleOnly>() let x' = make<SingleOnly>() let y = make<NewAlways>() let y' = make<NewAlways>() printfn "x = x': %b" (x = x') printfn "y = y': %b" (y = y') Console.ReadKey(true) |> ignore
This program outputs the following:
SingleOnly constructor NewAlways constructor NewAlways constructor x = x': true y = y': false
Using the attribute above, we've completely abstracted away the implementation details of the singleton design pattern, reducing it down to a single attribute. Its worth noting that the program above hard-codes a value of true
or false
into the attribute constructor; if we wanted to, we could pass a string representing a key from the application's config file and make class construction dependent on the config file.
RetroSearch is an open source project built by @garambo | Open a GitHub Issue
Search and Browse the WWW like it's 1997 | Search results from DuckDuckGo
HTML:
3.2
| Encoding:
UTF-8
| Version:
0.7.4