C#中的函數式編程:序言(一)

来源:https://www.cnblogs.com/dhlee/archive/2018/03/13/functional-csharp-1.html
-Advertisement-
Play Games

學了那麼久的函數式編程語言,一直想寫一些相關的文章。經過一段時間的考慮,我決定開這個坑。 至於為什麼選擇C#,在我看來,編程語言分三類:一類是難以進行函數式編程的語言,這類語言包括Java6、C語言等。這類語言由於不支持匿名函數等特性,進行函數式編程會比較困難;一類是自稱“函數式編程語言”的語言,包 ...


學了那麼久的函數式編程語言,一直想寫一些相關的文章。經過一段時間的考慮,我決定開這個坑。

至於為什麼選擇C#,在我看來,編程語言分三類:一類是難以進行函數式編程的語言,這類語言包括Java6、C語言等。這類語言由於不支持匿名函數等特性,進行函數式編程會比較困難;一類是自稱“函數式編程語言”的語言,包括Scala、Clojure、F#、Haskell等。這類語言比較重視函數式編程,它的教學資料通常會包含函數式編程知識,因此這些語言的使用者大多也都已經掌握了函數式編程技巧;還有一類編程語言,它們不被稱作函數式編程語言,卻可以進行函數式編程。這些語言的使用者中懂得函數式編程的人相對較少,學習資料也較少提及函數式編程。這些語言包括Java8、C++11、C#、Rust、Kotlin、TypeScript、Python、Ruby等。

既然我的文章是要介紹函數式編程,首先我肯定不能選第一類,它們無法使用;而第二類編程語言的使用者已經掌握了函數式編程的技能。考慮到受眾面,我的選擇範圍定在第三類語言內。最終我通過隨機數選中了C#,如果我有精力我也會嘗試一下其他語言。

說了這麼多,那究竟什麼是函數式編程呢?根據Scala之父Martin Odersky的說法,函數式編程有狹義和廣義之分:狹義的函數式編程指的是表達式沒有副作用的編程,滿足這一特性的編程語言有Pure Lisp和不包含IO Monad與Unsafe operations的Haskell子集;而廣義的函數式編程指的是函數是第一公民的語言,這個範圍就大了很多,前面提到的第二類與第三類語言都屬於廣義的函數式編程。

而函數式編程的核心,就和這兩個定義相關:沒有副作用、函數是第一公民。

我們先來看副作用。我記得以前學C語言時有人喜歡用x++ + ++x為例去黑某個人寫的臭名昭著的C語言的書。這個表達式實際上是一種未定義行為。但是,如果我們把它換成(x + 1) + (x + 2),這個語句就毫無歧義。問題在於x++、++x是有副作用的。如果一個表達式是無副作用的,我們就可以用這個表達式的值替換成它,而程式的行為不會發生改變。我們稱這個性質為引用透明(Referential transparency)。就剛纔的例子,假設x的值是3,那麼對於(x + 1) + (x + 2)而言,我們可以把x + 1替換成它的值4,則表達式改寫成4 + (x + 2),或者把x + 2替換成5而改寫成(x + 1) + 5,這樣的改寫不會改變表達式的值。但是x++ + ++x就不可以,如果我們把x++換成3,那麼表達式的值就會變。所以x++和++x不是引用透明的。

引用透明的一大特性是,我們可以改變引用透明的表達式的執行次序,而不用擔心程式行為的變化。之所以x++ + ++x是未定義行為,是因為x++和++x不是引用透明的,從而導致x++和++x執行的先後順序會影響整個表達式的值。而x + 1和x + 2的先後順序則對錶達式的值沒有影響。這個特性在後面我們會用到。

下麵再給一個例子,考慮這段C#代碼

 1 class Program
 2 {
 3     static void Main()
 4     {
 5         for (int i = 0; i < 10; ++i)
 6         {
 7             System.Threading.Tasks.Task.Factory.StartNew(() =>
 8             {
 9                 System.Threading.Thread.Sleep(100);
10                 System.Console.WriteLine(i);
11             });
12         }
13         System.Console.ReadLine();
14     }
15 }

這段代碼會輸出什麼?

你可能會以為它會以某種次序輸出數字0到9,但實際輸出是10個數字10.

為了能讓程式輸出數字0到9,我們需要這樣修改程式:

 1 class Program
 2 {
 3     static void Main()
 4     {
 5         for (int i = 0; i < 10; ++i)
 6         {
 7             int _i = i;
 8             System.Threading.Tasks.Task.Factory.StartNew(() =>
 9             {
10                 System.Threading.Thread.Sleep(100);
11                 System.Console.WriteLine(_i);
12             });
13         }
14         System.Console.ReadLine();
15     }
16 }

如果你是JavaScript程式員,你可能會對這個策略有所熟悉。這是在迴圈中創建閉包(即使用了外部變數的匿名函數)時常遇到的坑。對於前一個程式,由於迴圈變數的i是變化的,因此i不滿足引用透明,我們不能在創建閉包時就用i的值替換掉i,而由於Sleep語句存在,最終輸出的時候i的值是10。而第二個程式輸出的不是i,而是_i,_i滿足一經初始化後不再被重新賦值,這是一個變數滿足引用透明的重要特征。此時我們就可以用_i的值替換掉_i,從而程式能輸出數字0~9.

從上面的例子可以看出,使用副作用可能會產生不經意的bug。因此,在函數式編程中,我們會儘量的少產生副作用。比如上面這段代碼,最完美的方案是用我們後面會提到的尾遞歸。

函數式編程的另一個特點是函數是第一公民。在很多傳統的編程語言中,函數有很多限制,比如我們不能在函數內部定義函數,我們不能創建一個函數類型的變數(註意:C語言的函數指針嚴格來講不算。因為函數指針無法指向帶閉包的函數)、我們不能將函數當成參數傳給一個函數、不能創建一個沒有名字的函數字面量等等。“函數是第一公民”的意思是,函數不應該受這些“歧視”。函數應該和其他類型擁有同等地位。當然,嚴格的滿足函數是第一公民的語言也並不多。C#也是到了7才支持在函數內部創建函數。但對於函數式編程而言,函數至少要有的“權力”包括:創建沒有名字的函數字面量(即匿名函數或Lambda表達式)、將函數作為參數傳給其他參數。

我相信大家都用過Linq吧。Linq就是一個典型的把函數當第一公民的例子。在函數式編程中,我們將深挖函數作為第一公民的價值。


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • Cpp1是什麼? "Cpp1 github項目" 作業要求描述 請編一個小軟體,實現四則運算(《構建之法》中開篇的題目),並具有以下功能: (1)能根據題目回答情況,自動判別答案的正誤,完成最後總成績的統計、輸出; (2)題目不要出現重覆; (3)可定製題目數量和列印方式; (4)可以控制下列參數: ...
  • 一級指針 int *p; //表示定義一個int型(4位元組)的指針p &p //表示p自身的地址位置 p //表示p指向的地址位置(也就是p變數的值) *p //表示p指向的地址裡面的內容 所以 * 的作用: p變數的值作為地址,去訪問這個地址的內容 二級指針 int **pp //表示定義一個in ...
  • Python 元組 元組的定義 元組(tuple)是一種Python對象類型,元組也是一種序列 Python中的元組與列表類似,不同之處元組的元素不能修改 元組使用小括弧,列表使用方括弧 元組的創建,即在括弧中添加元素,並使用逗號隔開 元組是一種序列,序列的基本操作 len() 、+、*、in、ma ...
  • 定時運行任務對於一個網站來說,是一個比較重要的任務,比如定時發佈文檔,定時清理垃圾信息等,現在的網站大多數都是採用PHP動態語言開發的,而對於PHP的實現決定了它沒有Java和.Net這種AppServer的概念,而http協議是一個無狀態的協議,PHP只能被用戶觸發,被調用,調用後會自動退出記憶體, ...
  • 題目描述 火星探險隊的登陸艙將在火星錶面著陸,登陸艙內有多部障礙物探測車。登陸艙著陸後,探測車將離開登陸艙向先期到達的傳送器方向移動。探測車在移動中還必須採集岩石標本。每一塊岩石標本由最先遇到它的探測車完成採集。每塊岩石標本只能被採集一次。岩石標本被採集後,其他探測車可以從原來岩石標本所在處通過。探 ...
  • 首先本人學習變編程有半年了吧,現在是大四準畢業生,今年六月份畢業,專業是機電類的,與IT行業幾乎不相關,所以我是零基礎學習編程的。 看視頻學習 我接觸python編程是看書本自己理解然後自己跟著書本寫代碼,學了一段時間後朋友介紹說上網搜培訓視頻更容易學,於是我去找了一套視頻自己慢慢看慢慢學。接觸視頻 ...
  • 簡單讀取 json 配置文件 背景 目前發現網上的 .NET Core 讀取配置文件有點麻煩,自己想搞個簡單點的。 .NET Core 已經不使用之前的諸如 app.config 和 web.config 等 xml 形式的配置文件,一致採用 json 格式來存儲配置文件信息。 json 文件 de ...
  • 在WPF自學入門(二)WPF-XAML佈局控制項的文章中分別介紹StackPanel,WarpPanel,DockPanel,Grid,Canvas五種佈局容器的使用,可以讓我們大致瞭解容器可以使用在什麼地方。今天我們就來簡單瞭解一下WPF中的三個帶標題的內容控制項,分別是GroupBox,TabCon ...
一周排行
    -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.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...