代码改变世界

这下没理由嫌Eval的性能差了吧?

2009-01-09 02:32  Jeffrey Zhao  阅读(18395)  评论(110编辑  收藏

Updated:提供思考题解答

好吧,你偏要说Eval性能差

写ASP.NET中使用Eval是再常见不过的手段了,好像任何一本ASP.NET书里都会描述如何把一个DataTable绑定到一个控件里去,并且通过Eval来取值的用法。不过在目前的DDD(Domain Driven Design)时代,我们操作的所操作的经常是领域模型对象。我们可以把任何一个实现了IEnumerable的对象作为绑定控件的数据源,并且在绑定控件中通过Eval来获取字段的值。如下:

protected void Page_Load(object sender, EventArgs e)
{
    List<Comment> comments = GetComments();
    this.rptComments.DataSource = comments;
    this.rptComments.DataBind();
}

<asp:Repeater runat="server" ID="rptComments">
    <ItemTemplate>
        Title: <%# Eval("Title") %><br />
        Conent: <%# Eval("Content") %>
    </ItemTemplate>
    <SeparatorTemplate>
        <hr />
    </SeparatorTemplate>
</asp:Repeater>

在这里,Eval对象就会通过反射来获取Title和Content属性的值。于是经常就有人会见到说:“反射,性能多差啊,我可不用!”。在这里我还是对这种追求细枝末节性能的做法持保留态度。当然,在上面的例子里我们的确可以换种写法:

<asp:Repeater runat="server" ID="rptComments">
    <ItemTemplate>
        Title: <%# (Container.DataItem as Comment).Title %><br />
        Conent: <%# (Container.DataItem as Comment).Content %>
    </ItemTemplate>
    <SeparatorTemplate>
        <hr />
    </SeparatorTemplate>
</asp:Repeater>

我们通过Container.DataItem来获取当前遍历过程中的数据对象,将其转换成Comment之后读取它的Title和Content属性。虽然表达式有些长,但似乎也是个不错的解决方法。性能嘛……肯定是有所提高了。

但是,在实际开发过程中,我们并不一定能够如此轻松的将某个特定类型的数据作为数据源,往往需要组合两种对象进行联合显示。例如,我们在显示评论列表时往往还会要显示发表用户的个人信息。由于C# 3.0中已经支持了匿名对象,所以我们可以这样做:

protected void Page_Load(object sender, EventArgs e)
{
    List<Comment> comments = GetComments();
    List<User> users = GetUsers();

    this.rptComments.DataSource = from c in comments
                                  from u in users
                                  where c.UserID == u.UserID
                                  order by c.CreateTime
                                  select new
                                  {
                                      Title = c.Title,
                                      Content = c.Content,
                                      NickName = u.NickName
                                  };
    this.rptComments.DataBind();
}

我们通过LINQ级联Comment和User数据集,可以轻松地构造出构造出作为数据源的匿名对象集合(有没有看出LINQ的美妙?)。上面的匿名对象将包含Title,Content和NickName几个公有属性,因此在页面中仍旧使用Eval来获取数据,不提。

不过我几乎可以肯定,又有人要叫了起来:“LINQ没有用!我们不用LINQ!Eval性能差!我们不用Eval!”。好吧,那么我免为其难地为他们用“最踏实”的技术重新实现一遍:

private Dictionary<int, User> m_users;
protected User GetUser(int userId)
{
    return this.m_users[userId];
}

protected void Page_Load(object sender, EventArgs e)
{
    List<Comment> comments = GetComments();
    List<User> users = GetUsers();

    this.m_users = new Dictionary<int, User>();
    foreach (User u in users)
    {
        this.m_users[u.UserID] = u;
    }

    this.rptComments.DataSource = comments;
    this.rptComments.DataBind();
}

<asp:Repeater runat="server" ID="rptComments">
    <ItemTemplate>
        Title: <%# (Container.DataItem as Comment).Title %><br />
        Conent: <%# (Container.DataItem as Comment).Content %><br />
        NickName: <%# this.GetUser((Container.DataItem as Comment).UserID).NickName %>
    </ItemTemplate>
    <SeparatorTemplate>
        <hr />
    </SeparatorTemplate>
</asp:Repeater>

兄弟们自己做判断吧。

嫌反射性能差?算有那么一点道理吧……

反射速度慢?我同意它是相对慢一些。

反射占CPU多?我同意他是相对多一点。

所以Eval不该使用?我不同意——怎能把孩子和脏水一起倒了?我们把反射访问属性的性能问题解决不就行了吗?

性能差的原因在于Eval使用了反射,解决这类问题的传统方法是使用Emit。但是.NET 3.5中现在已经有了Lambda Expression,我们动态构造一个Lambda Expression之后可以通过它的Compile方法来获得一个委托实例,至于Emit实现中的各种细节已经由.NET框架实现了——这一切还真没有太大难度了。

public class DynamicPropertyAccessor
{
    private Func<object, object> m_getter;

    public DynamicPropertyAccessor(Type type, string propertyName)
        : this(type.GetProperty(propertyName))
    { }

    public DynamicPropertyAccessor(PropertyInfo propertyInfo)
    {
        // target: (object)((({TargetType})instance).{Property})

        // preparing parameter, object type
        ParameterExpression instance = Expression.Parameter(
            typeof(object), "instance");

        // ({TargetType})instance
        Expression instanceCast = Expression.Convert(
            instance, propertyInfo.ReflectedType);

        // (({TargetType})instance).{Property}
        Expression propertyAccess = Expression.Property(
            instanceCast, propertyInfo);

        // (object)((({TargetType})instance).{Property})
        UnaryExpression castPropertyValue = Expression.Convert(
            propertyAccess, typeof(object));

        // Lambda expression
        Expression<Func<object, object>> lambda = 
            Expression.Lambda<Func<object, object>>(
                castPropertyValue, instance);

        this.m_getter = lambda.Compile();
    }

    public object GetValue(object o)
    {
        return this.m_getter(o);
    }
}

在DynamicPropertyAccessor中,我们为一个特定的属性构造一个形为o => object((Class)o).Property的Lambda表达式,它可以被Compile为一个Func<object, object>委托。最终我们可以通过为GetValue方法传入一个Class类型的对象来获取那个指定属性的值。

这个方法是不是比较眼熟?没错,我在《方法的直接调用,反射调用与……Lambda表达式调用》一文中也使用了类似的做法。

测试一下性能?

我们来比对一下属性的直接获取值,反射获取值与……Lambda表达式获取值三种方式之间的性能。

var t = new Temp { Value = null };

PropertyInfo propertyInfo = t.GetType().GetProperty("Value");
Stopwatch watch1 = new Stopwatch();
watch1.Start();
for (var i = 0; i < 1000000; i ++)
{
    var value = propertyInfo.GetValue(t, null);
}
watch1.Stop();
Console.WriteLine("Reflection: " + watch1.Elapsed);

DynamicPropertyAccessor property = new DynamicPropertyAccessor(t.GetType(), "Value");
Stopwatch watch2 = new Stopwatch();
watch2.Start();
for (var i = 0; i < 1000000; i++)
{
    var value = property.GetValue(t);
}
watch2.Stop();
Console.WriteLine("Lambda: " + watch2.Elapsed);

Stopwatch watch3 = new Stopwatch();
watch3.Start();
for (var i = 0; i < 1000000; i++)
{
    var value = t.Value;
}
watch3.Stop();
Console.WriteLine("Direct: " + watch3.Elapsed);

结果如下:

Reflection: 00:00:04.2695397
Lambda: 00:00:00.0445277
Direct: 00:00:00.0175414

使用了DynamicPropertyAccessor之后,性能虽比直接调用略慢,也已经有百倍的差距了。更值得一提的是,DynamicPropertyAccessor还支持对于匿名对象的属性的取值。这意味着,我们的Eval方法完全可以依托在DynamicPropertyAccessor之上。

离快速Eval只有一步之遥了

“一步之遥”?没错,那就是缓存。调用一个DynamicPropertyAccessor的GetValue方法很省时,可是构造一个DynamicPropertyAccessor对象却非常耗时。因此我们需要对DynamicPropertyAccessor对象进行缓存,如下:

public class DynamicPropertyAccessorCache
{
    private object m_mutex = new object();
    private Dictionary<Type, Dictionary<string, DynamicPropertyAccessor>> m_cache =
        new Dictionary<Type, Dictionary<string, DynamicPropertyAccessor>>();

    public DynamicPropertyAccessor GetAccessor(Type type, string propertyName)
    {
        DynamicPropertyAccessor accessor;
        Dictionary<string, DynamicPropertyAccessor> typeCache;

        if (this.m_cache.TryGetValue(type, out typeCache))
        {
            if (typeCache.TryGetValue(propertyName, out accessor))
            {
                return accessor;
            }
        }

        lock (m_mutex)
        {
            if (!this.m_cache.ContainsKey(type))
            {
                this.m_cache[type] = new Dictionary<string, DynamicPropertyAccessor>();
            }

            accessor = new DynamicPropertyAccessor(type, propertyName);
            this.m_cache[type][propertyName] = accessor;

            return accessor;
        }
    }
}

经过测试之后发现,由于每次都要从缓存中获取DynamicPropertyAccessor对象,调用性能有所下降,但是依旧比反射调用要快几十上百倍。

FastEval——还有人会拒绝吗?

FastEval方法,如果在之前的.NET版本中,我们可以将其定义在每个页面的共同基类里。不过既然我们在用.NET 3.5,我们可以使用Extension Method这种没有任何侵入的方式来实现:

public static class FastEvalExtensions
{
    private static DynamicPropertyAccessorCache s_cache = 
        new DynamicPropertyAccessorCache();

    public static object FastEval(this Control control, object o, string propertyName)
    {
        return s_cache.GetAccessor(o.GetType(), propertyName).GetValue(o);
    }

    public static object FastEval(this TemplateControl control, string propertyName)
    {
        return control.FastEval(control.Page.GetDataItem(), propertyName);
    }
}

我们在Control上的扩展,确保了每个页面中都可以直接通过一个对象和属性名获取一个值。而在TemplateControl上的扩展,则使得各类可以绑定控件或页面(Page,MasterPage,UserControl)都可以直接通过属性名来获取当前正在绑定的那个数据对象里的属性值。

现在,您还有什么理由拒绝FastEval?

其他

其实我们整篇文章都小看了Eval方法的作用。Eval方法的字符串参数名为“expression”,也就是表达式。事实上我们甚至可以使用“.”来分割字符串以获取一个对象深层次的属性,例如<%# Eval("Content.Length") %>。那么我们的FastEval可以做到这一点吗?当然可以——只不过这需要您自己来实现了。:)

最后再留一个问题供大家思考:现在DynamicPropertyAccessor只提供一个GetValue方法,那么您能否为其添加一个SetValue方法来设置这个属性呢?希望大家踊跃回复,稍后我将提供我的做法。

思考题解答

有一点大家应该知道,一个属性其实是由一对get/set方法组成(当然可能缺少其中一个)。而获取了一个属性的PropertyInfo对象之后,可以通过它的GetSetMethod方法来获取它的设置方法。接下来的工作,不就可以完全交给《方法的直接调用,反射调用与……Lambda表达式调用》一文里的DynamicMethodExecutor了吗?因此为DynamicPropertyAccessor添加一个SetValue方法也很简单:

public class DynamicPropertyAccessor
{
    ...
    private DynamicMethodExecutor m_dynamicSetter;

    ...

    public DynamicPropertyAccessor(PropertyInfo propertyInfo)
    {
        ...

        MethodInfo setMethod = propertyInfo.GetSetMethod();
        if (setMethod != null)
        {
            this.m_dynamicSetter = new DynamicMethodExecutor(setMethod);
        }
    }

    ...

    public void SetValue(object o, object value)
    {
        if (this.m_dynamicSetter == null)
        {
            throw new NotSupportedException("Cannot set the property.");
        }

        this.m_dynamicSetter.Execute(o, new object[] { value });
    }
}

在下面的评论中,Such Cloud已经想到了类似的做法,值得鼓励,同时多谢支持。