编写高质量代码改善C#程序的157个建议[为类型输出格式化字符串、实现浅拷贝和深拷贝、用dynamic来优化反射]

原文:编写高质量代码改善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的学习。

 

 

时间: 2024-10-30 09:21:24

编写高质量代码改善C#程序的157个建议[为类型输出格式化字符串、实现浅拷贝和深拷贝、用dynamic来优化反射]的相关文章

编写高质量代码改善C#程序的157个建议[匿名类型、Lambda、延迟求值和主动求值]

原文:编写高质量代码改善C#程序的157个建议[匿名类型.Lambda.延迟求值和主动求值] 前言 从.NET3.0开始,C#开始一直支持一个新特性:匿名类型.匿名类型由var.赋值运算符和一个非空初始值(或以new开头的初始化项)组成.匿名类型有如下基本特性: 1.既支持简单类型也支持复杂类型.简单类型必须是一个非空初始值,复杂类型则是一个以new开头的初始化项. 2.匿名类型的属性是只读的,没有属性设置器,它一旦倍初始化就不可更改. 3.如果两个匿名类型的属性值相同,那么就任务这两个匿名类型

编写高质量代码改善C#程序的157个建议[4-9]

原文:编写高质量代码改善C#程序的157个建议[4-9] 前言 本文首先亦同步到http://www.cnblogs.com/aehyok/p/3624579.html.本文主要来学习记录一下内容: 建议4.TryParse比Parse好 建议5.使用int?来确保值类型也可以为null 建议6.区别readonly和const的使用方法 建议7.将0值设为枚举的默认值 建议8.避免给枚举类型的元素提供显式的值 建议9.习惯重载运算符 建议4.TryParse比Parse好 如果注意观察,除st

编写高质量代码改善C#程序的157个建议[C#闭包的陷阱、委托、事件、事件模型]

原文:编写高质量代码改善C#程序的157个建议[C#闭包的陷阱.委托.事件.事件模型] 前言 本文已更新至http://www.cnblogs.com/aehyok/p/3624579.html .本文主要学习记录以下内容: 建议38.小心闭包中的陷阱 建议39.了解委托的实质 建议40.使用event关键字对委托施加保护 建议41.实现标准的事件模型 建议38.小心闭包中的陷阱 首先我们先来看一段代码: class Program { static void Main(string[] arg

编写高质量代码改善C#程序的157个建议[IEnumerable&lt;T&gt;和IQueryable&lt;T&gt;、LINQ避免迭代、LINQ替代迭代]

原文:编写高质量代码改善C#程序的157个建议[IEnumerable<T>和IQueryable<T>.LINQ避免迭代.LINQ替代迭代] 前言 本文已更新至http://www.cnblogs.com/aehyok/p/3624579.html .本文主要学习记录以下内容: 建议29.区别LINQ查询中的IEnumerable<T>和IQueryable<T> 建议30.使用LINQ取代集合中的比较器和迭代器 建议31.在LINQ查询中避免不必要的迭代

编写高质量代码改善C#程序的157个建议[为泛型指定初始值、使用委托声明、使用Lambda替代方法和匿名方法]

原文:编写高质量代码改善C#程序的157个建议[为泛型指定初始值.使用委托声明.使用Lambda替代方法和匿名方法] 前言 泛型并不是C#语言一开始就带有的特性,而是在FCL2.0之后实现的新功能.基于泛型,我们得以将类型参数化,以便更大范围地进行代码复用.同时,它减少了泛型类及泛型方法中的转型,确保了类型安全.委托本身是一种引用类型,它保存的也是托管堆中对象的引用,只不过这个引用比较特殊,它是对方法的引用.事件本身也是委托,它是委托组,C#中提供了关键字event来对事件进行特别区分.一旦我们

编写高质量代码改善C#程序的157个建议[动态数组、循环遍历、对象集合初始化]

原文:编写高质量代码改善C#程序的157个建议[动态数组.循环遍历.对象集合初始化] 前言   软件开发过程中,不可避免会用到集合,C#中的集合表现为数组和若干集合类.不管是数组还是集合类,它们都有各自的优缺点.如何使用好集合是我们在开发过程中必须掌握的技巧.不要小看这些技巧,一旦在开发中使用了错误的集合或针对集合的方法,应用程序将会背离你的预想而运行. 本文已更新至http://www.cnblogs.com/aehyok/p/3624579.html .本文主要学习记录以下内容: 建议16.

编写高质量代码改善C#程序的157个建议[10-12]

原文:编写高质量代码改善C#程序的157个建议[10-12] 前言 本文已更新至http://www.cnblogs.com/aehyok/p/3624579.html .本文主要学习记录以下内容: 建议10.创建对象时需要考虑是否实现比较器 建议11.区别对待==和Equals 建议12.重写Equals时也要重写GetHashCode 建议10.创建对象时需要考虑是否实现比较器 有对象的地方就会存在比较,就像小时候每次拿着考卷回家,妈妈都会问你隔壁的那谁谁谁考了多少分呀.下面我们也来举个简单

编写高质量代码改善C#程序的157个建议[泛型集合、选择集合、集合的安全]

原文:编写高质量代码改善C#程序的157个建议[泛型集合.选择集合.集合的安全] 前言   软件开发过程中,不可避免会用到集合,C#中的集合表现为数组和若干集合类.不管是数组还是集合类,它们都有各自的优缺点.如何使用好集合是我们在开发过程中必须掌握的技巧.不要小看这些技巧,一旦在开发中使用了错误的集合或针对集合的方法,应用程序将会背离你的预想而运行. 本文已更新至http://www.cnblogs.com/aehyok/p/3624579.html .本文主要学习记录以下内容: 建议20.使用

编写高质量代码改善C#程序的157个建议[协变和逆变]

原文:编写高质量代码改善C#程序的157个建议[协变和逆变] 前言 本文已更新至http://www.cnblogs.com/aehyok/p/3624579.html .本文主要学习记录以下内容: 建议42.使用泛型参数兼容泛型接口的不可变性 建议43.让接口中的泛型参数支持协变 建议44.理解委托中的协变 建议45.为泛型类型参数指定协变 建议42.使用泛型参数兼容泛型接口的不可变性 让返回值类型返回比声明的类型派生程度更大的类型,就是"协变".协变不是一种新出现的技术,在以往的编