深刻理解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,或百度併發

還有不少其它方面的....待續

相關文章
相關標籤/搜索