代码改变世界

当 dynamic 遇上 internal

2011-06-29 07:53 鹤冲天 阅读(...) 评论(...) 编辑 收藏

dynamic 简介

.Net 4 引入了 dynamic,我们可以简单动态访问对象的属性或调用其方法,免去反射的繁琐和不雅:

1
2
3
dynamic person = new Person { ID = 1, Name = "鹤冲天" };
var name = person.Name;
person.Work();

dynamic 对成员的访问是有限制的,只允许访问公有成员,访问私有或保护成员将会抛出异常:

image

这与于 .net 其它部分是一致的。

dynamic 不支持其它程序集中的 internal 类型

但 dynamic 对其它程序集中的 internal 类确不支持,不能不说是一个遗憾。

试看如下解决方案,其中有两个项目:

image

ClassLibrary1 是个类库,有两个类:

1
2
3
4
5
internal class Student
{
    public int ID { get; set; }
    public string Name { get; set; }
}

1
2
3
4
5
6
7
8
9
10
11
public class StudentRepository
{
    public dynamic GetByID(int id)
    {
        return new Student { ID = id, Name = "鹤冲天" };
    }
    public dynamic Select()
    {
        return new { ID = 2, Name = "鹤中天" };
    }
}

Student 为 internal 类型。

WhenDynamicMeetInternal 项目引用了 ClassLibrary1,在 Program 中编码如下:

1
2
3
var repository = new StudentRepository();
dynamic student = repository.GetByID(1);
var name = student.Name;

调试时,抛出以下异常:

image

想必大家都想到了,为什么不把 Student 设成 public 呢?

是的,Students 设成 public 后问题轻松解决:

image

但只解决了部分,却没解决根本,我们来看第二个调用:

1
2
dynamic student2 = repository.Select();
var name2 = student2.Name;

调试时依然有类似异常:

image

为什么呢?

从上图的 Watch 窗口中我们可以看出匿名类型 student2 的类型不是 public,则其只能是 internal 的(没有 private 的类或结构)。其实匿名类型都是 internal 的,不确定可以自己试下。

我们可以将 Student 设为 public,但对匿名类型,我们却无能为力。

dynamic 拒绝为外部程序集的 internal 类型服务,会带来好多麻烦。下面我们探讨解决办法。

解决办法

使用 InternalsVisibleTo

image

在 ClassLibrary1 项目的 AssemblyInfo.cs 文件(见上图)末尾中加入一行代码:

1
[assembly: InternalsVisibleTo("WhenDynamicMeetInternal")]

这种方式缺点多多,不推荐,也就不多解释了。

创建新的动态类型 ReflectionDynamicObject

创建类 ReflectionDynamicObject,继承至 DynamicObject,重写 TryGetMember、TryInvokeMember 等几个方法,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
using System.Dynamic;
using System.Globalization;
using System.Reflection;

internal sealed class ReflectionDynamicObject : DynamicObject
{
    private object RealObject { get; set; }

    public override bool TryConvert(ConvertBinder binder, out object result)
    {
        result = this.RealObject;
        return true;
    }
    public override bool TryGetMember(GetMemberBinder binder, out object result)
    {
        PropertyInfo property = this.RealObject.GetType().GetProperty(binder.Name, BindingFlags.GetProperty | 
            BindingFlags.Public | BindingFlags.Instance);
        if (property == null)
        {
            result = null;
        }
        else
        {
            result = property.GetValue(this.RealObject, null);
            result = WrapObjectIfInternal(result);
        }
        return true;
    }
    public override bool TryInvokeMember(InvokeMemberBinder binder, object[] args, out object result)
    {
        result = this.RealObject.GetType().InvokeMember(binder.Name, BindingFlags.InvokeMethod | BindingFlags.NonPublic | 
            BindingFlags.Public | BindingFlags.Instance, null, this.RealObject, args, CultureInfo.InvariantCulture);
        return true;
    }
    public static object WrapObjectIfInternal(object o)
    {
        if (o == null) return null;
        if (o.GetType().IsPublic) return o;
        return new ReflectionDynamicObject { RealObject = o };
    }
    public override string ToString()
    {
        return this.RealObject.ToString();
    }
}

有这个类,就可以解决 internal 的问题了:

1
2
dynamic student3 = ReflectionDynamicObject.WrapObjectIfInternal(repository.Select());
var name3 = student3.Name;

运行截图:

image

关于 ReflectionDynamicObject

ReflectionDynamicObject 是从 System.Web.WebPages.dll 程序集中反编译出来的:

image

附源码:WhenDynamicMeetInternal.rar (48KB)