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

 在.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
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值