《Effective C#》筆記(3) - 泛型

只定義恰好夠用的約束條件

泛型約束能夠規定一個泛型類必須採用什麼樣的類型參數纔可以正常地運做。設定約束條件的時候,太寬或太嚴都不合適。 若是根本就不加約束,那麼程序必須在運行的時候作不少檢查,並執行更多的強制類型轉換操做。並且在編譯器爲這個泛型類型的定義生成IL碼的時候,經過約束還能夠爲提供更多的提示,若是你不給出任何提示,那麼編譯器就只好假設這些類型參數所表示的都是最爲基本的System.Object,也就是假設未來的實際類型只支持由System.Object所公佈的那些方法,這使得凡是沒有定義在System.Object裏面的用法全都會令編譯器報錯,甚至連最爲基本的new T()等操做也不支持。算法

但添加約束的時候也不要過度嚴格,以致於限制了泛型類的使用範圍,只添加確實有必要的約束便可。ide

建立泛型類時,應該給實現了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已經被釋放了。this

泛型類將根據類型參數所建立的那個實例看成成員變量 在這種狀況下,那麼代碼會複雜一些。該類擁有的這個引用所指向的對象類型可能實現了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的全部權轉移到該類以外,因而也就不用關心資源的釋放了。|code

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);

這樣寫還有個好處是,未來若是又添加了一些針對其餘類型的具體版本,那麼編譯器在處理那些類型的參數時就不會去調用泛型版本,而是會直接調用與之相應的具體版本。

但也要注意的是,並不是每一種泛型算法都可以繞開泛型類而單純以泛型方法的形式得以實現。 有兩種狀況,必須把類寫成泛型類:

  1. 該類須要將某個值用做其內部狀態,而該值的類型必須以泛型來表達(例如集合類)
  2. 該類須要實現泛型版的接口。

除此以外的其餘狀況一般均可以考慮用包含泛型方法的非泛型來實現。

只把必備的契約定義在接口中,把其餘功能留給擴展方法去實現

若是程序中有不少個類都必須實現所要設計的某個接口,那麼定義接口的時候就應該定義儘可能少的方法,後續能夠採用擴展方法的形式編寫一些針對該接口的便捷方法。這樣作不只可使實現接口的人少寫一些代碼,並且能夠令使用接口的人可以充分利用那些擴展方法。

但使用擴展方法時須要注意一點:若是已經針對某個接口定義了擴展方法,而其餘一些類又想要以它們本身的方式來實現這個同名方法,那麼擴展方法就會被覆蓋,相似下面這樣,針對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版)》 比爾·瓦格納

相關文章
相關標籤/搜索