一旦我們開始編寫稍微復雜的C#代碼,就肯定離不開泛型、委托和事件。
1.總是優先考慮泛型
泛型的優點是多方面的,無論是泛型類還是泛型方法都同時具備可重用性、類型安全和高效率等特性,這都是非泛型類和非泛型方法無法具備的
2.避免在泛型類型中聲明靜態成員
- 實際上,隨着你為T指定不同的數據類型,MyList<T>相應地也變成了不同的數據類型,在它們之間是不共享靜態成員的。
- 但是若T所指定的數據類型是一致的,那么兩個泛型對象間還是可以共享靜態成員的,如局部的List
和List 的變量。但是,為了規避因此而引起的混淆,仍舊建議在實際的編碼工作中,盡量避免聲明泛型類型的靜態成員。
非泛型類型中的泛型方法並不會在運行時的本地代碼中生成不同的類型。
例如:
static void Main(string[]args)
{
Console.WriteLine(MyList.Func<int>());
Console.WriteLine(MyList.Func<int>());
Console.WriteLine(MyList.Func<string>());
}
class MyList
{
static int count;
public static int Func<T>()
{
return count++;
}}
輸出 0 ;1;2
3.為泛型參數設定約束
在編碼過程中,應該始終考慮為泛型參數設定約束。約束使泛型參數成為一個實實在在的“對象”,讓它具有了我們想要的行為和屬性,而不僅僅是一個object。
指定約束示例:
- 指定參數是值類型。(除Nullable外) where T:struct
- 指定參數是引用類型 。 where T:class
- 指定參數具有無參數的公共構造方法。 where T:new()
注意,CLR目前只支持無參構造方法約束。
- 指定參數必須是指定的基類,或者派生自指定的基類。
- 指定參數必須是指定的接口,或者實現指定的接口。
- 指定T提供的類型參數必須是為U提供的參數,或者派生自為U提供的參數。 where T:U
- 可以對同一類型的參數應用多個約束,並且約束自身可以是泛型類型。
4.使用default為泛型類型變量指定初始值
有些算法,比如泛型集合List<T>的Find算法,所查找的對象有可能會是值類型,也有可能是引用類型。在這種算法內部,我們常常會為這些值類型變量或引用類型變量指定默認值。於是,問題來了:值類型變量的默認初始值是0值,而引用類型變量的默認初始值是null值,顯然,這會導致下面的代碼編譯出錯:
public T Func<T>()
{
T t=null;
T t=0;
return t;
}
代碼"T t=null;"在Visual Studio編譯器中會警示:錯誤1不能將Null轉換為類型形參“T”,因為它可能是不可以為null值的類型。請考慮改用“default(T)”.
代碼"T t=0;"會警示:錯誤1無法將類型“int”隱式轉換為“T”。
改進
public T Func<T>()
{
T t=default(T);
return t;
}
5.使用FCL中的委托聲明
- 要注意FCL中存在三類這樣的委托聲明,它們分別是:Action、Func、Predicate。尤其是在它們的泛型版本出來以后,已經能夠滿足我們在實際編碼過程中的大部分需要。
- 我們應該習慣在代碼中使用這類委托來代替自己的委托聲明。
- 除了Action、Func和Predicate外,FCL中還有用於表示特殊含義的委托聲明。
//如用於表示注冊事件方法的委托聲明:
public delegate void EventHandler(object sender,EventArgs e);
public delegate void EventHandler<TEventArgs>(object sender,TEventArgs e);
//表示線程方法的委托聲明:
public delegate void ThreadStart();
public delegate void ParameterizedThreadStart(object obj);
//表示異步回調的委托聲明:
public delegate void AsyncCallback(IAsyncResult ar);
在FCL中每一類委托聲明都代表一類特殊的用途,雖然可以使用自己的委托聲明來代替,但是這樣做不僅沒有必要,而且會讓代碼失去簡潔性和標准性。在我們實現自己的委托聲明前,應該首先查看MSDN,確信有必要之后才這樣做。
6.使用Lambda表達式代替方法和匿名方法
在實際的編碼工作中熟練運用它,避免寫出煩瑣且不美觀的代碼。
7.小心閉包中的陷阱
如果匿名方法(Lambda表達式)引用了某個局部變量,編譯器就會自動將該引用提升到該閉包對象中,即將for循環中的變量i 修改成了引用閉包對象(編譯器自動創建)的公共變量i。
示例如下:
static void Main(string[]args)
{
List<Action>lists=new List<Action>();
for(int i=0;i<5;i++)
{
Action t=()=>
{
Console.WriteLine(i.ToString());
};
lists.Add(t);
}
foreach(Action t in lists)
{
t();
}
}
以上結果全部輸出5;
另外一種實現方式;
static void Main(string[]args)
{
List<Action>lists=new List<Action>();
TempClass tempClass=new TempClass();
for(tempClass.i=0;tempClass.i<5;tempClass.i++)
{
Action t=tempClass.TempFuc;
lists.Add(t);
}
foreach(Action t in lists)
{
t();
}
}
class TempClass
{
public int i;
public void TempFuc()
{
Console.WriteLine(i.ToString());
}
}
這段代碼所演示的就是閉包對象。所謂閉包對象,指的是上面這種情形中的TempClass對象(在第一段代碼中,也就是編譯器為我們生成的“<>c__DisplayClass2”對象)。如果匿名方法(Lambda表達式)引用了某個局部變量,編譯器就會自動將該引用提升到該閉包對象中,即將for循環中的變量i修改成了引用閉包對象的公共變量i。這樣一來,即使代碼執行后離開了原局部變量i的作用域(如for循環),包含該閉包對象的作用域也還存在。理解了這一點,就能理解代碼的輸出了。
8.了解委托的本質
理解C#中的委托需要把握兩個要點:
- 委托是方法指針。
- 委托是一個類,當對其進行實例化的時候,要將引用方法作為它的構造方法的參數。
9.使用event關鍵字為委托施加保護
首先沒有event加持的委托,我們可以對它隨時進行修改賦值,以至於一個方法改動了另一個方法的委托鏈引用,比如賦值為null,另外一個方法中調用的時候將拋出異常。
如果有event加持的時候,我們修改的時候,比如:
fl.FileUploaded=null;
fl.FileUploaded=Progress;
fl.FileUploaded(10);
以上代碼編譯會出現錯誤警告:
事件 “ConsoleApplication1.FileUploader.FileUploaded ”
只能出現在+=或-=的左邊(從類型“ConsoleApplication1.FileUploader”中使用時除外)
10.實現標准的事件模型
有了上面的event加持,但是還不能夠規范。
EventHandler的原型聲明:
public delegate void EventHandler(object sender,EventArgs e);
微軟為事件模型設定的幾個規范:
- 委托類型的名稱以EventHandler結束;
- 委托原型返回值為void;
- 委托原型具有兩個參數:sender表示事件觸發者,e表示事件參數;
- 事件參數的名稱以EventArgs結束。
11.使用泛型參數兼容泛型接口的不可變性
- 讓返回值類型返回比聲明的類型派生程度更大的類型,就是“協變”。
- 編譯器對於接口和委托類型參數的檢查是非常嚴格的,除非用關鍵字out特別聲明,不然這段代碼只會編譯失敗。比如下例
例如:
class Program{
static void Main(string[]args)
{
ISalary<Programmer>s=new BaseSalaryCounter<Programmer>();
PrintSalary(s);
}
static void PrintSalary(ISalary<Employee>s)
{
s.Pay();
}
}
interface ISalary<T>
{
void Pay();
}
class BaseSalaryCounter<T>:ISalary<T>
{
public void Pay()
{
Console.WriteLine("Pay base salary");
}
}
class Employee
{
public string Name{get;set;}
}
class Programmer:Employee{}
class Manager:Employee{}
報錯: 無法從“ConsoleApplication4.ISalary<ConsoleApplication4.Programmer>”轉換為“ConsoleApplication4.ISalary<ConsoleApplication4.Employee>”
要讓PrintSalary完成需求,我們可以使用泛型類型參數:
static void PrintSalary<T>(ISalary<T>s)
{
s.Pay();
}
實際上,只要泛型類型參數在一個接口聲明中不被用來作為方法的輸入參數,我們都可姑且把它看成是“返回值”類型的。所以,泛型類型參數這種模式是滿足“協變”的定義的。但是,只要將T作為輸入參數,便不滿足“協變”的定義了。如:
interface ISalary<out T>
{
void Pay(T t);
}
編譯會提示:差異無效:類型參數“T”必須是在“ISalary
12.讓接口中的泛型參數支持協變
除了11中提到的使用泛型參數兼容泛型接口的不可變性外,還有一種辦法就是為接口中的泛型聲明加上out關鍵字來支持協變。
out關鍵字是FCL 4.0中新增的功能,它可以在泛型接口和委托中使用,用來讓類型參數支持協變性。通過協變,可以使用比聲明的參數派生類型更大的參數。通過下面例子我們應該能理解這種應用。
比如:
static void Main(string[]args)
{
ISalary<Programmer>s=new BaseSalaryCounter<Programmer>();
ISalary<Manager>t=new BaseSalaryCounter<Manager>();
PrintSalary(s);
PrintSalary(t);
}
static void PrintSalary(ISalary<Employee>s)//用法正確
{
s.Pay();
}
}
interface ISalary<out T> //使用了out關鍵字
{
void Pay();
}
FCL 4.0對多個接口進行了修改以支持協變,如IEnumerable<out T>、IEnumerator<out T>、IQuerable<out T>等。由於IEnumerable<out T>現在支持協變,所以上段代碼在FCL 4.0中能運行得很好。
在我們自己的代碼中,如果要編寫泛型接口,除非確定該接口中的泛型參數不涉及變體,否則都建議加上out關鍵字。協變增大了接口的使用范圍,而且幾乎不會帶來什么副作用。
13.理解委托中的協變
委托中的泛型變量天然是部分支持協變的。
比如:
public delegate T GetEmployeeHanlder<T>(string name);
static void Main(){
GetEmployeeHanlder<Employee>getAEmployee=GetAManager;
Employee e=getAEmployee("Mike");
}
因為存在下面這樣一種情況,所以編譯通不過:
GetEmployeeHanlder<Manager>getAManager=GetAManager;GetEmployeeHanlder<Employee>getAEmployee=getAManager;
static Manager GetAManager(string name)
{
Console.WriteLine("我是經理:"+name);
return new Manager(){Name=name};
}
static Employee GetAEmployee(string name)
{
Console.WriteLine("我是雇員:"+name);
return new Employee(){Name=name};
}
要讓上面的代碼編譯通過,同樣需要為委托中的泛型參數指定out關鍵字:
public delegate T GetEmployeeHanlder<out T>(string name);
FCL 4.0中的一些委托聲明已經用out關鍵字來讓委托支持協變了,如我們常常會使用到的:
public delegate TResult Func<out TResult>()和
public delegate TOutput Converter<in TInput,out TOutput>(TInput input)
14.為泛型類型參數指定逆變
逆變是指方法的參數可以是委托或泛型接口的參數類型的基類。FCL 4.0中支持逆變的常用委托有:
Func<in T,out TResult>
Predicate<in T>
//常用泛型接口有:
IComparer<in T>
舉例:
class Program
{
static void Main()
{
Programmer p=new Programmer{Name="Mike"};
Manager m=new Manager{Name="Steve"};
Test(p,m);
}
static void Test<T>(IMyComparable<T>t1,T t2)
{ //省略 }}
public interface IMyComparable<in T>
{
int Compare(T other);
}
public class Employee:IMyComparable<Employee>
{
public string Name{get;set;}
public int Compare(Employee other)
{
return Name.CompareTo(other.Name);
}
}
public class Programmer:Employee,IMyComparable<Programmer>
{
public int Compare(Programmer other)
{
return Name.CompareTo(other.Name);
}
}
public class Manager:Employee{
}
在上面的這個例子中,如果不為接口IMy-Comparable的泛型參數T指定in關鍵字,將會導致Test(p, m)編譯錯誤。由於引入了接口的逆變性,這讓方法Test支持了更多的應用場景。在FCL4.0之后版本的實際編碼中應該始終注意這一點。
總結
如有需要, 上一篇的《C#規范整理·集合和Linq》也可以看看!
深入理解協變和逆變傳送門《逆變與協變詳解》