跳转到内容

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的值硬编码到属性构造函数中;如果我们想,可以传递一个代表应用程序配置文件中的键的字符串,并使类构造取决于配置文件。

前一章:高级数据结构 索引 下一章:引用
华夏公益教科书