這下沒理由嫌Eval的性能差了吧?
好吧,你偏要說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
這個方法是不是比較眼熟?沒錯,我在《方法的直接調用,反射調用與……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已經想到了類似的做法,值得鼓勵,同時多謝支持。