F# 程式設計/反射
| F# : 反射 |
反射允許程式設計師在執行時檢查型別並呼叫物件的函式,而無需在編譯時知道其資料型別。
乍一看,反射似乎違背了 ML 的精神,因為它本質上不是型別安全的,因此使用反射的型別錯誤直到執行時才會被發現。但是,.NET 的型別理念最好表述為靜態型別,在可能的情況下,動態型別,在需要時,其中反射的作用是將動態型別的最理想行為引入靜態型別世界。事實上,動態型別可以節省大量時間,通常促進設計更具表現力的 API,並允許程式碼被重構比靜態型別所能達到的程度更大。
本節旨在簡要概述反射,而不是全面的教程。
有各種方法可以檢查物件的型別。最直接的方法是對任何非空物件呼叫.GetType()方法(繼承自System.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 = ?;
...
也可以使用內建的typeof方法獲取型別資訊,而無需實際物件
> 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和typeof返回一個System.Type的例項,它具有一系列有用的屬性,例如
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
- 使用指定的繫結約束呼叫指定的成員,並匹配指定的引數列表
以下程式將列印傳入的任何物件的屬性
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
該程式輸出以下內容
----------- Program+Car WheelCount: 4 Year: 2009 Model: Focus Make: Ford ----------- Program+Cat Name: Mittens Age: 3
除了發現型別之外,我們還可以動態地呼叫函式和設定屬性
let dynamicSet x propName propValue =
let property = x.GetType().GetProperty(propName)
property.SetValue(x, propValue, null)
反射尤其引人注目,因為它可以讀取/寫入私有欄位,即使是在看起來不可變的物件上也是如此。特別是,我們可以探索和操縱 F# 列表的底層屬性
> 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]
上面的示例就地修改列表,以生成一個迴圈連結列表。在 .NET 中,"不可變"並不真正意味著不可變,私有成員大多是幻覺。
- 注意:反射的功能具有明確的安全隱患,但對反射安全的全面討論遠遠超出了本節的範圍。鼓勵讀者訪問 MSDN 上的反射安全注意事項文章以瞭解更多資訊。
雖然 .NET 的內建反射 API 很有用,但 F# 編譯器執行了很多魔法,這使得使用普通反射的內建型別(如聯合、元組、函式和其他內建型別)看起來很奇怪。Microsoft.FSharp.Reflection 名稱空間提供了一個包裝器來探索 F# 型別。
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"
使用 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 屬性和反射緊密相連。屬性允許程式設計師使用在執行時使用的元資料來修飾類、函式、成員和其他原始碼。許多 .NET 類使用屬性以各種方式註釋程式碼;只有透過反射才能訪問和解釋屬性。本節將簡要概述屬性。鼓勵有興趣獲得更完整概述的讀者閱讀 MSDN 的使用屬性擴充套件元資料系列。
屬性使用[<AttributeName>]定義,這是一種在本書前面各章中已見過的符號。.NET 框架包含許多內建屬性,包括
- System.ObsoleteAttribute - 用於標記在未來版本中打算刪除的原始碼。
- System.FlagsAttribute - 指示列舉可以被視為位欄位。
- System.SerializableAttribute - 指示類可以被序列化。
- System.Diagnostics.DebuggerStepThroughAttribute - 指示偵錯程式不應單步執行方法,除非它包含斷點。
我們可以透過定義一個從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"
我們可以使用反射訪問屬性
> 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;}|]
MyAttribute類具有在例項化時列印到控制檯的副作用,這表明當MyClass的例項建立時,MyAttribute不會被構造。
屬性通常用於修飾類,為其提供任何型別的臨時功能。例如,假設我們想根據屬性來控制建立類的單個例項還是多個例項
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
該程式輸出以下內容
SingleOnly constructor NewAlways constructor NewAlways constructor x = x': true y = y': false
使用上面的屬性,我們完全抽象了單例設計模式的實現細節,將其簡化為一個單一屬性。值得注意的是,上面的程式將true或false的值硬編碼到屬性建構函式中;如果我們想,可以傳遞一個代表應用程式配置檔案中的鍵的字串,並使類構造取決於配置檔案。