跳轉到內容

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.GetTypetypeof返回一個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 上的反射安全注意事項文章以瞭解更多資訊。

Microsoft.FSharp.Reflection 名稱空間

[編輯 | 編輯原始碼]

雖然 .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.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

使用上面的屬性,我們完全抽象了單例設計模式的實現細節,將其簡化為一個單一屬性。值得注意的是,上面的程式將truefalse的值硬編碼到屬性建構函式中;如果我們想,可以傳遞一個代表應用程式配置檔案中的鍵的字串,並使類構造取決於配置檔案。

前一章:高階資料結構 索引 下一章:引用
華夏公益教科書