打赏

数据类型转换

from:https://www.cnblogs.com/xiadao521/p/4092846.html

上一篇介绍了数据类型转换的一些情况,可以看出,如果不进行封装,有可能导致比较混乱的代码。本文通过TDD方式把数据类型转换公共操作类开发出来,并提供源码下载。

  我们在 应用程序框架实战十一:创建VS解决方案与程序集 一文已经创建了解决方案,包含一个类库项目和一个单元测试项目。单元测试将使用.Net自带的 MsTest,另外通过Resharper工具来观察测试结果。

  首先考虑我们期望的API长成什么样子。基于TDD开发,其中一个作用是帮助程序员设计期望的API,这称为意图导向编程。

  因为数据类型转换是Convert,所以我们先在单元测试项目中创建一个ConvertTest的类文件。 

  类创建好以后,我先随便创建一个方法Test,以迅速展开工作。测试的方法名Test,我是随便起的,因为现在还不清楚API是什么样,我一会再回过头来改。

复制代码
using Microsoft.VisualStudio.TestTools.UnitTesting;
namespace Util.Tests {
    /// <summary>
    /// 类型转换公共操作类测试
    /// </summary>
    [TestClass]
    public class ConvertTest {
        [TestMethod]
        public void Test() {
        }
    }
}     
复制代码

  为了照顾还没有使用单元测试的朋友,我在这里简单介绍一下MsTest。MsTest是.Net仿照JUnit打造的一个单元测试框架。在单元测试类上需要添加一个TestClass特性,在测试方法上添加TestMethod特性,用来识别哪些类的操作需要测试。还有一些其它特性,在用到的时候我再介绍。

  现在先来实现一个最简单的功能,把字符串”1”转换为整数1。

[TestMethod]
public void Test() {
    Assert.AreEqual( 1, Util.ConvertHelper.ToInt( "1" ) );
}

  我把常用公共操作类尽量放到顶级命名空间Util,这样我就可以通过编写Util.来弹出代码提示,这样我连常用类也不用记了。

  使用ConvertHelper是一个常规命名,大多数开发人员可以理解它是一个类型转换的公共操作类。我也这样用了多年,不过后面我发现Util.ConvertHelper有点啰嗦,所以我简化成Util.Convert,但Convert又和系统重名了,所以我现在使用Util.Conv,你不一定要按我的这个命名,你可以使用ConvertHelper这样的命名以提高代码清晰度。

  System.Convert使用ToInt32来精确表示int是一个32位的数字,不过我们的公共操作类不用这样精确,ToInt就可以了,如果要封装ToInt64呢,我就用ToLong,这样比较符合我的习惯。

  现在代码被简化成了下面的代码。

Assert.AreEqual( 1, Util.Conv.ToInt( "1" ) );

  Assert在测试中用来断言,断言就是比较实际计算出来的值是否和预期一致,Assert包含大量比较方法,AreEqual使用频率最高,用来比较预期值(左边)与实际值(右边)是否值相等,还有一个AreSame方法用来比较是否引用相等。   

     

  由于Conv类还未创建,所以显示一个红色警告。   现在在Util类库项目中创建一个Conv类。

  创建了Conv类以后,单元测试代码检测到Conv,但ToInt方法未创建,所以红色警告转移到ToInt方法。

  现在用鼠标左键单击红色ToInit方法,Resharper在左侧显示一个红色的灯泡。

  单击红色灯泡提示,选择第一项”Create Method ‘Conv.ToInt’”。

  Resharper会在Conv类中自动创建一个ToInt方法。

public class Conv {
    public static int ToInt( string s ) {
        throw new NotImplementedException();
     }
}    

  方法体抛出一个未实现的异常,这正是我们想要的。TDD的口诀是“红、绿、重构”,第一步需要先保证方法执行失败,显示红色警告。至于未何需要测试先行,以及首先执行失败,牵扯TDD开发价值观,请大家参考相关资料。

  准备工作已经就绪,现在可以运行测试了。安装了Resharper以后,在添加了TestClass特性的左侧,会看见两个重叠在一起的圆形图标。另外,在TestMethod特性左侧,有一个黑白相间的圆形图标。

   单击Test方法左侧的图标,然后点击Run按钮。如果单击TestClass特性左侧的图标,会运行该类所有测试。

  测试开始运行,并显示红色警告,提示未实现的异常,第一步完成。

  为了实现功能,现在来添加ToInt方法的代码。

public static int ToInt( string s ) {
    int result;
    int.TryParse( s, out result );
    return result;
}

  再次运行测试,已经能够成功通过,第二步完成。 

  第三步是进行重构,现在看哪些地方可以重构。参数s看起来有点不爽,改成data,并添加XML注释。

复制代码
        /// <summary>
        /// 转换为整型
        /// </summary>
        /// <param name="data">数据</param>
        public static int ToInt( string data ) {
            int result;
            int.TryParse( data, out result );
            return result;
        }    
复制代码

  另外重构一下测试,为了更容易找到相关测试,一般测试文件名使用类名+Test,现在测试文件名改成ConvTest.cs,测试类名改成ConvTest。把测试方法名改成TestToInt,并添加XML注释。

复制代码
        /// <summary>
        /// 测试转换为整型
        /// </summary>
        [TestMethod]
        public void TestToInt() {
            Assert.AreEqual( 1, Util.Conv.ToInt( "1" ) );
        }    
复制代码

  关于测试的命名,很多著作都提出了自己不同的方法。在《.Net单元测试艺术》中,作者建议使用三部分进行组合命名。还有一些著作建议将测试内容用下划线分隔单词,拼成一个长句子,以方便阅读和理解。这可能对英文水平好的人很有效,不过我的英文水平很烂,我拿一些单词拼成一个长句以后,发现更难理解了。所以我所采用的测试方法命名可能不一定好,你可以按你容易理解的方式来命名。

  重构之后,需要重新测试代码,以观察是否导致失败。

  上面简单介绍了TDD的一套开发流程,主要为了照顾还没有体验过单元测试的人,后面直接粘贴代码,以避免这样低效的叙述方式。

  单元测试代码如下。

 ConvTest

  Conv类代码如下。

复制代码
  1 using System;
  2 using System.Collections.Generic;
  3 using System.Linq;
  4 
  5 namespace Util {
  6     /// <summary>
  7     /// 类型转换
  8     /// </summary>
  9     public static class Conv {
 10 
 11         #region 数值转换
 12 
 13         /// <summary>
 14         /// 转换为整型
 15         /// </summary>
 16         /// <param name="data">数据</param>
 17         public static int ToInt( object data ) {
 18             if ( data == null )
 19                 return 0;
 20             int result;
 21             var success = int.TryParse( data.ToString(), out result );
 22             if ( success == true )
 23                 return result;
 24             try {
 25                 return Convert.ToInt32( ToDouble( data, 0 ) );
 26             }
 27             catch ( Exception ) {
 28                 return 0;
 29             }
 30         }
 31 
 32         /// <summary>
 33         /// 转换为可空整型
 34         /// </summary>
 35         /// <param name="data">数据</param>
 36         public static int? ToIntOrNull( object data ) {
 37             if ( data == null )
 38                 return null;
 39             int result;
 40             bool isValid = int.TryParse( data.ToString(), out result );
 41             if ( isValid )
 42                 return result;
 43             return null;
 44         }
 45 
 46         /// <summary>
 47         /// 转换为双精度浮点数
 48         /// </summary>
 49         /// <param name="data">数据</param>
 50         public static double ToDouble( object data ) {
 51             if ( data == null )
 52                 return 0;
 53             double result;
 54             return double.TryParse( data.ToString(), out result ) ? result : 0;
 55         }
 56 
 57         /// <summary>
 58         /// 转换为双精度浮点数,并按指定的小数位4舍5入
 59         /// </summary>
 60         /// <param name="data">数据</param>
 61         /// <param name="digits">小数位数</param>
 62         public static double ToDouble( object data, int digits ) {
 63             return Math.Round( ToDouble( data ), digits );
 64         }
 65 
 66         /// <summary>
 67         /// 转换为可空双精度浮点数
 68         /// </summary>
 69         /// <param name="data">数据</param>
 70         public static double? ToDoubleOrNull( object data ) {
 71             if ( data == null )
 72                 return null;
 73             double result;
 74             bool isValid = double.TryParse( data.ToString(), out result );
 75             if ( isValid )
 76                 return result;
 77             return null;
 78         }
 79 
 80         /// <summary>
 81         /// 转换为高精度浮点数
 82         /// </summary>
 83         /// <param name="data">数据</param>
 84         public static decimal ToDecimal( object data ) {
 85             if ( data == null )
 86                 return 0;
 87             decimal result;
 88             return decimal.TryParse( data.ToString(), out result ) ? result : 0;
 89         }
 90 
 91         /// <summary>
 92         /// 转换为高精度浮点数,并按指定的小数位4舍5入
 93         /// </summary>
 94         /// <param name="data">数据</param>
 95         /// <param name="digits">小数位数</param>
 96         public static decimal ToDecimal( object data, int digits ) {
 97             return Math.Round( ToDecimal( data ), digits );
 98         }
 99 
100         /// <summary>
101         /// 转换为可空高精度浮点数
102         /// </summary>
103         /// <param name="data">数据</param>
104         public static decimal? ToDecimalOrNull( object data ) {
105             if ( data == null )
106                 return null;
107             decimal result;
108             bool isValid = decimal.TryParse( data.ToString(), out result );
109             if ( isValid )
110                 return result;
111             return null;
112         }
113 
114         /// <summary>
115         /// 转换为可空高精度浮点数,并按指定的小数位4舍5入
116         /// </summary>
117         /// <param name="data">数据</param>
118         /// <param name="digits">小数位数</param>
119         public static decimal? ToDecimalOrNull( object data, int digits ) {
120             var result = ToDecimalOrNull( data );
121             if ( result == null )
122                 return null;
123             return Math.Round( result.Value, digits );
124         }
125 
126         #endregion
127 
128         #region Guid转换
129 
130         /// <summary>
131         /// 转换为Guid
132         /// </summary>
133         /// <param name="data">数据</param>
134         public static Guid ToGuid( object data ) {
135             if ( data == null )
136                 return Guid.Empty;
137             Guid result;
138             return Guid.TryParse( data.ToString(), out result ) ? result : Guid.Empty;
139         }
140 
141         /// <summary>
142         /// 转换为可空Guid
143         /// </summary>
144         /// <param name="data">数据</param>
145         public static Guid? ToGuidOrNull( object data ) {
146             if ( data == null )
147                 return null;
148             Guid result;
149             bool isValid = Guid.TryParse( data.ToString(), out result );
150             if ( isValid )
151                 return result;
152             return null;
153         }
154 
155         /// <summary>
156         /// 转换为Guid集合
157         /// </summary>
158         /// <param name="guid">guid集合字符串,范例:83B0233C-A24F-49FD-8083-1337209EBC9A,EAB523C6-2FE7-47BE-89D5-C6D440C3033A</param>
159         public static List<Guid> ToGuidList( string guid ) {
160             var listGuid = new List<Guid>();
161             if ( string.IsNullOrWhiteSpace( guid ) )
162                 return listGuid;
163             var arrayGuid = guid.Split( ',' );
164             listGuid.AddRange( from each in arrayGuid where !string.IsNullOrWhiteSpace( each ) select new Guid( each ) );
165             return listGuid;
166         }
167 
168         #endregion
169 
170         #region 日期转换
171 
172         /// <summary>
173         /// 转换为日期
174         /// </summary>
175         /// <param name="data">数据</param>
176         public static DateTime ToDate( object data ) {
177             if ( data == null )
178                 return DateTime.MinValue;
179             DateTime result;
180             return DateTime.TryParse( data.ToString(), out result ) ? result : DateTime.MinValue;
181         }
182 
183         /// <summary>
184         /// 转换为可空日期
185         /// </summary>
186         /// <param name="data">数据</param>
187         public static DateTime? ToDateOrNull( object data ) {
188             if ( data == null )
189                 return null;
190             DateTime result;
191             bool isValid = DateTime.TryParse( data.ToString(), out result );
192             if ( isValid )
193                 return result;
194             return null;
195         }
196 
197         #endregion
198 
199         #region 布尔转换
200 
201         /// <summary>
202         /// 转换为布尔值
203         /// </summary>
204         /// <param name="data">数据</param>
205         public static bool ToBool( object data ) {
206             if ( data == null )
207                 return false;
208             bool? value = GetBool( data );
209             if ( value != null )
210                 return value.Value;
211             bool result;
212             return bool.TryParse( data.ToString(), out result ) && result;
213         }
214 
215         /// <summary>
216         /// 获取布尔值
217         /// </summary>
218         private static bool? GetBool( object data ) {
219             switch ( data.ToString().Trim().ToLower() ) {
220                 case "0":
221                     return false;
222                 case "1":
223                     return true;
224                 case "是":
225                     return true;
226                 case "否":
227                     return false;
228                 case "yes":
229                     return true;
230                 case "no":
231                     return false;
232                 default:
233                     return null;
234             }
235         }
236 
237         /// <summary>
238         /// 转换为可空布尔值
239         /// </summary>
240         /// <param name="data">数据</param>
241         public static bool? ToBoolOrNull( object data ) {
242             if ( data == null )
243                 return null;
244             bool? value = GetBool( data );
245             if ( value != null )
246                 return value.Value;
247             bool result;
248             bool isValid = bool.TryParse( data.ToString(), out result );
249             if ( isValid )
250                 return result;
251             return null;
252         }
253 
254         #endregion
255 
256         #region 字符串转换
257 
258         /// <summary>
259         /// 转换为字符串
260         /// </summary>
261         /// <param name="data">数据</param>
262         public static string ToString( object data ) {
263             return data == null ? string.Empty : data.ToString().Trim();
264         }
265 
266         #endregion
267 
268         #region 通用转换
269 
270         /// <summary>
271         /// 泛型转换
272         /// </summary>
273         /// <typeparam name="T">目标类型</typeparam>
274         /// <param name="data">数据</param>
275         public static T To<T>( object data ) {
276             if ( data == null || string.IsNullOrWhiteSpace( data.ToString() ) )
277                 return default( T );
278             Type type = Nullable.GetUnderlyingType( typeof( T ) ) ?? typeof( T );
279             try {
280                 if ( type.Name.ToLower() == "guid" )
281                     return (T)(object)new Guid( data.ToString() );
282                 if ( data is IConvertible )
283                     return (T)Convert.ChangeType( data, type );
284                 return (T)data;
285             }
286             catch {
287                 return default( T );
288             }
289         }
290 
291         #endregion
292     }
293 }
复制代码

  Conv公共操作类的用法,在单元测试中已经说得很清楚了,这也是单元测试的一个用途,即作为API说明文档。

  单元测试最强大的地方,可能是能够帮助你回归测试,如果你发现我的代码有BUG,请通知我一声,我只需要在单元测试中增加一个测试来捕获这个BUG,就可以永久修复它,并且由于采用TDD方式可以获得很高的测试覆盖率,所以我花上几秒钟运行一下全部测试,就可以知道这次修改有没有影响其它代码。这也是你创建自己的应用程序框架所必须要做的,它可以给你提供信心。

  可以看到,我在单元测试中进行了很多边界测试,比如参数为null或空字符串等。但不可能穷举所有可能出错的情况,因为可能想不到,另外时间有限,也不可能做到。当在项目上发现BUG后,再通过添加单元测试的方式修复BUG就可以了。由于你的项目代码调用的是应用程序框架API,所以你只需要在框架内修复一次,项目代码完全不动。

  像数据类型转换这样简单的操作,你发现写单元测试非常容易,因为它有明确的返回值,但如果没有返回值呢,甚至有外部依赖呢,那就没有这么简单了,需要很多技巧,所以你多看几本TDD和单元测试方面的著作有很多好处。

  另外,再补充一下,Conv这个类里面有几个法宝。一个是ToGuidList这个方法,当你需要把字符串转换为List<Guid>的时候就用它。还有一个泛型转换的方法To<T>,很多时候可以用它进行泛型转换。

  最后,我把所有方法参数类型都改成了object,主要是想使用起来方便一点,而不是只支持字符串参数,这可能导致装箱和拆箱,从而造成一些性能损失,不过我的大多数项目在性能方面还没有这么高的要求,所以这个损失对我来讲无关痛痒。

  还有些数据类型转换,我没有放进来,主要是我平时很少用到,当我用到时再增加

posted @ 2019-05-23 11:35  刘奇云  阅读(875)  评论(0编辑  收藏  举报