讓泛型的思維扎根在腦海——深刻理解泛型

来源:https://www.cnblogs.com/green-jcx/archive/2022/09/09/16671687.html
-Advertisement-
Play Games

1.前言 往往一些剛接觸C#編程的初學者,對於泛型的認識就是直接跳到對泛型集合的使用上,雖然微軟為我們提供了很多內置的泛型類型,但是如果我們只是片面的瞭解調用方式,這會導致我們對泛型盲目的使用。至於為什麼要使用泛型,什麼情況下定義屬於自己的泛型,定義泛型又能為程式帶來哪些好處。要理清這些問題,我們就 ...


1.前言

往往一些剛接觸C#編程的初學者,對於泛型的認識就是直接跳到對泛型集合的使用上,雖然微軟為我們提供了很多內置的泛型類型,但是如果我們只是片面的瞭解調用方式,這會導致我們對泛型盲目的使用。至於為什麼要使用泛型,什麼情況下定義屬於自己的泛型,定義泛型又能為程式帶來哪些好處。要理清這些問題,我們就必須深刻理解泛型的本質,形成泛型編程的思維方式。

接下來我將基於一個基礎示例,然後通過需求不斷的演化示例,從而讓泛型在關鍵時刻脫穎而出,以便讓我們能夠深刻體會泛型的作用。假設.NET沒有為我們提供用於存儲數據的集合,而我們需要一個能夠用於存儲string元素的集合,基於這個情況我們自定義了一個用於存儲字元串的集合類:

    class ArraryStr
    {
        public ArraryStr()
        {
            _items = new string[100]; //初始化存儲元素的容量,只是為了演示故將容量定義為固定值
        }

        private string[] _items; //存儲元素的數組
        private int _count;   //元素總數
        public int Count
        {
            get { return _count; }
        }

        public void Add(string item) //新增元素
        {
            _items[_count] = item;
            _count++;
        }

        public string this[int index] //索引
        {
            get { return _items[index]; }
            set { _items[index] = value;  }
        }

    } // END ArraryStr

為了驗證自定義string集合的可行性,我們對其進行瞭如下的應用:

1             ArraryStr arraryStr = new ArraryStr();
2             arraryStr.Add("張三");
3             Console.WriteLine(arraryStr[0]);

2.重覆

目前對於創建string類型的集合已經大功告成,而此刻我們又接到了一個新的需求,即我們需要一個集合存儲int類型的元素。基於自定義string集合的經驗來看,我們可以發現,string集合類型和我們即將要創建的int集合類型的結構和內容幾乎是一樣的。這就意味著我們可以使用江湖盛行的“複製大法”,將之前的代碼複製一遍,然後輕微修改下即可。下麵是兩個集合類型代碼的對比圖。

在早年有款熱門的游戲叫做“大家來找茬”,該游戲主要玩法就是在兩個大致相同的圖片中,查找兩者之間的細微差異之處。我們使用的“複製大法”,促使我們編寫的代碼形成了可以用於這個游戲游玩的場景。“對於上面的兩個代碼截圖,你能找出圖中不同的地方嗎?”

對於軟體開發者而言,面對的最主要的敵人就是“變化”,假設後面還會出現N個類型的元素需要我們定義集合來存儲,那我們是不是要將相同的代碼無窮盡的複製下去?DRY(Don't Repeat Yourself,不要重覆自己,請記住這是作為一名軟體開發者編碼的原則,“複製大法”很明顯的違背了這個原則。


3.安全和性能

通過“複製,粘貼”的手段可以很明顯的感受到我們在做重覆的事情,在重覆中我們可以發現:集合存儲的類型在增加,但是集合的結構和添加元素的方法都是相同的邏輯。簡單來說就是,不同類型的處理,其處理邏輯都是類似的。基於這個特點,為了滿足自定義集合能夠應對所有類型的存儲,我們必須使用一個通用類型來作為代表,此時此刻我們腦海中就能浮現出一句話:object是一切類型的基類。這就意味著我們添加的所有類型,都可以隱式的轉換為object類型,從而使得自定義集合可以添加任何類型的元素。讓我們來運用這個object類型來試試:

   class ArraryList
    {
        public ArraryList() { _items = new object[100]; }

        private object[] _items;
        private int _count;
        public int Count
        {
            get { return _count; }
        }

        public void Add(object item)
        {
            _items[_count] = item;
            _count++;
        }

        public object this[int index]
        {
            get { return _items[index]; }
            set { _items[index] = value; }
        }
    } // END ArraryStr
    internal class Program
    {
        static void Main(string[] args)
        {
            ArraryList arraryList = new ArraryList();
            arraryList.Add("張三");
            arraryList.Add(18);

            string name = (string)arraryList[0];
            int age = (int)arraryList[1];

        } // END Main()

    }

在上面的代碼中,我們結合了object是一切類型基類的特點,對集合類型進行改造,併成功的使用該方式的集合添加了不同類型的元素。雖然在使用的角度來看已經完美無缺(可以添加任何類型),但是獲取集合元素進行賦值的時候,還使用了類型強制轉換的手段。這是因為這種方式存在很嚴重的問題,主要包括以下兩個方面:

  1. 類型安全方面,如果集合的第一個元素是sting類型,但是你客觀認為是int類型,於是你在獲取時進行了int類型的強制轉換,這個時候代碼不會提示錯誤且可以正常編譯,那麼這就意味著程式在運行時會產生一個你無法預料的類型無效轉換的異常。
  2. 性能方面,值類型元素添加到集合時,必然會存在裝箱操作;而在獲取元素並賦值給一個值類型變數時,又會發生相應的拆箱操作。這種拆箱和裝箱的操作,在操作大量元素時會大幅度的損失程式的性能。

到目前位置,我們還是沒有能創建一個能夠存儲任何類型的集合,但是我們可以對於上述的示例演變的過程進行一個總結:對於不同類型有相同處理邏輯的情況,如果一味的複製會導致我們出現重覆代碼,如果使用object來作為解決重覆的方案,會存在類型安全和性能的問題。至於如何讓徹底解決這些問題,這就要說到了本文講解的主題——泛型。


4.代碼模板

C#中有兩種不同的機制來編寫跨類型(一個類型代替多個類型)可復用的代碼:繼承和泛型。繼承的復用性來自於基類,而泛型的復用性是通過帶有“占位符”的代碼模板類型實現的。繼承實現復用是站在面向對象的角度思考的,而泛型的復用是站在實現特定功能上思考的。相比於繼承,泛型不用遵循里氏替換原則,並且能夠提高類型的安全性,減少類型轉換帶來的拆箱和裝箱。

怎麼樣理解泛型?泛型本質上相當於一種“代碼模板”,可以用一套代碼,為不同類型的同一邏輯使用統一的方式實現。其中“模板”一詞的概念需要進行深刻的體會。例如,公司在招聘時會與用人方簽訂勞動合同,而這個勞動合同的主要內容對於所有人來說幾乎都是一樣的,只是在極個別的地方有所差異,如薪資、姓名等。所以公司不會為某個人(張三或李四)去特意的制定合同,而是會統一制定一份勞動合同作為模板,將其中針對個人存在差異的部分通過“下劃線”進行占位預留,“下劃線”的值將在簽訂合同時由具體的聘用者根據自身情況填寫。

對於這種模板方式的使用,公司在制定合同時則不用考慮簽訂合同的人具體是誰,因為勞動合同(模板)和使用者是分開的,所以公司只用專註於合同的主要內容即可。而我們在實際的編程運用中,使用泛型的目的,其實和公司制定通用的勞動合同模板是一個道理。假設你的公司需要雇佣100名員工時,你不希望為每一個人都制定一個專屬的合同吧?假設你的代碼中,如果遇到10個類型,它們的操作處理邏輯都一樣時,你不希望為這個10個類型寫10個處理方式吧?

通過上面的介紹和例子,接下來我們將泛型運用到我們的示例中來,代碼如下:

 1     class ArraryList<T>
 2     {
 3         public ArraryList() { _items = new T[100]; }
 4 
 5         private T[] _items;
 6         private int _count;
 7         public int Count
 8         {
 9             get { return _count; }
10         }
11 
12         public void Add(T item)
13         {
14             _items[_count] = item;
15             _count++;
16         }
17 
18         public T this[int index]
19         {
20             get { return _items[index]; }
21             set { _items[index] = value; }
22         }
23     } // END ArraryStr
24     internal class Program
25     {
26         static void Main(string[] args)
27         {
28             ArraryList<string> arraryStr = new ArraryList<string>();
29             arraryStr.Add("張三");
30             Console.WriteLine(arraryStr[0]);
31 
32             ArraryList<int> arraryInt = new ArraryList<int>();
33             arraryInt.Add(18);
34             Console.WriteLine(arraryInt[0]);
35 
36         } // END Main()
37 
38     }

5.類型參數

在上面的代碼中,我們將集合類型定義為了泛型類,該類型中出現的T屬於泛型中的類型參數(Type Parameter)。泛型為了達到通用處理的目的,所以不能將某個具體類型作為處理的目標類型,故而將要處理的類型用“T”作為一個類型占位符。

“T”並不是真正的數據類型,它更像是泛型使用的類型藍圖,所以在使用時,泛型類型的消費者必須將一個具體類型作為“類型參數”傳遞到尖括弧內,以此構造一個有明確處理類型的泛型實例。所以我們在外部使用泛型時不能以:“ArraryList<T>list =new ArraryList<T>()”、“T t=new T()”這種方式去實例化泛型類型。另外,“T”本身僅僅是類型參數的名稱,它只是代表了類型參數的標識而已,這意味著我們可以使用其他字元來為類型參數命名。


6.替換

通過類型參數的使用我們可以得知,泛型類型代碼在靜態階段沒有明確的類型,那麼在程式運行的時候,它又是如何和使用時指定的“類型參數”進行對接的呢?為了搞清楚這個問題,下麵我們來瞭解下泛型運行時的本質。

我們編寫的C#程式在編譯後生成的代碼,並不是電腦可以直接執行的代碼,而是會生成CIL(通用中間語言)代碼並包含在程式集中,如果想要生成電腦可執行的代碼,則還需要JIT(即時編譯器)對CIL代碼進行二次編譯。然而泛型類型確認其具體類型的時機,就在JIT進行二次編譯時,JIT編譯的代碼如果包含了泛型的內容,那麼它會根據泛型類型的消費者指定的類型參數,將CIL中泛型代碼中的占位符T替換為一個具體的類型,從而明確當前執行的泛型代碼是針對哪個類型來使用的,其中替換的過程是由CLR在運行時進行主導,JIT來實際操作完成的。這個在運行時確認了類型的泛型又被稱之為“封閉類型”,反之在運行時確認之前的泛型稱為“開放類型”。

泛型使用占位符在運行時替換具體類型的機制,其實和本文中例舉勞動合同模板使用“下劃線”的方式有同樣的思想。在指定勞動合同模板時,對於聘用者的姓名並不能寫一個具體的名字,因為模板的目的是為了通用化,所以對於名字採用了“下劃線”的方式。當公司與某個具體的人簽訂合同的時候,勞動合同模板中的下劃線將由聘用者根據自身情況填寫。回到泛型中其使用思想也是如此,我們使用泛型的目的是為了讓多個類型的處理通用化,所以在定義泛型代碼的時候並不能指定一個具體類型,故使用類型參數T進行代替,這個類型參數T就相當於勞動合同模板中的“下劃線”,當泛型在實際運行的時候,JIT會根據泛型消費者指定的具體類型與占位符T進行替換。


7.總結

本文並不是專門適用於介紹泛型的使用細節的文章,而是通過一個實例根據需求不斷演化的過程,對泛型一步步深入,從而更加深刻的理解泛型的使用初衷,相比瞭解泛型“隻言片語”而言,形成泛型的編程概念和思維顯得尤為重要。在泛型的機制中,我們可以將不同類型存在相同處理邏輯的情況,形成一個通用的方案,從而不在為特定的類型進行編碼,用一套通用的代碼模板會服務於更多的類型,並且在使用上能保證類型安全和提供良好的性能。
  知識改變命運
您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 鋼鐵知識庫,一個學習python爬蟲、數據分析的知識庫。人生苦短,快用python。 之前我們使用requests庫爬取某個站點的時候,每發出一個請求,程式必須等待網站返迴響應才能接著運行,而在整個爬蟲過程中,整個爬蟲程式是一直在等待的,實際上沒有做任何事情。 像這種占用磁碟/記憶體IO、網路IO的任 ...
  • 一個菜鳥的設計模式之旅,使用 Golang 實現。本節實現單例模式。三個工作者需要各自找到電梯搭乘!電梯只有一個! ...
  • 一、問題 在用freemarker生成word文檔的時候,在本地可以成功獲取到類路徑下的資源文件。但是打了jar包放在linux系統下啟動,無法獲取到該文件,導致生成的word文檔是個空文檔。 二、解決 1、文件存放路徑 2、原先代碼 第一種 File docxFile = ResourceUtil ...
  • ApplicationContextAware是一個介面,它提供一個方法setApplicationContext,當spring註冊完成之後,會把ApplicationContext對象以參數的方式傳遞到方法里,在方法里我們可以實現自己的邏輯,去獲取自己的bean,當前對接的斷言等;一般用在被封裝 ...
  • 來源:cnblogs.com/jae-tech/p/15409340.html 寫在前面 此異常非彼異常,標題所說的異常是業務上的異常。 最近做了一個需求,消防的設備巡檢,如果巡檢發現異常,通過手機端提交,後臺的實時監控頁面實時獲取到該設備的信息及位置,然後安排員工去處理。 因為需要服務端主動向客戶 ...
  • 一、為什麼要進行類型別名優化 首先我們來看一下前面寫的UserMapper.xml配置文件: <?xml version="1.0" encoding="UTF-8" ?> <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "h ...
  • 在實際業務中,當後臺數據發生變化,客戶端能夠實時的收到通知,而不是由用戶主動的進行頁面刷新才能查看,這將是一個非常人性化的設計。有沒有那麼一種場景,後臺數據明明已經發生變化了,前臺卻因為沒有及時刷新,而導致頁面顯示的數據與實際存在差異,從而造成錯誤的判斷。那麼如何才能在後臺數據變更時及時通知客戶端呢... ...
  • 一、實驗目的 1.熟悉和掌握小型區域網的配置方法 2.掌握子網劃分中IP地址的分配方法 3.掌握DHCP的配置方法 4.掌握VLAN的配置方法 5.掌握路由的配置方法 6.掌握交換機、路由器的配置方法 二、設備與環境 微型電腦、Windows 系列操作系統 、ensp軟體 三、實驗內容 某公司的小 ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...