您好,登錄后才能下訂單哦!
這篇文章主要講解了“Effective C#之如何使用泛型”,文中的講解內容簡單清晰,易于學習與理解,下面請大家跟著小編的思路慢慢深入,一起來研究和學習“Effective C#之如何使用泛型”吧!
泛型約束可以規定一個泛型類必須采用什么樣的類型參數才能夠正常地運作。設定約束條件的時候,太寬或太嚴都不合適。 如果根本就不加約束,那么程序必須在運行的時候做很多檢查,并執行更多的強制類型轉換操作。而且在編譯器為這個泛型類型的定義生成IL碼的時候,通過約束還可以為提供更多的提示,如果你不給出任何提示,那么編譯器就只好假設這些類型參數所表示的都是最為基本的System.Object,也就是假設將來的實際類型只支持由System.Object所公布的那些方法,這使得凡是沒有定義在System.Object里面的用法全都會令編譯器報錯,甚至連最為基本的new T()等操作也不支持。
但添加約束的時候也不要過分嚴格,以至于限制了泛型類的使用范圍,只添加確實有必要的約束即可。
如果在泛型類里面根據類型參數創建了實例,那么就應該判斷該實例所屬的類型是否實現了IDisposable接口。如果實現了,就必須編寫相關的代碼,以防程序在離開泛型類之后發生資源泄漏。這還要分不同的情況: 泛型類的方法根據類型參數所表示的類型來創建實例并使用該實例 類似下面的寫法,如果T是非托管資源,那么就會造成內存泄露:
public interface IEngine { void DoWork(); } public class EngineDriver<T> where T : IEngine, new() { public void GetThingsDone() { var driver =new T(); driver.DoWork(); } }
正確的寫法應該是:
var driver =new T(); using (driver as IDisposable) { driver.DoWork(); }
編譯器會把driver視為IDisposable,并創建隱藏的局部變量,用以保存指向這個IDisposable的引用。在T沒有實現IDisposable的情況下,這個局部變量的值是null,此時編譯器不調用Dispose(),因為它在調用之前會先做檢查。反之,如果T實現了IDisposable,那么編譯器會生成相應的代碼,以便在程序退出using塊的時候調用Dispose()方法。 這段代碼等同于:
var a = driver as IDisposable; driver.DoWork(); a?.Dispose();
使用using后,需要注意的是所有調用driver實例的操作都不可以放在using區域之后,因為那時driver已經被釋放了。
泛型類將根據類型參數所創建的那個實例當作成員變量 在這種情況下,那么代碼會復雜一些。該類擁有的這個引用所指向的對象類型可能實現了IDisposable接口,也可能沒有實現,但為了應對可能實現了IDisposable接口的情況,泛型類本身就必須實現IDisposable,并且要判斷相關的資源是否實現了這個接口,如果實現了,就要調用該資源的Dispose()方法。
public class EngineDriver2<T> : IDisposable where T : IEngine, new() { // it's expensive to create, so create to null private Lazy<T> driver = new Lazy<T>(() => new T()); public void GetThingsDone() => driver.Value.DoWork(); public void Dispose() { if (driver.IsValueCreated) { var resource = driver.Value as IDisposable; resource?.Dispose(); } } }
或者可以將driver的所有權轉移到該類之外,于是也就不用關心資源的釋放了。|
public sealed class EngineDriver3<T> where T : IEngine { private T driver; public EngineDriver3(T driver) { this.driver = driver; } }
如果有多個相互重載的方法,那么編譯器就需要判斷哪一個方法應該得到調用。而在引入泛型方法之后,這套判斷規則會變得更加復雜,因為只要能夠替換其中的類型參數,就可以與這個泛型方法相匹配。 比如有下面三個類型,它們之間的關系如代碼所示:
public class MyBase { } public interface IMsgWriter { void WriteMsg(); } public class MyDerived : MyBase, IMsgWriter { void IMsgWriter.WriteMsg() => Console.WriteLine("Inside MyDerived.WriteMsg"); }
接下來定義三個重載方法,其中包括了泛型方法:
static void WriteMsg(MyBase b) { Console.WriteLine("Inside WriteMsg(MyBase b)"); } static void WriteMsg<T>(T obj) { Console.WriteLine("Inside WriteMsg<T>(T obj)"); } static void WriteMsg(IMsgWriter obj) { Console.Write("Inside WriteMsg(IMsgWriter obj)"); }
那么如下三種調用寫法,結果是怎樣的呢?
MyDerived derived = new MyDerived(); WriteMsg(derived); var msgWriter = derived as IMsgWriter; WriteMsg(msgWriter); var mbase = derived as MyBase; WriteMsg(mbase);
下面為運行結果,與你預想是否一致呢?
Inside WriteMsg<T>(T obj) Inside WriteMsg(IMsgWriter obj) Inside WriteMsg(MyBase b)
第一條結果表明了一個極為重要的現象:如果對象所屬的類繼承自基類MyBase,那么以該對象為參數來調用WriteMsg時,WriteMsg<T>總是會先于WriteMsg(MyBase b)而得到匹配,這是因為如果要與泛型版的方法相匹配,那么編譯器可以直接把子類MyDerived視為其中的類型參數T,但若要與基類版的方法相匹配,則必須將MyDerived型的對象隱式地轉換成MyBase型的對象,所以,它認為泛型版的WriteMsg更好。 如果要調用到WriteMsg(MyBase b), 需要將MyDerived型的對象顯式地轉換成MyBase型對象,就像第三條測試那樣。
一般來說,我們通常的習慣是定義泛型類,但有時更推薦用泛型方法。因為使用泛型方法時所提供的泛型參數只需與該方法的要求相符即可,而使用泛型類時所提供的泛型參數則必須滿足該類所定義的每一條約束。如果將來還要給類里面添加代碼,那么可能會對類級別的泛型參數施加更多的約束,從而令該類的適用場景變得越來越窄。
此外,泛型方法相比泛型類會更加靈活,比如下面的泛型工具類獲取提供了獲取較大值的方法:
public class Utils<T> { public static T Max(T left, T right) { return Comparer<T>.Default.Compare(left, right) > 0 ? left : right; } }
因為是泛型,那么每次調用都要提供類型:
Utils<string>.Max("c", "d"); Utils<int>.Max(4, 3);
這樣雖然類本身的實現比較方便,但調用端使用起來卻比較麻煩,更重要的是,值類型可以直接使用Math.Max,而不需要每次都讓程序在運行的時候先去判斷相關類型是否實現了IComparer<T>,然后才能調用合適的方法,Math.Max可以提供更好的性能,所以可以改進為對于值類型提供不同版本的Max方法:
public class Utils1 { public static T Max<T>(T left, T right) { return Comparer<T>.Default.Compare(left, right) > 0 ? left : right; } public static int Max(int left, int right) { return Math.Max(left, right) > 0 ? left : right; } public static double Max(double left, double right) { return Math.Max(left, right) > 0 ? left : right; } }
經過這樣的修改,將泛型類改成了部分使用泛型方法,對于int、double,編譯器會直接調用非泛型的版本,其它的類型會匹配到泛型版本。
Utils1.Max("c", "d"); Utils1.Max(4, 3);
這樣寫還有個好處是,將來如果又添加了一些針對其他類型的具體版本,那么編譯器在處理那些類型的參數時就不會去調用泛型版本,而是會直接調用與之相應的具體版本。
但也要注意的是,并非每一種泛型算法都能夠繞開泛型類而單純以泛型方法的形式得以實現。 有兩種情況,必須把類寫成泛型類:
該類需要將某個值用作其內部狀態,而該值的類型必須以泛型來表達(例如集合類)
該類需要實現泛型版的接口。
除此之外的其他情況通常都可以考慮用包含泛型方法的非泛型來實現。
如果程序中有很多個類都必須實現所要設計的某個接口,那么定義接口的時候就應該定義盡量少的方法,后續可以采用擴展方法的形式編寫一些針對該接口的便捷方法。這樣做不僅可以使實現接口的人少寫一些代碼,而且可以令使用接口的人能夠充分利用那些擴展方法。
但使用擴展方法時需要注意一點:如果已經針對某個接口定義了擴展方法,而其他一些類又想要以它們自己的方式來實現這個同名方法,那么擴展方法就會被覆蓋,類似下面這樣,針對IFoo定義了擴展方法NextMarker,同時也在MyType中實現了NextMarker。
public interface IFoo { int Marker { get; set; } } public static class FooExtension { public static void NextMarker(this IFoo foo) { foo.Marker++; } } public class MyType: IFoo { public int Marker { get; set; } public void NextMarker() { this.Marker += 5; } }
那么下面代碼的結果就是5,而不是1
var myType =new MyType(); myType.NextMarker(); Console.WriteLine(myType.Marker); // 5
而如果需要調用擴展方法,需要顯示地將myType轉換為IFoo。
var myType =new MyType(); var a = myType as IFoo; a.NextMarker();
感謝各位的閱讀,以上就是“Effective C#之如何使用泛型”的內容了,經過本文的學習后,相信大家對Effective C#之如何使用泛型這一問題有了更深刻的體會,具體使用情況還需要大家實踐驗證。這里是億速云,小編將為大家推送更多相關知識點的文章,歡迎關注!
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。