只定義剛好夠用的約束條件
泛型約束可以規定一個泛型類必須采用什么樣的型別引數才能夠正常地運作,設定約束條件的時候,太寬或太嚴都不合適,
如果根本就不加約束,那么程式必須在運行的時候做很多檢查,并執行更多的強制型別轉換操作,而且在編譯器為這個泛型型別的定義生成IL碼的時候,通過約束還可以為提供更多的提示,如果你不給出任何提示,那么編譯器就只好假設這些型別引數所表示的都是最為基本的System.Object,也就是假設將來的實際型別只支持由System.Object所公布的那些方法,這使得凡是沒有定義在System.Object里面的用法全都會令編譯器報錯,甚至連最為基本的new T()等操作也不支持,
但添加約束的時候也不要過分嚴格,以至于限制了泛型類的使用范圍,只添加確實有必要的約束即可,
創建泛型類時,應該給實作了IDisposable的型別引數提供支持
如果在泛型類里面根據型別引數創建了實體,那么就應該判斷該實體所屬的型別是否實作了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
如果要呼叫到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
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#:改善C#代碼的50個有效方法(原書第3版)》 比爾·瓦格納
轉載請註明出處,本文鏈接:https://www.uj5u.com/net/255416.html
標籤:.NET技术
