深刻理解C#:编程技巧总结(二)

原创文章,转载请注明出处! 如下总结参阅了:MSDN文档、《C#高级编程》、《C#本质论》、前辈们的博客等资料,若有不正确的地方,请帮忙及时指出!以避免误导!

在上一篇 深刻理解C#:编程技巧总结(一) 中总结了25点,这一篇继续:

26.系列化与反系列化

  • 使用的场合:
    便于保存,把持有运行状态的对象系列化后保存到本地,在下次运行程序时,反系列化该对象来恢复状态
    便于传输,在网络中传输系列化后的对象,接收方反系列化该对象还原
    复制黏贴,复制到剪贴板,而后黏贴html

  • 用来辅助系列化和反系列化的特性:在System.Runtime.Serialization命名空间下
    OnDeserialized,应用于某个方法,该方法会在反系列化后当即被自动调用(可用于处理生成的对象的成员)
    OnDeserializing,应用于某个方法,该方法会在执行反系列化时被自动调用
    OnSerialized,应用于某个方法,对象在被系列化后调用该方法
    OnSerializing,应用于某个方法,在系列化对象前调用该方法
    若是以上辅助特性仍不能知足需求,那就要为目标对象实现ISerializable接口了算法

  • ISerializable接口:该接口运行对象本身控制系列化与反系列化的过程(实现该接口的同时也必须应用Serializable特性)数据库

原理:若系列化一个对象时,发现对象实现了ISerializable接口,则会忽略掉类型全部的系列化特性应用,转而调用类型的GetObjectData()接口方法,该方法会构造一个SerializationInfo对象,方法内部负责对该对象设置须要系列化的字段,而后系列化器根据该对象来系列化。反系列化时,若发现反系列化后的对象实现了ISerializable接口,则反系列化器会把数据反系列化为SerializationInfo类型的对象,而后调用匹配的构造函数来构造目标类型的对象。编程

系列化:须要实现GetObjectData()方法,该方法在对象被系列化时自动被调用
void ISerializable.GetObjectData(SerializationInfo info, StreamingContext context){ }c#

反序列化:须要为它定义一个带参数的受保护的构造函数,用于反系列化后从新构造对象
protected Person(SerializationInfo info, StreamingContext context) { }
利用该接口,能够实现将数据流反系列化为其余任意指定对象(在原对象定义GetObjectData()方法,在目标对象定义用于反系列化的构造函数,但两个对象都必须实现ISerializable接口)
注意:
若父类为实现ISerializable接口,只有子类实现ISerializable接口,若想系列化从父类继承的字段,则须要在子类的反系列化构造器中和GetObjectData()方法中,添加继承自父类的字段的处理代码
若父类也实现了ISerializable接口,则只需在子类的反系列化构造器中和GetObjectData()方法中调用父类的版本便可windows

public class Class1
{
    public static void Main()
    {
        Person person = new Person() { FirstName = "RuiFu", LastName = "Su"};
        //系列化person对象并存进文件中,MyBinarySerializer为自定义工具类
        MyBinarySerializer.SerializeToFile<Person>(person, @"c:\", "Person.txt");
        //从文件中取出数据反系列化为Man类型的对象
        Man man = MyBinarySerializer.DeserializeFromFile<Man>(@"c:\Person.txt");
        Console.WriteLine(man.Name);
        Console.ReadKey();
    }
}
[Serializable]
public class Man:ISerializable
{
    public string Name;
    protected Man(SerializationInfo info,StreamingContext context)
    {
        Name = info.GetString("Name");
    }
    void ISerializable.GetObjectData(SerializationInfo info,StreamingContext context)
    { }
}
[Serializable]
public class Person:ISerializable
{
    public string FirstName;
    public string LastName;
    void ISerializable.GetObjectData(SerializationInfo info, StreamingContext context)
    {
        //设置反系列化后的对象类型
        info.SetType(typeof(Man)); 
        //根据原对象的成员来为info对象添加字段(这些字段将被系列化)
        info.AddValue("Name", string.Format("{0} {1}", LastName, FirstName));
    }
}

不该该被系列化的成员:
为了节省空间、流量,若是一个字段反系列化后对保存状态无心义,就不必系列化它
若是一个字段能够经过其它字段推算出来,则不必系列化它,而用OnDeserializedAttribute特性来触发推算方法执行
对于私密信息不该该被系列化
若成员对应的类型自己未被设置为可系列化,则应该把他标注为不可系列化[NonSerialized],不然运行时会抛出SerializationException
把属性设置为不可系列化:把它的后备字段设置为不可系列化便可实现
把事件设置为不可系列化:[field:NonSerialized]网络

正常系列化与反系列化示例:自定义了工具类MyBinarySerializer多线程

using System;
using System.Collections.Generic;
using System.IO;
using System.Runtime.Serialization.Formatters.Binary;
using System.Runtime.Serialization;
public class Class1
{
    public static void Main()
    {
        Person person1 = new Person() { 
            FirstName = "RuiFu", LastName = "Su", FullName = "Su RuiFU",IDCode="0377"};
        //系列化person1并存进文件中
        MyBinarySerializer.SerializeToFile<Person>(person1, @"c:\", "Person.txt");
        //从文件中取出数据反系列化为对象(文件中不含FullName信息,但系列化后自动执行了预约义的推算方法)
        Person person2 = MyBinarySerializer.DeserializeFromFile<Person>(@"c:\Person.txt");
        Console.WriteLine(person2.FullName);
        Console.ReadKey();
    }
}
[Serializable]
public class Person
{
    public string FirstName;
    public string LastName;
    [NonSerialized] //禁止被系列化
    public string FullName; //可被以上2个字段推算出来
    [OnDeserialized] //反系列化后将被调用的方法
    void GetFullName(StreamingContext context)
    {
        FullName = string.Format("{0} {1}", LastName, FirstName);
    }
    [NonSerialized]
    private string idCode;
    public string IDCode
    {
        get
        {
            return idCode;
        }
        set
        {
            idCode = value;
        }
    }
    [field: NonSerialized]
    public event EventHandler NameChanged;
}
//自定义的系列化与反系列化工具类
public class MyBinarySerializer
{
    //将类型系列化为字符串
    public static string Serialize<T>(T t)
    {
        using(MemoryStream stream = new MemoryStream())
        {
            BinaryFormatter formatter = new BinaryFormatter();
            formatter.Serialize(stream, t);
            return System.Text.Encoding.UTF8.GetString(stream.ToArray());
        }
    }
    //将类型系列化为文件
    public static void SerializeToFile<T>(T t, string path, string fullName)
    {
        if(!Directory.Exists(path))
        {
            Directory.CreateDirectory(path);
        }
        string fullPath = string.Format(@"{0}\{1}", path, fullName);
        using(FileStream stream = new FileStream(fullPath,FileMode.OpenOrCreate))
        {
            BinaryFormatter formatter = new BinaryFormatter();
            formatter.Serialize(stream, t);
            stream.Flush();
        }
    }
    //将字符串反系列化为类型
    public static TResult Deserialize<TResult>(string s) where TResult: class
    {
        byte[] bs = System.Text.Encoding.UTF8.GetBytes(s);
        using(MemoryStream stream = new MemoryStream())
        {
            BinaryFormatter formatter = new BinaryFormatter();
            return formatter.Deserialize(stream) as TResult;
        }
    }
    //将文件反系列化为类型
    public static TResult DeserializeFromFile<TResult>(string path) where TResult: class
    {
        using(FileStream stream = new FileStream(path,FileMode.Open))
        {
            BinaryFormatter formatter = new BinaryFormatter();
            return formatter.Deserialize(stream) as TResult;
        }
    }
}

27.异常处理:抛出异常是须要消耗性能的(但相对于低几率事件,这点性能影响是微不足道的)

  • 不要利用异常处理机制来实现控制流的转移
  • 不要对能预知到的大几率、可恢复的错误抛出异常,而应该用实际代码来处理可能出现的错误
  • 仅在为了防止出现小几率预知错误、没法预知的错误和没法处理的状况才尝试抛出异常(如:运行代码会形成内存泄漏、资源不可用、应用程序状态不可恢复,则须要抛出异常)
  • 若要把错误呈现给最终的用户,则应该先捕获该异常,对敏感信息进行包装后,从新抛出一个显示友好信息的新异常
  • 底层代码引起的异常对于高层代码没有意义时,则能够捕获该异常,并从新引起意思明确的异常
  • 在从新引起异常时,老是为新异常对象提供Inner Exception对象参数(不须要提供信息时最好直接用空的throw;语句,它会把原始异常对象从新抛出),该对象保存了旧异常的一切信息,包括异常调用栈,便于代码调试
  • 用异常处理代替返回错误代码的方式,由于返回错误代码不利于维护
  • 千万不要捕获在当前上下文中没法处理的异常,不然就可能制造了一个隐藏的很深的BUG
  • 避免使用多层嵌套的try...catch,嵌套多了,谁都会蒙掉
  • 对于正常的业务逻辑,使用Test-Doer模式来代替抛出异常
private bool CheckNumber(int number, ref string message)
{
    if(number < 0)
    {
        message = "number不能为负数。";
        return false;
    }
    else if(number > 100)
    {
        message = "number不能大于100。";
        return false;
    }
    return true;
}
//调用:
string msg = string.Empty;
if(CheckNumber(59, ref msg)
{
    //正常逻辑处理代码
}
  • 对于try...finally,除非在执行try块的代码时程序意外退出,不然,finally块老是会被执行

28.多线程的异常处理

  • 在线程上如有未处理的异常,则会触发进程AppDomain.UnHandledException事件,该事件会接收到未处理异常的通知从而调用在它上面注册的方法,而后应用程序退出(注册方法没法阻止应用程序的退出,咱们只能利用该方法来记录日志)
  • 在Windows窗体程序中,能够用Application.ThreadException事件来处理窗体线程中所发生的未被处理的异常,用AppDomain.UnHandledException事件来处理非窗体线程中发生的未被处理的异常。ThreadException事件能够阻止应用程序的退出。
  • 正常状况下,try...catch只能捕获当前线程的异常,一个线程中的异常只能在该线程内部才能被捕获到,也就是说主线程没法直接捕获子线程中的异常,若要把线程中的异常抛给主线程处理,须要用特殊手段,我写了以下示例代码作参考:
static Action<Exception> action;//直接用预约义的Action委托类
    static Exception exception;
    public static void Main()
    {
        action = CatchThreadException; //注册方法
        Thread t1 = new Thread(new ThreadStart(delegate
            {
                try
                {
                    Console.WriteLine("子线程执行!");
                    throw new Exception("子线程t1异常");
                }
                catch(Exception ex)
                {
                    OnCatchThreadException(ex); //执行方法
                    //若是是windows窗体程序,则能够直接用以下方法:
                    //this.BeginInvoke((Action)delegate
                    //{
                    //    throw ex; //将在主线程引起Application.ThreadException
                    //}
                }
            }));
        t1.Start();
        t1.Join();//等待子线程t1执行完毕后,再返回主线程执行
        if(exception!=null)
        {
            Console.WriteLine("主线程:{0}", exception.Message);
        }
        
        Console.ReadKey();
    }
    static void CatchThreadException(Exception ex)
    {
        exception = ex;
    }
    static void OnCatchThreadException(Exception ex) //定义触发方法
    {
        var actionCopy = action;
        if(actionCopy!=null)
        {
            actionCopy(ex); //触发!!!
        }
    }

29.自定义异常

  • 仅在有特殊须要的时候才使用自定义异常
  • 为了应对不一样的业务环境,能够在底层捕获各类业务环境可能引起的异常(如使用不一样的数据库类型等),而后都抛出一个共同的自定义异常给调用者,这样一来,调用者只要捕获该自定义异常类型便可
  • 让自定义异常类派生自System.Exception类或其它常见的基本异常,并让你的异常类应用[Serializable],这样就能够在须要的时候系列化异常(也能够对异常类实现ISerializable接口来自定义系列化过程)
  • 若是要对异常信息进行格式化,则须要重写Message属性
    标准自定义异常类模板:(建立自定义异常标准类的快捷方式:在VS中输入Exception后按Tab键)
[Serializable]
    public class MyException : Exception
    {
        public MyException() { }
        public MyException(string message) : base(message) { }
        public MyException(string message, Exception inner) : base(message, inner) { }
        //用于在反系列化时构造该异常类的实例
        protected MyException(
          SerializationInfo info,
          StreamingContext context)
            : base(info, context) { }  
    }

30.在CLR中方法的执行过程:

  • 首先将参数值依次存进内存栈,执行代码的过程当中,会根据须要去栈中取用参数值
  • 遇到return语句时,方法返回,并把return语句的结果值存入栈顶,这个值就是最终的返回值
  • 若方法内存在finally块,则即便在try块中有return语句,最终也会在执行finlly块以后才退出方法,在这种状况下,若返回值的类型为值类型,则在finally块中对返回变量的修改将无效,方法的最终返回值都是根据return语句压入栈顶中的值(对于引用类型,返回值只是一个引用,能成功修改该引用指向的对象,但对该引用自己的修改也是无效的),以下:
//1.值类型
   public static int SomeMethod(int a)
    {
        try
        {
            a = 10;
            return a;
        }
        finally
        {
            a = 100;
            Console.WriteLine("a={0}", a);
        }
    }
//调用
Console.WriteLine(SomeMethod(1));
//a=100
//10   这是方法的返回值,finally没法修改返回值
//2.引用类型
public class Person:ISerializable
{
    public string FirstName;
}
public static Person SomeMethod(Person a)
    {
        try
        {
            a.FirstName = "Wang";
            return a;
        }
        finally
        {
            a.FirstName = "Su";
            a = null;
            Console.WriteLine("a={0}", a);
        }
    }
//调用
Person person = new Person();
Console.WriteLine(SomeMethod(person).FirstName);  
//a=
//Su  finally成功修改了对象的字段,但对引用a自己的改变不影响返回值对象

31.线程池与线程的区别

  • 线程:经过System.Threading.Thread类开辟的线程,用完就自行销毁,不可重用。主要用于密集型复杂运算
  • 线程池:由System.Threading.ThreadPool类管理的一组线程,可重用。主要用于I/O等异步操做。线程池中的一条线程任务完成后,该线程不会自行销毁。相反,它会以挂起状态返回线程池。若是应用程序再次向线程池发出请求,那么这个挂起的线程将激活并执行任务,而不会建立新线程。这节约了不少开销。只要线程池中应用程序任务的排队速度低于一个线程处理每项任务的速度,那么就能够反复重用同一线程,从而在应用程序生存期内节约大量开销。
  • 线程池能够提供四种功能:异步调用方法、以必定的时间间隔调用方法、当单个内核对象获得信号通知时调用方法、当异步 I/O 请求结束时调用方法

32.多线程和异步的区别

  • 异步操做的本质:是硬件的功能,不消耗CPU资源。硬件在收到CPU的指令后,本身直接和内存交换数据,完成后会触发一个中断来通知操做完成(如:委托的BeginInvoke()方法,执行该方法时,在线程池ThreadPool中启用一条线程来处理任务,完成后会调用方法参数中指定的回掉函数,线程池中的线程是分配好的,使用时不须要new操做)
  • 线程的本质:是操做系统提供的一种逻辑功能,它是进程中一段并发运行的代码,线程须要操做系统投入CPU资源来运行和调度
  • 异步操做的优缺点:由于异步操做无须额外的线程负担,而且使用回调的方式进行处理,在设计良好的状况下,处理函数能够没必要使用共享变量(即便没法彻底不用,最起码能够减小 共享变量的数量),减小了死锁的可能。固然异步操做也并不是完美无暇。编写异步操做的复杂程度较高,程序主要使用回调方式进行处理,与普通人的思惟方式有些 出入,并且难以调试。
  • 多线程的优缺点:多线程的优势很明显,线程中的处理程序依然是顺序执行,符合普通人的思惟习惯,因此编程简单。可是多线程的缺点也一样明显,线程的使用(滥用)会给系统带来上下文切换的额外负担。而且线程间的共享变量可能形成死锁的出现。
  • 什么时候使用:当须要执行I/O操做时,应该使用异步操做。I/O操做不只包括了直接的文件、网络的读写,还包括数据库操做、Web Service、HttpRequest以及.net Remoting等跨进程的调用。而线程的适用范围则是那种须要长时间CPU运算的场合,例如耗时较长的图形处理和算法执行。

32.线程同步

  • 线程同步:就是多个线程在某个对象上执行等待(等待被解锁、等待同步信号),直到该对象被解锁或收到信号。被等待的对象必须是引用类型
  • 锁定:使用关键字lock和类型Monitor(二者本质上是同样的,lock只是Monitor的语法糖),锁定一个对象并建立一段代码的块做用域,同时只容许一个线程进入该代码块,退出代码块时解锁对象,后续线程按顺序进入
  • 信号同步:涉及的类型都继承自抽象类WaitHandle,包括SemaphoreMutexEventWaitHandle(子类AutoResetEventManualResetEvent),他们的原理都同样,都是维护一个系统内核句柄。
  • EventWaitHandle维护一个由内核产生的布尔变量(阻滞状态),false表示阻塞线程,true则解除阻塞。它的子类AutoResetEvent在执行完Set()方法后会自动还原状态(每次只给一个WaitOne()方法发信号),而ManualResetEvent类在执行Set()后不会再改变状态,它的全部WaitOne()方法都能收到信号。只要WaitOne()未收到信号,它就一直阻塞当前线程,以下示例:
public static void Main()
    {
        AutoResetEvent autoReset = new AutoResetEvent(false);
        ManualResetEvent manualReset = new ManualResetEvent(false);
        Thread t1 = new Thread(new ThreadStart(() =>
            {
                autoReset.WaitOne();
                Console.WriteLine("线程t1收到autoReset信号!");
            }));
        
        Thread t2 = new Thread(new ThreadStart(() =>
            {
                autoReset.WaitOne();
                Console.WriteLine("线程t2收到autoReset信号!");
            }));
        t1.Start();
        t2.Start();
        Thread.Sleep(1000);
        autoReset.Set();//t1 t2 只能有一个收到信号
        Thread t3 = new Thread(new ThreadStart(() =>
        {
            manualReset.WaitOne();
            Console.WriteLine("线程t3收到manualReset信号!");
        }));
        Thread t4 = new Thread(new ThreadStart(() =>
        {
            manualReset.WaitOne();
            Console.WriteLine("线程t4收到manualReset信号!");
        }));
        t3.Start();
        t4.Start();
        Thread.Sleep(1000);
        manualReset.Set();//t3 t4都能收到信号
        Console.ReadKey();
    }

34.实现c#每隔一段时间执行代码:

方法一:调用线程执行方法,在方法中实现死循环,每一个循环用Thread.Sleep()设定阻塞时间(或用thread.Join());
方法二:使用System.Timers.Timer类;
方法三:使用System.Threading.Timer
具体怎么实现,就不细说了,看MSDN,或百度并发

还有不少其它方面的....待续

相关文章
相关标签/搜索