主頁 > .NET開發 > 《Effective C#》筆記(3) - 泛型

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

2021-02-02 05:59:38 .NET開發

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

泛型約束可以規定一個泛型類必須采用什么樣的型別引數才能夠正常地運作,設定約束條件的時候,太寬或太嚴都不合適,
如果根本就不加約束,那么程式必須在運行的時候做很多檢查,并執行更多的強制型別轉換操作,而且在編譯器為這個泛型型別的定義生成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視為其中的型別引數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,然后才能呼叫合適的方法,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版)》 比爾·瓦格納

轉載請註明出處,本文鏈接:https://www.uj5u.com/net/255409.html

標籤:C#

上一篇:不實現串流f議,實時播放影像的選擇?USB CAM

下一篇:C# 使用 sid 連接 Oracle(無需安裝 Oracle 客戶端)

標籤雲
其他(157675) Python(38076) JavaScript(25376) Java(17977) C(15215) 區塊鏈(8255) C#(7972) AI(7469) 爪哇(7425) MySQL(7132) html(6777) 基礎類(6313) sql(6102) 熊猫(6058) PHP(5869) 数组(5741) R(5409) Linux(5327) 反应(5209) 腳本語言(PerlPython)(5129) 非技術區(4971) Android(4554) 数据框(4311) css(4259) 节点.js(4032) C語言(3288) json(3245) 列表(3129) 扑(3119) C++語言(3117) 安卓(2998) 打字稿(2995) VBA(2789) Java相關(2746) 疑難問題(2699) 细绳(2522) 單片機工控(2479) iOS(2429) ASP.NET(2402) MongoDB(2323) 麻木的(2285) 正则表达式(2254) 字典(2211) 循环(2198) 迅速(2185) 擅长(2169) 镖(2155) 功能(1967) .NET技术(1958) Web開發(1951) python-3.x(1918) HtmlCss(1915) 弹簧靴(1913) C++(1909) xml(1889) PostgreSQL(1872) .NETCore(1853) 谷歌表格(1846) Unity3D(1843) for循环(1842)

熱門瀏覽
  • WebAPI簡介

    Web體系結構: 有三個核心:資源(resource),URL(統一資源識別符號)和表示 他們的關系是這樣的:一個資源由一個URL進行標識,HTTP客戶端使用URL定位資源,表示是從資源回傳資料,媒體型別是資源回傳的資料格式。 接下來我們說下HTTP. HTTP協議的系統是一種無狀態的方式,使用請求/ ......

    uj5u.com 2020-09-09 22:07:47 more
  • asp.net core 3.1 入口:Program.cs中的Main函式

    本文分析Program.cs 中Main()函式中代碼的運行順序分析asp.net core程式的啟動,重點不是剖析原始碼,而是理清程式開始時執行的順序。到呼叫了哪些實體,哪些法方。asp.net core 3.1 的程式入口在專案Program.cs檔案里,如下。ususing System; us ......

    uj5u.com 2020-09-09 22:07:49 more
  • asp.net網站作為websocket服務端的應用該如何寫

    最近被websocket的一個問題困擾了很久,有一個需求是在web網站中搭建websocket服務。客戶端通過網頁與服務器建立連接,然后服務器根據ip給客戶端網頁發送資訊。 其實,這個需求并不難,只是剛開始對websocket的內容不太了解。上網搜索了一下,有通過asp.net core 實作的、有 ......

    uj5u.com 2020-09-09 22:08:02 more
  • ASP.NET 開源匯入匯出庫Magicodes.IE Docker中使用

    Magicodes.IE在Docker中使用 更新歷史 2019.02.13 【Nuget】版本更新到2.0.2 【匯入】修復單列匯入的Bug,單元測驗“OneColumnImporter_Test”。問題見(https://github.com/dotnetcore/Magicodes.IE/is ......

    uj5u.com 2020-09-09 22:08:05 more
  • 在webform中使用ajax

    如果你用過Asp.net webform, 說明你也算是.NET 開發的老兵了。WEBform應該是2011 2013左右,當時還用visual studio 2005、 visual studio 2008。后來基本都用的是MVC。 如果是新開發的專案,估計沒人會用webform技術。但是有些舊版 ......

    uj5u.com 2020-09-09 22:08:50 more
  • iis添加asp.net網站,訪問提示:由于擴展配置問題而無法提供您請求的

    今天在iis服務器配置asp.net網站,遇到一個問題,記錄一下: 問題:由于擴展配置問題而無法提供您請求的頁面。如果該頁面是腳本,請添加處理程式。如果應下載檔案,請添加 MIME 映射。 WindowServer2012服務器,添加角色安裝完.netframework和iis之后,運行aspx頁面 ......

    uj5u.com 2020-09-09 22:10:00 more
  • WebAPI-處理架構

    帶著問題去思考,大家好! 問題1:HTTP請求和回傳相應的HTTP回應資訊之間發生了什么? 1:首先是最底層,托管層,位于WebAPI和底層HTTP堆疊之間 2:其次是 訊息處理程式管道層,這里比如日志和快取。OWIN的參考是將訊息處理程式管道的一些功能下移到堆疊下端的OWIN中間件了。 3:控制器處理 ......

    uj5u.com 2020-09-09 22:11:13 more
  • 微信門戶開發框架-使用指導說明書

    微信門戶應用管理系統,采用基于 MVC + Bootstrap + Ajax + Enterprise Library的技術路線,界面層采用Boostrap + Metronic組合的前端框架,資料訪問層支持Oracle、SQLServer、MySQL、PostgreSQL等資料庫。框架以MVC5,... ......

    uj5u.com 2020-09-09 22:15:18 more
  • WebAPI-HTTP編程模型

    帶著問題去思考,大家好!它是什么?它包含什么?它能干什么? 訊息 HTTP編程模型的核心就是訊息抽象,表示為:HttPRequestMessage,HttpResponseMessage.用于客戶端和服務端之間交換請求和回應訊息。 HttpMethod類包含了一組靜態屬性: private stat ......

    uj5u.com 2020-09-09 22:15:23 more
  • 部署WebApi隨筆

    一、跨域 NuGet參考Microsoft.AspNet.WebApi.Cors WebApiConfig.cs中配置: // Web API 配置和服務 config.EnableCors(new EnableCorsAttribute("*", "*", "*")); 二、清除默認回傳XML格式 ......

    uj5u.com 2020-09-09 22:15:48 more
最新发布
  • C#多執行緒學習(二) 如何操縱一個執行緒

    <a href="https://www.cnblogs.com/x-zhi/" target="_blank"><img width="48" height="48" class="pfs" src="https://pic.cnblogs.com/face/2943582/20220801082530.png" alt="" /></...

    uj5u.com 2023-04-19 09:17:20 more
  • C#多執行緒學習(二) 如何操縱一個執行緒

    C#多執行緒學習(二) 如何操縱一個執行緒 執行緒學習第一篇:C#多執行緒學習(一) 多執行緒的相關概念 下面我們就動手來創建一個執行緒,使用Thread類創建執行緒時,只需提供執行緒入口即可。(執行緒入口使程式知道該讓這個執行緒干什么事) 在C#中,執行緒入口是通過ThreadStart代理(delegate)來提供的 ......

    uj5u.com 2023-04-19 09:16:49 more
  • 記一次 .NET某醫療器械清洗系統 卡死分析

    <a href="https://www.cnblogs.com/huangxincheng/" target="_blank"><img width="48" height="48" class="pfs" src="https://pic.cnblogs.com/face/214741/20200614104537.png" alt="" /&g...

    uj5u.com 2023-04-18 08:39:04 more
  • 記一次 .NET某醫療器械清洗系統 卡死分析

    一:背景 1. 講故事 前段時間協助訓練營里的一位朋友分析了一個程式卡死的問題,回過頭來看這個案例比較經典,這篇稍微整理一下供后來者少踩坑吧。 二:WinDbg 分析 1. 為什么會卡死 因為是表單程式,理所當然就是看主執行緒此時正在做什么? 可以用 ~0s ; k 看一下便知。 0:000> k # ......

    uj5u.com 2023-04-18 08:33:10 more
  • SignalR, No Connection with that ID,IIS

    <a href="https://www.cnblogs.com/smartstar/" target="_blank"><img width="48" height="48" class="pfs" src="https://pic.cnblogs.com/face/u36196.jpg" alt="" /></a>...

    uj5u.com 2023-03-30 17:21:52 more
  • 一次對pool的誤用導致的.net頻繁gc的診斷分析

    <a href="https://www.cnblogs.com/dotnet-diagnostic/" target="_blank"><img width="48" height="48" class="pfs" src="https://pic.cnblogs.com/face/3115652/20230225090434.png" alt=""...

    uj5u.com 2023-03-28 10:15:33 more
  • 一次對pool的誤用導致的.net頻繁gc的診斷分析

    <a href="https://www.cnblogs.com/dotnet-diagnostic/" target="_blank"><img width="48" height="48" class="pfs" src="https://pic.cnblogs.com/face/3115652/20230225090434.png" alt=""...

    uj5u.com 2023-03-28 10:13:31 more
  • C#遍歷指定檔案夾中所有檔案的3種方法

    <a href="https://www.cnblogs.com/xbhp/" target="_blank"><img width="48" height="48" class="pfs" src="https://pic.cnblogs.com/face/957602/20230310105611.png" alt="" /></a&...

    uj5u.com 2023-03-27 14:46:55 more
  • C#/VB.NET:如何將PDF轉為PDF/A

    <a href="https://www.cnblogs.com/Carina-baby/" target="_blank"><img width="48" height="48" class="pfs" src="https://pic.cnblogs.com/face/2859233/20220427162558.png" alt="" />...

    uj5u.com 2023-03-27 14:46:35 more
  • 武裝你的WEBAPI-OData聚合查詢

    <a href="https://www.cnblogs.com/podolski/" target="_blank"><img width="48" height="48" class="pfs" src="https://pic.cnblogs.com/face/616093/20140323000327.png" alt="" /><...

    uj5u.com 2023-03-27 14:46:16 more