寫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") %>
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 %>
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 %>
Conent: <%# (Container.DataItem as Comment).Content %>
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 });
}
}