程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> .NET網頁編程 >> C# >> 關於C# >> c#擴展方法奇思妙用高級篇五:ToString(string format) 擴展

c#擴展方法奇思妙用高級篇五:ToString(string format) 擴展

編輯:關於C#

在.Net中,System.Object.ToString()是用得最多的方法之一,ToString()方法在Object類中被定義為virtual,Object類給了它一個默認實現:

1 public virtual string ToString()
2 {
3  return this.GetType().ToString();
4 }

.Net中原生的class或struct,如int,DateTime等都對它進行重寫(override),以讓它返回更有價值的值,而不是類型的名稱。合理重寫的ToString()方法中編程、調試中給我們很大方便。但終究一個類只有一個ToString()方法,不能滿足我們多樣化的需求,很多類都對ToString()進行了重載。如下:

1 string dateString = DateTime.Now.ToString("yyyy");  //2009
2 string intString = 10.ToString("d4");  //0010

int、DateTime都實現了ToString(string format)方法,極大方便了我們的使用。

對於我們自己定義的類型,我們也應該提供一個合理的ToString()重寫,如果能夠提供再提供一個ToString(string format),就會令我們後期的工作更加簡單。試看以下類型: 

1 public class People
2 {
3  private List<People> friends = new List<People>();
4 
5  public int Id { get; set; }
6  public string Name { get; set; }
7  public DateTime Brithday { get; set; }
8  public People Son { get; set; }
9  public People[] Friends { get { return friends.ToArray(); } }
10
11  public void AddFriend(People newFriend)
12  {
13   if (friends.Contains(newFriend)) throw new ArgumentNullException("newFriend", "該朋友已添加");
14   else friends.Add(newFriend);
15  }
16  public override string ToString()
17  {
18   return string.Format("Id: {0}, Name: {1}", Id, Name);
19  }
20 
21 }

一個簡單的類,我們給出一個ToString()重寫,返回包含Id和Name兩個關鍵屬性的字符串。現在我們需要一個ToString(string format)重寫,以滿足以下應用:

1 People p = new People { Id = 1, Name = "鶴沖天", Brithday = new DateTime(1990, 9, 9) };
2 string s0 = p.ToString("Name 生日是 Brithday"); //理想輸出:鶴沖天 生日是 1990-9-9
3 string s1 = p.ToString("編號為:Id,姓名:Name"); //理想輸出:編號為:1,姓名:鶴沖天

想想怎麼實現吧,記住format是可變的,不定使用了什麼屬性,也不定進行了怎樣的組合...

也許一個類好辦,要是我們定義很多類,幾十、幾百個怎麼辦?一一實現ToString(string format)會把人累死的。好在我們有擴展方法,我們對object作一擴展ToString(string format),.Net中object是所有的基類,對它擴展後所有的類都會自動擁有了。當然已有ToString(string format)實現的不會,因為原生方法的優先級高,不會被擴展方法覆蓋掉。

來看如何實現吧(我們會一步一步改進,為區分各個版本,分別擴展為ToString1、ToString2...分別對應版本一、版本二...):

1 public static string ToString1(this object obj, string format)
2 {
3   Type type = obj.GetType();
4   PropertyInfo[] properties =  type.GetProperties(
5   BindingFlags.GetProperty | BindingFlags.Public | BindingFlags.Instance);
6
7   string[] names = properties.Select(p => p.Name).ToArray();
8   string pattern = string.Join("|", names);
9
10   MatchEvaluator evaluator = match =>
11   {
12    PropertyInfo property = properties.First(p => p.Name == match.Value);
13    object propertyValue = property.GetValue(obj, null);
14    if (propertyValue != null) return propertyValue.ToString();
15    else return "";
16   };
17   return Regex.Replace(format, pattern, evaluator);
18 }

3~5行通過反射獲取了公有的、實例的Get屬性(如果需要靜態的或私有的,修改第5行中即可),7~8行動態生成一個正則表達式來匹配format,10~16行是匹配成功後的處理。這裡用到反射和正則表達式,如果不熟悉不要緊,先調試運行吧,測試一下前面剛提到的應用:

第一個和我們理想的有點差距,就是日期上,我們應該給日期加上"yyyy-MM-dd"的格式,這個我們稍後改進,我們現在有一個更大的問題:

如果我們想輸出:“People: Id 1, Name 鶴沖天”,format怎麼寫呢?寫成format="People: Id Id, Name Name",這樣沒法處理了,format中兩個Id、兩個Name,哪個是常量,哪個是變量啊?解決這個問題,很多種方法,如使用轉義字符,可是屬性長了不好寫,如format="\B\r\i\t\h\d\a\y Brithday"。我權衡了一下,最後決定采用類似Sql中對字段名的處理方法,在這裡就是給變量加上中括號,如下:

1 People p2 = new People { Id = 1, Name = "鶴沖天", Brithday = new DateTime(1990, 9, 9) };
2 string s2 = p1.ToString2("People:Id [Id], Name [Name], Brithday [Brithday]");

版本二的實現代碼如下:

1 public static string ToString2(this object obj, string format)
2 {
3  Type type = obj.GetType();
4  PropertyInfo[] properties = type.GetProperties(
5  BindingFlags.GetProperty | BindingFlags.Public | BindingFlags.Instance);
6 
7  MatchEvaluator evaluator = match =>
8  {
9   string propertyName = match.Groups["Name"].Value;
10  PropertyInfo property = properties.FirstOrDefault(p => p.Name == propertyName);
11  if (property != null)
12   {
13    object propertyValue = property.GetValue(obj, null);
14    if (propertyValue != null) return propertyValue.ToString();
15    else return "";
16   }
17   else return match.Value;
18  };
19  return Regex.Replace(format, @"\[(?<Name>[^\]]+)\]", evaluator, RegexOptions.Compiled);
20 }

調試執行一下:

與版本一類似,不過這裡沒有動態構建正則表達式,因為有了中括號,很容易區分常量和變量,所以我們通過“屬性名”來找“屬性”(對應代碼中第10行)。如果某個屬性找不到,我們會將這“[Name]”原樣返回(對就第17行)。另一種做法是拋出異常,我不建議拋異常,在ToString(string format)是不合乎“常理”的。

版本二相對版本一效率有很大提高,主要是因為版本二只使用一個簡單的正則表達式:@"\[(?<Name>[^\]]+)\]"。而版本一中的如果被擴展類的屬性特別多,動態生成的正則表達式會很長,執行起來也會相對慢。

我們現在來解決兩個版本中都存在的時間日期格式問題,把時間日期格式"yyyy-MM-dd"也放入中括號中,測試代碼如下:

1 People p3 = new People { Id = 1, Name = "鶴沖天", Brithday = new DateTime(1990, 9, 9) };
2 string s3 = p3.ToString3("People:Id [Id: d4], Name [Name], Brithday [Brithday: yyyy-MM-dd]");

版本三實現代碼:

1 public static string ToString3(this object obj, string format)
2 {
3  Type type = obj.GetType();
4  PropertyInfo[] properties = type.GetProperties(
5  BindingFlags.GetProperty | BindingFlags.Public | BindingFlags.Instance);
6 
7  MatchEvaluator evaluator = match =>
8  {
9   string propertyName = match.Groups["Name"].Value;
10   string propertyFormat = match.Groups["Format"].Value;
11
12   PropertyInfo propertyInfo = properties.FirstOrDefault(p => p.Name == propertyName);
13   if (propertyInfo != null)
14   {
15    object propertyValue = propertyInfo.GetValue(obj, null);
16    if (string.IsNullOrEmpty(propertyFormat) == false)
17     return string.Format("{0:" + propertyFormat + "}", propertyValue);
18    else return propertyValue.ToString();
19   }
20   else return match.Value;
21  };
22  string pattern = @"\[(?<Name>[^\[\]:]+)(\s*:\s*(?<Format>[^\[\]:]+))?\]";
23  return Regex.Replace(format, pattern, evaluator, RegexOptions.Compiled);
24 }

測試一下,可OK了:

對於簡單的值類型屬性沒問題了,但對於復雜一些類型如,如People的屬性Son(Son就是兒子,我一開始寫成了Sun),他也是一個People類型,他也有屬性的,而且他也可能有Son...

先看下調用代碼吧:

1 People p4 = new People { Id = 1, Name = "鶴沖天", Brithday = new DateTime(1990, 9, 9) };
2 p4.Son = new People { Id = 2, Name = "鶴小天", Brithday = new DateTime(2015, 9, 9) };
3 p4.Son.Son = new People { Id = 3, Name = "鶴微天", Brithday = new DateTime(2040, 9, 9) };
4 string s4 = p4.ToString4("[Name] 的孫子 [Son.Son.Name] 的生日是:[Son.Son.Brithday: yyyy年MM月dd日]。");

“鶴沖天”也就是我了,有個兒子叫“鶴小天”,“鶴小天”有個兒子,也就是我的孫子“鶴微天”。哈哈,祖孫三代名字都不錯吧(過會先把小天、微天這兩個名字注冊了)!主要看第4行,format是怎麼寫的。下面是版本四實現代碼,由版本三改進而來:

1 public static string ToString4(this object obj, string format)
2 {
3  MatchEvaluator evaluator = match =>
4   {
5    string[] propertyNames = match.Groups["Name"].Value.Split('.');
6    string propertyFormat = match.Groups["Format"].Value;
7
8    object propertyValue = obj;
9    try
10    {
11     foreach (string propertyName in propertyNames)
12      propertyValue = propertyValue.GetPropertyValue(propertyName);
13    }
14    catch
15    {
16     return match.Value;
17    }
18
19    if (string.IsNullOrEmpty(format) == false)
20     return string.Format("{0:" + propertyFormat + "}", propertyValue);
21    else return propertyValue.ToString();
22 };
23  string pattern = @"\[(?<Name>[^\[\]:]+)(\s*[:]\s*(?<Format>[^\[\]:]+))?\]";
24  return Regex.Replace(format, pattern, evaluator, RegexOptions.Compiled);
25 }

為了反射獲取屬性方法,用到了GetPropertyValue擴展如下(版本三的實現用上這個擴展會更簡潔)(考慮性能請在此方法加緩存):

1 public static object GetPropertyValue(this object obj, string propertyName)
2 {
3  Type type = obj.GetType();
4  PropertyInfo info = type.GetProperty(propertyName);
5  return info.GetValue(obj, null);
6 }

先執行,再分析:

 

執行正確! 版本四,8~17行用來層層獲取屬性。也不太復雜,不多作解釋了。說明一下,版本四是不完善的,沒有做太多處理。

我們最後再來看一下更復雜的應用,Peoplee有Friends屬性,這是一個集合屬性,我們想獲取朋友的個數,並列出朋友的名字,如下:

1 People p5 = new People { Id = 1, Name = "鶴沖天"};
2 p5.AddFriend(new People { Id = 11, Name = "南霸天" });
3 p5.AddFriend(new People { Id = 12, Name = "日中天" });
4 string s5 = p5.ToString5("[Name] 目前有 [Friends: .Count] 個朋友:[Friends: .Name]。");

注意,行4中的Count及Name前都加了個小點,表示是將集合進行操作,這個小點是我看著方便自己定義的。再來看實現代碼,到版本五了:

1 public static string ToString5(this object obj, string format)
2  {
3   MatchEvaluator evaluator = match =>
4   {
5    string[] propertyNames = match.Groups["Name"].Value.Split('.');
6    string propertyFormat = match.Groups["Format"].Value;
7
8    object propertyValue = obj;
9 
10   try
11    {
12     foreach (string propertyName in propertyNames)
13      propertyValue = propertyValue.GetPropertyValue(propertyName);
14    }
15    catch
16    {
17     return match.Value;
18    }
19
20    if (string.IsNullOrEmpty(propertyFormat) == false)
21    {
22     if (propertyFormat.StartsWith("."))
23     {
24      string subPropertyName = propertyFormat.Substring(1);
25      IEnumerable<object> objs = ((IEnumerable)propertyValue).Cast<object>();
26      if (subPropertyName == "Count")
27       return objs.Count().ToString();
28      else
29       {
30        string[] subProperties = objs.Select(
31        o => o.GetPropertyValue(subPropertyName).ToString()).ToArray();
32        return string.Join(", ", subProperties);
33       }
34     }
35     else
36      return string.Format("{0:" + propertyFormat + "}", propertyValue);
37    }
38    else return propertyValue.ToString();
39  };
40  string pattern = @"\[(?<Name>[^\[\]:]+)(\s*[:]\s*(?<Format>[^\[\]:]+))?\]";
41  return Regex.Replace(format, pattern, evaluator, RegexOptions.Compiled);
42 }

執行結果:

比較不可思議吧,下面簡單分析一下。行22~行33是對集合進行操作的相關處理,這裡只是簡單實現了Count,當然也可以實現Min、Max、Sum、Average等等。“.Name”這個表示方法不太好,這裡主要是為了展示,大家能明白了就好。

就寫到這裡吧,版本六、版本七...後面還很多,當然一個比一個離奇,不再寫了。給出五個版本,版本一存在問題,主要看後三個版本,給出多個版本是為滿足不同朋友的需要,一般來說版本三足夠,對於要求比較高,追求新技術的朋友,我推薦版本四、五。要求更高的,就是沒給出的六、七...了。

ToString(string format)擴展帶來便利性的同時,也會帶來相應的性能損失,兩者很難兼得。

最後重申下,本系列文章,側重想法,所給的代碼僅供演示、參考,沒有考慮性能、異常處理等,如需實際使用,請自行完善。

  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved