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