1.泛型的本質安全
泛型的好處不用多說,在.NET中我看到有不少技術都是以泛型爲基礎的,不過由於不懂泛型而只能對那些技術一臉茫然。泛型主要用於集合類,最主要的緣由是它不須要裝箱拆箱且類型安全,好比很經常使用的List<T>。對於List<T>我之後還想進行深究,如今我寫了一個超簡版的MyList<T>集合,以下面第一段代碼所示。代碼很簡單,但在寫的過程當中有一個細節,若是我爲listInt賦值string類型的變量時編譯器會提示報錯。編譯器很智能,可是從這個現象中你會不會好奇泛型中的T是在什麼狀況下指定的呢,是生成IL時仍是JIT動態編譯時?老方法我將exe放入Reflector工具中,發現IL代碼中全是T,這說明在編譯時T僅僅只是一個佔位符,真真的替換是在運行時動態替換的。但是在寫泛型類時代碼只有一份,那我爲MyList建立int、string類型的對象時這個代碼是如何公用的呢?對於值類型集合好比listInt,因爲最終須要替換T,那麼確定是有一份完整的代碼裏面T被替換爲int。對於引用類型,由於變量只是一個指向堆中的指針,所以代碼只有一份。總結起來就是值類型代碼有多份而引用類型代碼只有一份,另外編寫自定義泛型代碼時最好使用有意義的T,好比.net中常見的TResult表示返回值,這樣可讀性較好。ide
class Program { static void Main(string[] args) { MyList<int> listInt = new MyList<int>(); MyList<string> listString = new MyList<string>(); listInt.Add(24); listInt[1] = 5; listString[2] = "ha ha"; } } public class MyList<T> { T[] array; int current = -1; public MyList() { array = new T[10]; } public void Add(T t) { current++; if (current < 10) array[current] = t; } public T this[int index] { get { return array[index]; } set { array[index] = value; } } }
2.泛型規範函數
這個很重要,主要包括約束和default。.NET是推薦咱們開發者儘量的多使用約束,由於約束越多越能夠保證程序不會出錯。泛型約束由where指定,六種約束以下所示。這些約束能夠單獨使用也能夠一塊兒使用,但也有不能一塊兒使用的好比值類型與引用類型約束。關於default的做用咱們能夠思考這樣一個問題,若是在泛型類中咱們須要初始化一個T變量。由於T既有多是值類型也有多是引用類型,因此不能直接用new或等於0。那如何判斷T是值類型仍是引用類型呢?這裏就要用到default,對於引用類型default(T)將返回null,對於數值類型default(T)將返回0。這裏之因此寫數值類型是由於值類型還多是結構體,default會將結構體中的成員初始化爲0或null。還有一種特殊狀況就是可空值類型,此時將返回Nullable<T>,這樣初始變量直接使用T t=default(T)就能夠了。雖然泛型類給人帶來了神祕感,不過運行時它的本質就是一個普通的類,所以依舊具備類的特性好比繼承。這爲咱們開發者帶來了不少好處,好比我想要有一個int集合類,它除了有List<int>的功能外還有自定義的某些功能,這時候只需MyList : List<int>就能夠獲得想要的效果了,很是方便。工具
where T : struct 值類型約束,T必須爲值類型。學習
where T:class 引用類型約束,T必須爲引用類型。ui
where T:new() 構造器約束,T必須擁有公共無參構造函數且new()約束放在最後。this
where T:U 裸類型約束,T必須是U或派生自U。spa
where T:BaseClass 基類約束,T必須爲BaseClass類或其子類。.net
where T:Interface 接口約束,T必須爲指定的接口或其實現接口。3d
3.反射建立泛型
和非泛型類同樣,利用反射能夠在運行時獲取關於泛型類的成員信息。在學習過程我沒想到居然還可使用反射建立泛型類,更神奇的是還能夠在代碼裏直接寫IL指令,代碼以下所示。流程上仍是那個規則,建立程序集-模塊-類-字段和方法,其中最主要的就是Builder結尾的一系列方法。有一個很差理解的地方就是爲方法添加方法體,正常的邏輯是直接調用ReturnType的有參構造函數建立List<TName1>對象,但是在.NET裏並無這樣的方法,注意這裏ReturnType已是綁定了TName1的List對象而不是普通的List<T>。因此咱們須要拿到List<T>這個類型的有參構造函數,它被封裝在cInfo對象裏,而後再將咱們的ReturnType和cInfo關聯起來獲得List<TName1>的構造函數。除了構造函數中的T須要替換爲TName1外,參數IEnumerable<T>中的T也要被替換爲TName1,體如今代碼裏是這一句ienumOf.MakeGenericType(TFromListOf),最後它將隨構造函數一塊兒與TName1進行關聯。在建立Hello方法我將它設置爲靜態的,本來我是想設置爲實例方法而後調試時去看看是否真的添加了這個方法,不過很奇怪我建立的實例o做爲Invoke的參數老是報錯提示沒法找到方法入口,監視o發現裏面根本沒有Hello方法,在靜態方法下調試也沒有從o裏看到有關方法的信息,若是讀者你對此有本身的想法歡迎留言,若有錯誤還請指出。
public class BaseClass { } public interface IInterfaceA { } public interface IInterfaceB { } //做爲TName1的類型參數 public class ClassT1 { } //做爲TName2的類型參數 public class ClassT2 :BaseClass,IInterfaceA, IInterfaceB { } public class ReflectionT { public void CreateGeneric() { //建立一個名爲」ReflectionT「的動態程序集,這個程序集能夠執行和保存。 AppDomain myDomain = AppDomain.CurrentDomain; AssemblyName assemblyName = new AssemblyName("ReflectionT"); AssemblyBuilder assemblyBuilder = myDomain.DefineDynamicAssembly(assemblyName, AssemblyBuilderAccess.RunAndSave); //在這個程序集中建立一個與程序集名相同的模塊,接着建立一個類MyClass。 ModuleBuilder moudleBuilder = assemblyBuilder.DefineDynamicModule(assemblyName.Name, assemblyName.Name + ".dll"); TypeBuilder myType = moudleBuilder.DefineType("MyClass", TypeAttributes.Public); //建立類型參數名,將達到這樣的效果:public MyClass<TParam1,TParam2> string[] tNames = { "TName1", "TName2" }; GenericTypeParameterBuilder[] gtps = myType.DefineGenericParameters(tNames); GenericTypeParameterBuilder tName1 = gtps[0]; GenericTypeParameterBuilder tName2 = gtps[1]; //爲泛型添加約束,TName1將會被添加構造器約束和引用類型約束 tName1.SetGenericParameterAttributes(GenericParameterAttributes.DefaultConstructorConstraint | GenericParameterAttributes.ReferenceTypeConstraint); //TName2達到的效果將是:where TName2:ValueType,IComparable,IEnumerable Type baseType = typeof(BaseClass); Type interfaceA = typeof(IInterfaceA); Type interfaceB = typeof(IInterfaceA); Type[] interfaceTypes = { interfaceA, interfaceB }; tName2.SetBaseTypeConstraint(baseType); tName2.SetInterfaceConstraints(interfaceTypes); /*爲泛型類MyClass添加字段: private string name; public TName1 tField1; */ FieldBuilder fieldBuilder = myType.DefineField("name", typeof(string), FieldAttributes.Public); FieldBuilder fieldBuilder2 = myType.DefineField("tField1", tName1, FieldAttributes.Public); //爲泛型類添加方法Hello Type listType = typeof(List<>); Type ReturnType = listType.MakeGenericType(tName1); Type[] parameter = { tName1.MakeArrayType() }; MethodBuilder methodBuilder = myType.DefineMethod( "Hello", //方法名 MethodAttributes.Public | MethodAttributes.Static, //指定方法的屬性 ReturnType, //方法的放回類型 parameter); //方法的參數 //爲方法添加方法體 Type ienumOf = typeof(IEnumerable<>); Type TFromListOf = listType.GetGenericArguments()[0]; Type ienumOfT = ienumOf.MakeGenericType(TFromListOf); Type[] ctorArgs = { ienumOfT }; ConstructorInfo cInfo = listType.GetConstructor(ctorArgs); //最終的目的是要調用List<TName1>的構造函數 : new List<TName1>(IEnumerable<TName1>); ConstructorInfo ctor = TypeBuilder.GetConstructor(ReturnType, cInfo); //設置IL指令 ILGenerator msil = methodBuilder.GetILGenerator(); msil.Emit(OpCodes.Ldarg_0); msil.Emit(OpCodes.Newobj, ctor); msil.Emit(OpCodes.Ret); //建立並保存程序集 Type finished = myType.CreateType(); assemblyBuilder.Save(assemblyName.Name + ".dll"); //建立這個MyClass這個類 Type[] typeArgs = { typeof(ClassT1), typeof(ClassT2) }; Type constructed = finished.MakeGenericType(typeArgs); object o = Activator.CreateInstance(constructed); MethodInfo mi = constructed.GetMethod("Hello"); ClassT1[] inputParameter = { new ClassT1(), new ClassT1() }; object[] arguments = { inputParameter }; List<ClassT1> listResult = (List<ClassT1>)mi.Invoke(null, arguments); //查看返回結果中的數量和徹底限定名 Console.WriteLine(listResult.Count); Console.WriteLine(listResult[0].GetType().FullName); //查看類型參數以及約束 foreach (Type t in finished.GetGenericArguments()) { Console.WriteLine(t.ToString()); foreach (Type c in t.GetGenericParameterConstraints()) { Console.WriteLine(" "+c.ToString()); } } } }
4.泛型中的out和in
在VS查看IEnumerable<T>的定義時會看到在T前面有一個out,與其對應的還有一個in。這就是.NET中的協變與逆變,剛開始筆者對於這2個概念很暈,主要如下4個疑惑,我想若是你解決了的話應該也會有更進一步的認識。
1.爲何須要協變和逆變,協變與逆變有什麼效果?
2.爲何有了協變與逆變就能夠類型安全的進行轉換,不加out和in就不能夠轉換?
3.使用協變和逆變須要注意什麼?
4.協變與逆變爲何只能用於接口和委託?
下面第一段代碼解決了第一個問題。對於第二個問題請看第二段代碼,裏面對無out、in的泛型爲何不安全講得很清楚,從中咱們要注意到若是要當進行協變時Function2是徹底ok的,當進行逆變時Function1又是徹底ok的。因此加out只是讓開發者在代碼裏沒法使用in的功能,加in則是讓開發者沒法使用out的功能。讀者能夠本身動手試試,在out T的狀況下做爲輸入參數將會報錯,一樣將in T做爲返回參數也會報錯,且VS報錯時會直接告訴你這樣只能在協變或逆變狀況下使用。也就是說加了out後,只有Function2可以編譯經過,這樣o=str將不會受Function1的影響而不安全;加了in後,只有Function1可以編譯經過,這樣str=o將不會受Function2的影響而不安全。使用out和in要注意它們只能用於接口和委託,且不能做用於值類型。out用於屬性時只能用於只讀屬性,in則是隻寫屬性,進行協變和逆變時這2個類型參數必需要有繼承關係,如今爲何不能用於值類型你應該懂了吧。對於第四個疑惑我沒有找到一個徹底正確的答案,只是發現了我認同的想法。接口和委託,有什麼共同點?顯然就是方法,在接口或委託中聲明的T都將用於方法且只能用於方法,由上面的討論可知協變和逆變這種狀況正是適用於方法這樣的成員。對於在抽象類中不可使用的緣由,或許微軟是以爲在抽象類中再搞一個僅限於方法的限制太麻煩了吧。
public interface INone<T> { } public interface IOut<out T> { } public interface IIn<in T> { } public class MyClass<T> : INone<T>, IOut<T>, IIn<T> { } void hh() { INone<object> oBase1 = new MyClass<object>(); INone<string> o1 = new MyClass<string>(); //下面兩句都沒法編譯經過 //o1 = oBase1; //oBase1 = o1; //爲了可以進行轉換,因而出現了協變與逆變 IOut<object> oBase2 = new MyClass<object>(); IOut<string> o2 = new MyClass<string>(); //o2 = oBase2; 編譯不經過 //有了out關鍵字的話,就能夠實現從IOut<string>到IOut<object>的轉換-往父類轉換 oBase2 = o2; IIn<object> oBase3 = new MyClass<object>(); IIn<string> o3 = new MyClass<string>(); //oBase3 = o3; 編譯不經過 //有了in關鍵字的話,就能夠實現從IIn<object>到IOut<string>的轉換-往子類轉換 o3 = oBase3; }
public interface INone<T> { void Function1(T tParam); T Function2(); } class MyClass<T> : INone<T> { public void Function1(T tParam) { Console.WriteLine(tParam.ToString()); } public T Function2() { T t = default(T); return t; } } class hhh { void fangyz() { INone<object> o = new MyClass<object>(); INone<string> str = new MyClass<string>(); //假設str可以轉換爲o //o = str; object o1=new object(); //這樣的話就是object類型向string類型轉換了,類型不安全 o.Function1(o1); //這樣則是string類型向object類型轉換了,注意這樣是ok的,沒什麼問題 object o2=o.Function2(); //假設str可以轉換爲o //str=o; //string對象將轉變爲object,這樣沒問題 str.Function1("haha"); //這樣將是object向string類型的轉換,類型不安全。 string o3=str.Function2(); } }
聲明:本文原創發表於博客園,做者爲方小白 ,若有錯誤歡迎指出。本文未經做者許可不準轉載,不然視爲侵權。