原文:编写高质量代码改善C#程序的157个建议[为类型输出格式化字符串、实现浅拷贝和深拷贝、用dynamic来优化反射]
前言
本文已更新至http://www.cnblogs.com/aehyok/p/3624579.html 。本文主要学习记录以下内容:
建议13、为类型输出格式化字符串
建议14、正确实现浅拷贝和深拷贝
建议15、使用dynamic来简化反射实现
建议13、为类型输出格式化字符串
有两种方法可以为类型提供格式化的字符串输出。
一种是意识到类型会产生格式化字符串输出,于是让类型继承接口IFormattable。这对类型来说,是一种主动实现的方式,要求开发者可以预见类型在格式化方面的要求。
更多的时候,类型的使用者需为类型自定义格式化器,这就是第二种方法,也是最灵活多变的方法,可以根据需求的变化为类型提供多个格式化器。
下面我们就来看一下这两种方式的实现。
最简单的字符串输出是为类型重写ToString()方法,如果没有为类型重写该方法,默认会调用Ojbect的ToString方法,它会返回当前类型的类型名称。但即使是重写了ToString()方法,提供的字符串输出也是非常单一的,而通过实现IFormattable接口的ToString()方法,可以让类型根据用户的输入而格式化输出。
下面我们来看一个简单的小例子:
public class Person:IFormattable { public string IDCode { get; set; } public string FirstName { get; set; } public string LastName { get; set; } /// <summary> /// 实现接口Iformattable的方法ToString /// </summary> /// <param name="format"></param> /// <param name="formatProvider"></param> /// <returns></returns> public string ToString(string format, IFormatProvider formatProvider) { switch (format) { case"Ch": return this.ToString(); case"Eg": return string.Format("{0}{1}", this.FirstName, this.LastName); default: return this.ToString(); } } ///重写Object的方法ToString() public override string ToString() { return string.Format("{0}{1}",this.LastName,this.FirstName); } }
调用代码如下所示:
static void Main(string[] args) { Person person = new Person() { FirstName="Kris",LastName="aehyok"}; Console.WriteLine(person); Console.WriteLine(person.ToString("Ch",null)); Console.WriteLine(person.ToString("Eg", null)); Console.ReadLine(); }
调用执行结果如下:
下面我们来继续介绍第二实现方式——格式化器。如果类型本身没有提供格式化的功能,那么格式化器就可以派上用场了。格式化器的好处就是可以根据需求的变化,随时增加或者修改它。
接下来我们继续来看另外的一个小例子:
首先定义一个实体类Person:
public class Person { public string IDCode { get; set; } public string FirstName { get; set; } public string LastName { get; set; } }
一个典型的格式化器应该继承IFormatProvider和ICustomerFormatter,看代码:
public class PersonFomatter:IFormatProvider,ICustomFormatter { #region IFormatProvider成员 public object GetFormat(Type formatType) { if (formatType == typeof(ICustomFormatter)) { return this; } else { return null; } } #endregion #region ICustomFormatter成员 public string Format(string format, object arg, IFormatProvider formatProvider) { Person person = arg as Person; if (person == null) { return string.Empty; } switch (format) { case"Ch": return string.Format("{0} {1}",person.LastName,person.FirstName); case"": return string.Format("{0} {1}",person.FirstName,person.LastName); case"CHM": return string.Format("{0} {1}:{2}", person.LastName, person.FirstName, person.IDCode); default: return string.Format("{0} {1}", person.LastName, person.FirstName); } } #endregion }
调用代码如下:
class Program { static void Main(string[] args) { Person person = new Person() { FirstName="Kris", LastName="aehyok", IDCode="ID000001"}; Console.WriteLine(person.ToString()); PersonFomatter pFomatter = new PersonFomatter(); Console.WriteLine(pFomatter.Format("Ch", person, null)); Console.WriteLine(pFomatter.Format("Eg", person, null)); Console.WriteLine(pFomatter.Format("CHM", person, null)); Console.ReadLine(); } }
调用执行结果如下:
其实还有另外一种变通的形式,就是将这两种方式合并一起使用的过程,下面来看一下具体的实现代码:
public class Person:IFormattable { public string IDCode { get; set; } public string FirstName { get; set; } public string LastName { get; set; } /// <summary> /// 实现接口Iformattable的方法ToString /// </summary> /// <param name="format"></param> /// <param name="formatProvider"></param> /// <returns></returns> public string ToString(string format, IFormatProvider formatProvider) { switch (format) { case"Ch": return this.ToString(); case"Eg": return string.Format("{0}{1}", this.FirstName, this.LastName); default: //return this.ToString(); ICustomFormatter customerFormatter = formatProvider as ICustomFormatter; if (formatProvider == null) { return this.ToString(); } return customerFormatter.Format(format, this, null); } } ///重写Object的方法ToString() public override string ToString() { return string.Format("{0}{1}",this.LastName,this.FirstName); } }
PersonFomatter自定义格式化器的代码并没有发生任何的改变。
调用代码如下:
static void Main(string[] args) { Person person = new Person() { FirstName="Kris", LastName="aehyok", IDCode="ID000001"}; Console.WriteLine(person.ToString()); PersonFomatter pFomatter = new PersonFomatter(); Console.WriteLine(pFomatter.Format("Ch", person, null)); Console.WriteLine(pFomatter.Format("Eg", person, null)); Console.WriteLine(pFomatter.Format("CHM", person, null)); Console.WriteLine(person.ToString("Ch",pFomatter)); Console.WriteLine(person.ToString("Eg", pFomatter)); Console.WriteLine(person.ToString("CHM", pFomatter)); Console.ReadLine(); }
调用执行结果如下所示:
建议14、正确实现浅拷贝和深拷贝
为对象创建副本的技术成为拷贝(也叫克隆)。我们将拷贝分为浅拷贝和深拷贝。
浅拷贝 将对象中的所有字段复制到新的对象(副本)中。其中,值类型字段的值被复制到副本中后,在副本中的修改不会影响到源对象对应的值。 而引用类型的字段被复制到副本中的是引用类型的引用,而不是引用的对象,在副本中对引用类型的字段值做修改会影响到源对象本身。
深拷贝 同样,将对象中的所有字段复制到新的对象中。不过无论是对象的值类型字段,还是引用类型字段,都会被重新创建并赋值,对于副本的修改,不会影响到源对象本身。
无论是浅拷贝还是深拷贝,微软都建议用类型继承ICloneable接口的方式明确告诉调用者:该类型可以被拷贝。当然,ICloneable接口只提供了一个声明为Clone的方法,我们可根据需求在Clone方法内实现浅拷贝或深拷贝。一个简答的浅拷贝的实现代码如下所示:
首先定义实体类:
public class Employee:ICloneable { public string IDCode { get; set; } public int Age { get; set; } public Department Department { get; set; } #region OCloneable成员 public object Clone() { return this.MemberwiseClone(); } #endregion } public class Department { public string Name{get;set;} public override string ToString() { return this.Name; } }
然后进行调用代码如下:
static void Main(string[] args) { Employee Niki = new Employee() { IDCode = "IDaehyok", Age = 25, Department = new Department() { Name="Depart1" } }; Employee Kris = Niki.Clone() as Employee; Console.WriteLine(string.Format("IDCode:{0}\tAge:{1}\tDepartment:{2}", Kris.IDCode, Kris.Age, Kris.Department)); ///开始改变Niki的值 Niki.IDCode = "IDNiki"; Niki.Age = 23; Niki.Department.Name = "Depart2"; Console.WriteLine(string.Format("IDCode:{0}\tAge:{1}\tDepartment:{2}", Kris.IDCode, Kris.Age, Kris.Department)); Console.ReadLine(); }
调用执行结果如下
注意到Employee的IDCode属string类型。理论上string类型是引用类型,但是由于该引用类型的特殊性(无论是实际还是语义),Object.MemberwiseClone方法仍旧为其创建了副本。也就是说,在浅拷贝过程,我们应该将字符串看成是值类型。Employee的Department属性是一个引用类型,所以,如果改变了源对象Niki中的值,那么副本Kris中的值也会随之一起变动。
Employee的深拷贝有多种实现方法,最简单的方式是手动的对字段进行逐个的赋值。但是这种方法容易出错,也就是说,如果类型的字段发生变化或有增减,那么该拷贝方法也要发生相应的变化,所以,建议使用序列化的形式来进行深拷贝。Employee深拷贝的一种实现方式如下:
[Serializable] public class Employee:ICloneable { public string IDCode { get; set; } public int Age { get; set; } public Department Department { get; set; } #region OCloneable成员 public object Clone() { using (Stream objectstream = new MemoryStream()) { IFormatter formatter = new BinaryFormatter(); formatter.Serialize(objectstream, this); objectstream.Seek(0, SeekOrigin.Begin); return formatter.Deserialize(objectstream) as Employee; } } #endregion } [Serializable] public class Department { public string Name{get;set;} public override string ToString() { return this.Name; } }
调用方法如下所示:
Employee Niki = new Employee() { IDCode = "IDaehyok", Age = 25, Department = new Department() { Name="Depart1" } }; Employee Kris = Niki.Clone() as Employee; Console.WriteLine(string.Format("IDCode:{0}\tAge:{1}\tDepartment:{2}", Kris.IDCode, Kris.Age, Kris.Department)); ///开始改变Niki的值 Niki.IDCode = "IDNiki"; Niki.Age = 23; Niki.Department.Name = "Depart2"; Console.WriteLine(string.Format("IDCode:{0}\tAge:{1}\tDepartment:{2}", Kris.IDCode, Kris.Age, Kris.Department)); Console.ReadLine();
最终代码调用结果如下
可以发现再次改变Niki的值,不会对副本Kris产生影响。
由于接口ICloneable,只有一个模棱两可的方法,所以,如果要在一个类中进行浅拷贝和深拷贝,只能由我们额外的实现两个方法。声明为DeepClone和Shallow。那么最终代码如下所示:
[Serializable] public class Employee:ICloneable { public string IDCode { get; set; } public int Age { get; set; } public Department Department { get; set; } #region OCloneable成员 public object Clone() { return this.MemberwiseClone(); } #endregion public Employee DeeptClone() { using (Stream objectstream = new MemoryStream()) { IFormatter formatter = new BinaryFormatter(); formatter.Serialize(objectstream, this); objectstream.Seek(0, SeekOrigin.Begin); return formatter.Deserialize(objectstream) as Employee; } } public Employee Shallow() { return Clone() as Employee; } }
建议15、使用dynamic来简化反射实现
Dynamic是Framework4.0的新特性。dynamic的出现让C#具有了弱类型的特性。编译器在编译的时候不再对类型进行检查,编译器默认dynamic对象支持开发者想要的任何类型。如果运行时不包含指定的特性,运行时程序会抛出一个RuntimeBinderException异常。
下面我们先来看一个简单的例子
public class DynamicSample { public string Name { get; set; } public int Add(int a, int b) { return a + b; } }
现在我们想调用上面实体类的Add方法,实现方式可以是这样的:
DynamicSample dynamicSample = new DynamicSample(); var addMethod = typeof(DynamicSample).GetMethod("Add"); int re = (int)addMethod.Invoke(dynamicSample, new object[] { 1, 2 }); Console.WriteLine(re);
下面我们再通过使用dynamic来实现一下:
dynamic dynamic = new DynamicSample(); int re2 = dynamic.Add(1, 2); Console.WriteLine(re2);
可以发现dynamic的实现方式很简洁,而且性能也有所提升,当然上面一次的调用我们是看不出什么效果的,假如上面的代码我们进行调用了10000000次。
int times = 10000000; ////第一种调用方式 DynamicSample reflectSample = new DynamicSample(); var addMethod = typeof(DynamicSample).GetMethod("Add"); Stopwatch watch1 = Stopwatch.StartNew();///用于测试运行时间 for (var i = 0; i < times; i++) { addMethod.Invoke(reflectSample, new object[] { 1, 2 }); } Console.WriteLine(string.Format("普通方法反射耗时:{0} 毫秒", watch1.ElapsedMilliseconds)); ////第二种调用方式 dynamic dynamicSample = new DynamicSample(); Stopwatch watch2 = Stopwatch.StartNew(); for (int i = 0; i < times; i++) { dynamicSample.Add(1, 2); } Console.WriteLine(string.Format("dynamic方式耗时:{0} 毫秒", watch2.ElapsedMilliseconds)); ////第三种调用方式 DynamicSample reflectSampleBetter = new DynamicSample(); var addMethod2 = typeof(DynamicSample).GetMethod("Add"); var delg = (Func<DynamicSample, int, int, int>)Delegate.CreateDelegate(typeof(Func<DynamicSample, int, int, int>), addMethod2); Stopwatch watch3 = Stopwatch.StartNew(); for (var i = 0; i < times; i++) { delg(reflectSampleBetter, 1, 2); } Console.WriteLine(string.Format("优化的反射耗时:{0} 毫秒", watch3.ElapsedMilliseconds)); Console.ReadLine();
调用执行后的结果为
现在可以看出很明显的区别,普通方法调用发射执行效率远远的低于使用dynamic。第三种方式是我们优化了发射之后的执行时间,比使用dynamic也有所提升,但是并不是特别明显,虽然带来了性能的提升,不过却牺牲了代码的整洁性。这种实现方式在我看来是得不偿失的。所以建议大家使用dynamic来优化发射。
到这里为止,第一章的内容暂时已经整理完毕,感觉自己学了不少知识,继续加油,接下来继续进行第二章集合和LINQ的学习。