主頁 > .NET開發 > 《Effective C#》筆記(4) - Linq

《Effective C#》筆記(4) - Linq

2021-02-03 06:02:30 .NET開發

優先考慮提供迭代器方法,而不要回傳集合

在創建這種回傳一系列物件的方法時,應該考慮將其寫成迭代器方法,使得呼叫者能夠更為靈活地處理這些物件,
迭代器方法是一種采用yield return語法來撰寫的方法,采用按需生成(generate-as-needed)的策略,它會等到呼叫方請求獲取某個元素的時候再去生成序列中的這個元素,
類似下面這個簡單的迭代器方法,用來生成從0到9的int序列:

public static IEnumerable<int> GetIntList()
  {
    var start = 0;
    while (start<10)
    {
      yield return start;
      start++;
    }
  }

對于這樣的寫法,編譯器會用特殊的辦法處理它們,然后在呼叫端使用方法的回傳結果時,只有真正使用這個元素時才會生成,這對于較大的序列來說,優勢是很明顯的,

那么有沒有哪種場合是不適宜用迭代器方法來生成序列的?比方說,如果該序列要反復使用,或是需要快取起來,那么還要不要撰寫迭代器方法了?
整體來說,對于集合的使用,可能有兩種情況:

  1. 只需在真正用到的時候去獲取
  2. 為了讓程式運行得更為高效,呼叫方需要一次獲取全部元素

為了兼顧這兩種場景,.net類別庫的處理方法,為IEnumerable提供了ToList()與ToArray(),這兩個方法就會根據所表示的序列自行獲取其中的元素,并將其保存到集合中,
所以建議任何時候都提供迭代器方法,然后在需要一次性獲取全部元素時,再采用逐步回傳序列元素的迭代器方法,以同時應對兩種情況,

優先考慮通過查詢陳述句來撰寫代碼,而不要使用回圈陳述句

C#剛開始就是一門命令式的語言,在后續的發展程序中,也依然了納入很多命令式語言應有的特性,開發者總是習慣使用手邊最為熟悉的工具(因此特別容易采用回圈結構來完成某些任務),然而熟悉的工具未必就是最好的,撰寫回圈結構時,總是應該想想能不能改用查詢陳述句或查詢方法來實作相同的功能,

查詢陳述句使得開發者能夠以更符合宣告式模型(declarative model)而非命令式模型(imperative model)的寫法來表達程式的邏輯,
與采用回圈陳述句所撰寫的命令式結構相比,查詢陳述句(也包括實作了查詢運算式模式(query expression pattern)的查詢方法)能夠更為清晰地表達開發者的想法,

比如說要把橫、縱坐標均位于0~99之間的所有整數點(X,Y)生成出來,用命令式寫法會用到這樣的雙層回圈:

public static IEnumerable<Tuple<int, int>> ProduceIndices()
{
  for (var i = 0; i < 100; i++)
  {
    for (int j = 0; j < 100; j++)
    {
      yield return Tuple.Create(i, j);
    }
  }
}

宣告式寫法則是這樣的:

public static IEnumerable<Tuple<int, int>> QueryIndices()
{
  return
    from x in Enumerable.Range(0, 100)
    from y in Enumerable.Range(0, 100)
    select Tuple.Create(x, y);
}

表面上看兩者在代碼了、可讀性方面差異不大,但命令式寫法過分關注了執行的細節,而且在需求變復雜后,宣告式寫法仍然可以保持簡潔,假設增加了要求:把這些點按照與原點之間的距離做降序排列,兩種寫法的差異就變得很明顯了:

public static IEnumerable<Tuple<int, int>> ProduceIndices1()
{
  var storage = new List<Tuple<int, int>>();
  for (var i = 0; i < 100; i++)
  {
    for (int j = 0; j < 100; j++)
    {
      storage.Add(Tuple.Create(i, j));
    }
  }
  
  storage.Sort((point1, point2)=>
    (point2.Item1*point2.Item1+point2.Item2*point2.Item2)
    .CompareTo(point1.Item1*point1.Item1+point1.Item2*point1.Item2));

  return storage;
}

public static IEnumerable<Tuple<int, int>> QueryIndices1()
{
  return
    from x in Enumerable.Range(0, 100)
    from y in Enumerable.Range(0, 100)
    orderby (x * x + y * y) descending
    select Tuple.Create(x, y);
}

可見命令式的模型很容易過分強調怎樣去實作操作,而令閱讀代碼的人忽視這些操作本身是打算做什么的,
還有一種觀點是認為通過查詢機制實作出來的代碼是不是要比用回圈寫出來的慢一些,確實存在一些情況會出現這個問題,但這種特例并不代表一般的規律,如果懷疑查詢式的寫法在某種特定情況下運行得不夠快,那么應該首先測量程式的性能,然后再做論斷,即便確實如此,也不要急著把整個演算法都重寫一遍,而是可以考慮利用并行化的(parallel)LINQ機制,因為使用查詢陳述句的另一個好處在于可以通過.AsParallel()方法來并行地執行這些查詢,

把針對序列的API設計得更加易于拼接

有時會對集合做一些變換,甚至會有多種變換,如果用回圈來做,可以分多輪回圈來做,但這樣做記憶體占用較高;或者可以在一輪回圈中完成所有的變換步驟,但這樣做的話又不便于復用,
這時使用基于IEnumerable的宣告式語法往往是更好的選擇,
比如要輸出一個序列中不重復的值,用命令式可以實作為:

public static void Unique(IEnumerable<int> nums)
{
  var unique=new HashSet<int>();
  foreach (var num in nums)
  {
    if (!unique.Contains(num))
    {
      unique.Add(num);
      Console.WriteLine(num);
    }
  }
}

用宣告式的實作則可以是:

public static IEnumerable<int> Unique2(IEnumerable<int> nums)
{
  var unique=new HashSet<int>();
  foreach (var num in nums)
  {
    if (!unique.Contains(num))
    {
      unique.Add(num);
      yield return num;
    }
  }
}

foreach (var num in Unique2(nums))
{
  Console.WriteLine(num);
}

后者看起來更繁瑣,但后者有兩個很大的好處,首先,它推遲了每一個元素的求值時機,更為重要的是,這種延遲執行機制使得開發者能夠把很多個這樣的操作拼接起來,從而可以更為靈活地復用它們,
比方說,如果要輸出的不是源序列中的每一種數值而是這些數值的平方:

public static IEnumerable<int> Square(IEnumerable<int> nums)
{
  foreach (var num in nums)
  {
    yield return num * num;
  }
}

呼叫時改為:

foreach (var num in Square(Unique2(nums)))
{
  Console.WriteLine(num);
}

這樣把復雜的演算法拆解成多個步驟,并把每個步驟都表示成這種小型的迭代器方法,然后借助延遲執行機制,就可以將這些方法拼成一條管道,使得程式只需把源序列處理一遍即可對其中的元素執行許多種小的變換,

掌握盡早執行與延遲執行之間的區別

盡早執行與延遲執行可以對應于命令式的代碼(imperative code)與宣告式的代碼(declarative code),前者重在詳細描述實作該結果所需的步驟,而后者則重在把執行結果定義出來,
命令式的代碼

var answer = DoStuff(Method1()
  ,Method2()
  ,Method3());

宣告式的代碼

var answer = DoStuff(()=>Method1()
  ,()=>Method2()
  ,()=>Method3());

在上面DoStuff的兩種實作中,命令式代碼的執行順序為:Method1->Method2->Method3->DoStuff;
而宣告式代碼只是將三個lambda傳到DoStuff方法,然后方法內部在需要的時候再單獨呼叫各自的方法,甚至有的方法不會被呼叫到,
在函式沒有副作用的前提下,兩種寫法的結果是相同的,但如果函式有副作用,那么兩種寫法的結果可能就不一樣了,
標準函式是否會產生副作用,既要考慮函式本身的代碼,又要考慮其回傳值是否會變化,如果方法還帶有引數,那么引數也是需要考慮的,

在兩種寫法可以得出相同結果的前提下,使用那個更好呢?要回答這個問題要考慮多方面的因素,
其中一個問題是要考慮用作輸入值與輸出值的那些資料所占據的空間,并將該因素與計算輸出值所花費的時間相權衡,在有些情況下更關心空間,在另一些情況寫更關心時間,實際作業中更多的情況或許介于兩極之間,因此答案往往不是唯一的,
然后,還要考慮自己會怎樣使用計算出來的結果,如果方法的結果比較固定,而且使用得較為頻繁,那么及早求出查詢結果是合理的;而如果查詢結果只是會偶爾才會用到,那么更適合采用惰性求值的方式,
最后一條判斷標準是看這個方法要不要放在遠程資料庫上面執行,LINQ to SQL需要將代碼決議運算式樹,采用及早求值還是惰性求值會對LINQ to SQL處理查詢請求的方式產生很大影響,這時應優先考慮惰性求值方式,

注意IEnumerable與IQueryable形式的資料源之間的區別

IEnumerable與IQueryable看起來功能似乎相同,而且IQueryable繼承自IEnumerable,但實際上兩者的行為是有所區別的,而且這種區別可能會極大地影響程式的性能,
比如下面這兩條針對db的查詢陳述句

var q = from c in dbContext.Customer
        where c.City == "London"
        select c;
var finalAnswer = from c in q
        order by c.Name
        select c;
var q = (from c in dbContext.Customer
        where c.City == "London"
        select c).AsEnumerable();
var finalAnswer = from c in q
        order by c.Name
        select c;

第一種寫法采用的是IQueryable所內置的LINQ to SQL機制,而第二種寫法則是把資料庫物件強制轉為IEnumerable形式的序列,并把排序等作業放在本地完成,
LINQ to SQL會把相關的查詢操作以及where子句與orderby子句合起來執行,只需向資料庫發出一次呼叫即可,
第二種寫法則把經過where子句所過濾的結果轉成IEnumerable型的序列,然后并采用LINQ toObjects機制來完成后續的操作,排序操作是在本地而不是在遠端執行的,

可見采用IQueryable更有優勢,但并不是所有的資料源都實作了IQueryable,為此,可以用AsQueryable()把IEnumerable試著轉換成IQueryable
AsQueryable()會判斷序列的運行期型別,如果是IQueryable型,那就把該序列當成IQueryable回傳,若是IEnumerable型,則會用LINQ toObjects的邏輯來創建一個實作IQueryable的wrapper(包裝器),所以使用AsQueryable()來撰寫代碼可以同時顧及這兩種情況,

用Single()及First()來明確地驗證你對查詢結果所做的假設

有許多查詢操作其實就是為了查找某個純量值而寫的,如果你要找的正是這樣的一個值,那么最好能夠設法直接查出該值,而不要回傳一個僅含該值的序列,
這些操作同時還具有對查詢結果所做的假設進行驗證的功能:

  • Single:只會在有且僅有一個元素合乎要求時把該元素回傳給呼叫方,如果沒有這樣的元素,或是有很多個這樣的元素,那么它就拋出例外
  • SingleOrDefault:要么查不到任何元素,要么只能查到一個元素
  • First:從序列中取第一個元素,序列為空則拋出例外
  • FirstOrDefault:序列為空時回傳null

但有時想找的那個元素未必總是序列中的第一個元素,此時可以重新安排元素順序,使得你想找的那個元素恰好出現在序列開頭;或者可以使用Skip跳轉到這個位置,再用First獲取,

參考書籍

《Effective C#:改善C#代碼的50個有效方法(原書第3版)》 比爾·瓦格納

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

標籤:C#

上一篇:C# 給Word不同頁面設定不同背景

下一篇:使用快取防擊穿,解決微信”被動回復用戶訊息”重試回復問題

標籤雲
其他(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