zoukankan      html  css  js  c++  java
  • 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 { getset; }
     6         public string Name { getset; }
     7         public DateTime Brithday { getset; }
     8         public People Son { getset; }
     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(199099) };
    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 != nullreturn 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(199099) };
    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 != nullreturn 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(199099) };
    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(199099) };
    2     p4.Son = new People { Id = 2, Name = "鹤小天", Brithday = new DateTime(201599) };
    3     p4.Son.Son = new People { Id = 3, Name = "鹤微天", Brithday = new DateTime(204099) };
    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)扩展带来便利性的同时,也会带来相应的性能损失,两者很难兼得。

     最后重申下,本系列文章,侧重想法,所给的代码仅供演示、参考,没有考虑性能、异常处理等,如需实际使用,请自行完善。 

     本人系列文章《c#扩展方法奇思妙用》,敬请关注!  

  • 相关阅读:
    git打补丁、还原补丁
    mysql 查两个表相同的值
    系统更新后vs2012无法打开方案资源管理器
    Node.js之Buffer
    html元素固定
    在windows上用netsh动态配置端口转发
    Git忽略规则及.gitignore规则不生效的解决办法
    MySQL5.7.10 初始化失败error
    Nginx和PHP-FPM的启动、重启、停止脚本分享
    centos添加nginx为系统服务
  • 原文地址:https://www.cnblogs.com/ldp615/p/1559020.html
Copyright © 2011-2022 走看看