原創文章,轉載請注明出處! 以下總結參閱了:MSDN文檔、《C#高級編程》、《C#本質論》、前輩們的博客等資料,如有不正確的地方,請幫忙及時指出!以免誤導!
在上一篇 深入理解C#:編程技巧總結(一) 中總結了25點,這一篇繼續:
26.系列化與反系列化
-
使用的場合:
便於保存,把持有運行狀態的對象系列化后保存到本地,在下次運行程序時,反系列化該對象來恢復狀態
便於傳輸,在網絡中傳輸系列化后的對象,接收方反系列化該對象還原
復制黏貼,復制到剪貼板,然后黏貼 -
用來輔助系列化和反系列化的特性:在
System.Runtime.Serialization
命名空間下
OnDeserialized
,應用於某個方法,該方法會在反系列化后立即被自動調用(可用於處理生成的對象的成員)
OnDeserializing
,應用於某個方法,該方法會在執行反系列化時被自動調用
OnSerialized
,應用於某個方法,對象在被系列化后調用該方法
OnSerializing
,應用於某個方法,在系列化對象前調用該方法
如果以上輔助特性仍不能滿足需求,那就要為目標對象實現ISerializable
接口了 -
ISerializable
接口:該接口運行對象自己控制系列化與反系列化的過程(實現該接口的同時也必須應用Serializable
特性)
原理:若系列化一個對象時,發現對象實現了ISerializable
接口,則會忽略掉類型所有的系列化特性應用,轉而調用類型的GetObjectData()
接口方法,該方法會構造一個SerializationInfo
對象,方法內部負責對該對象設置需要系列化的字段,然后系列化器根據該對象來系列化。反系列化時,若發現反系列化后的對象實現了ISerializable
接口,則反系列化器會把數據反系列化為SerializationInfo
類型的對象,然后調用匹配的構造函數來構造目標類型的對象。
系列化:需要實現GetObjectData()
方法,該方法在對象被系列化時自動被調用
void ISerializable.GetObjectData(SerializationInfo info, StreamingContext context){ }
反序列化:需要為它定義一個帶參數的受保護的構造函數,用於反系列化后重新構造對象
protected Person(SerializationInfo info, StreamingContext context) { }
利用該接口,可以實現將數據流反系列化為其他任意指定對象(在原對象定義GetObjectData()
方法,在目標對象定義用於反系列化的構造函數,但兩個對象都必須實現ISerializable
接口)
注意:
若父類為實現ISerializable
接口,只有子類實現ISerializable
接口,若想系列化從父類繼承的字段,則需要在子類的反系列化構造器中和GetObjectData()
方法中,添加繼承自父類的字段的處理代碼
若父類也實現了ISerializable
接口,則只需在子類的反系列化構造器中和GetObjectData()
方法中調用父類的版本即可
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
,包括Semaphore
、Mutex
、EventWaitHandle
(子類AutoResetEvent
、ManualResetEvent
),他們的原理都一樣,都是維護一個系統內核句柄。 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,或百度