聽我說,你的函數不夠函數,你的函數需要逆變

来源:https://www.cnblogs.com/dahua-dijkstra/archive/2022/04/27/16198839.html
-Advertisement-
Play Games

一個工作了7年的Java程式員,私信我關於分散式鎖的問題。 一上來就兩個靈魂拷問: Redis鎖超時怎麼辦? Redis主從切換導致鎖失效怎麼辦? 我說,彆著急,這些都是小問題。 那麼,關於“分散式鎖的理解和實現”這個問題,我們看看普通人高手的回答。 普通人: 嗯,分散式鎖,就是可以用來實現鎖的分佈 ...


聽我說,你的函數不夠函數,你的函數需要逆變

禁止轉載

Java8 引入了函數式介面,從此方法傳參可以傳遞函數了,有人說:

不就是傳一個方法嗎,語法糖!

lambda表達式?語法糖!

你是否認為協變和逆變只是定義了集合間的關係,如果你的回答是是,那麼這篇文章會改變你原有的看法。

嘗試回答一下問題:

  1. 函數和方法有區別嗎?
  2. 協變是什麼,有什麼使用場景
  3. 逆變是什麼,有什麼使用場景

一、純函數—沒有副作用

純函數的執行不會帶來對象內部參數、方法參數、資料庫等的改變,這些改變都是副作用。比如Integer::sum是一個純函數,輸入為兩個int,輸出為兩數之和,兩個輸入量不會改變,在Java 中可以申明為final int類型。

副作用的執行

Java對於不變類的約束明顯不足,比如final array只能保證引用的指向不變,array內部的值還是可以改變的,如果存在第二個引用指向相同的array,那麼將無法保證array不可變;標準庫中的collection常用的還是屬於可變muttable類型,可變類型在使用時很便利。

在函數式思想下,函數是一等公民,函數是有值的,比如Integer::sum就是函數類型BiFunction<Integer, Integer, Integer>的一個值,沒有副作用的函數保證了函數可以看做一個黑盒,一個固定的輸入便有固定的輸出。

那麼Java中對象的方法是純函數嗎?

大多數時候不是。對象的方法受到對象的狀態影響,如果對象的狀態不發生改變,同時不對外部產生影響(比如列印字元串),可以看做純函數。註意:Java中的final類無法保證內部參數狀態不發生改變。

那麼純函數的限制這麼多,究竟有什麼用呢?相信看到最後你會懂的。

本文之後討論的函數都預設為純函數。

二、協變—更抽象的繼承關係

協變和逆變描述了繼承關係的傳遞特性,協變比逆變更好理解,逆變我放到後面說。

學習這些概念前先不要考慮Java對泛型的實現,它會影響你的正確認識。當你對這些概念理解了之後,再往Java的實現上面去套用,你就很容易理解了。

協變的簡單定義:如果A是B的子類,那麼F(A)是F(B) 的子類。F表示的是一直類型變換。

比如:貓是動物,表示為Cat < Animal,那麼一群貓是一群動物,表示為List[Cat] < List[Aniaml]。

上面的關係很好理解,在面向對象語言中,is-a表示為繼承關係,即貓是動物的子類(subtype)。

所以,協變可以這樣表示:

A < B ⇒ F(A) < F(B)

在貓的例子中,F表示集合。

那麼如果F是函數呢?

我們定義函數F=Provider,函數的類型定義包括入參和出參,簡單地考慮入參為空,出參為Animal和Cat的情況。簡單理解為方法F定義為獲取貓或動物。

那麼Supplier作用Cat和Animal上,原來的類型關係保持嗎?

答案是保持,Supplier[Cat] < Supplier[Animal]。也就是說獲取一隻貓就是獲取一隻動物。轉換成面向對象的語言,Supplier[Cat]是Supplier[Animal]的子類。

在面向對象語言中,子類關係常常表現為不同類型之間的相容。也就是說傳值的類型必須為聲明的類型的子類。如下麵的代碼是好的

List[User] users = List(user1, user2)
List[Animal] animals = cats
Supplier[Animal] supplierWithAnimal = supplierWithCat
// 使用Supplier[Animal],實際上得到的是Cat
Animal animal = supplierWithAnimal.get()

我們來看下百度百科對於里氏替換原則(LSP)的定義:

里氏代換原則(Liskov Substitution Principle LSP)面向對象設計的基本原則之一。 里氏代換原則中說,任何父類可以出現的地方,子類一定可以出現。 LSP是繼承復用的基石,只有當子類可以替換掉父類,軟體單位的功能不受到影響時,父類才能真正被覆用,而子類也能夠在父類的基礎上增加新的行為。里氏代換原則是對“開-閉”原則的補充。實現“開-閉”原則的關鍵步驟就是抽象化。而子類與父類的繼承關係就是抽象化的具體實現,所以里氏代換原則是對實現抽象化的具體步驟的規範。

Animal animal = new Cat(”kitty”);

在UML圖中,一般父類在上,子類在下。因此,子類賦值到父類聲明的過程可以形象地稱為向上轉型。

總結一下:協變是LSP的體現,形象的理解為向上轉型。

三、逆變—難以理解的概念

與協變的定義相反,逆變可以這樣表示:

A < B ⇒ F(B) < F(A)

最簡單的逆變類是Consumer[T],考慮Consumer[Fruit] 和 Consumer[Apple]。榨汁機就是一類Consumer,接受的是水果,輸出的是果汁。我定義的函數accpt為了避免副作用,返回字元串,然後再列印。

下麵我用scala寫的示例,其比Java簡潔一些,也是靜態強類型語言。你可以使用網路上的線上運行環境運行scastie.scala-lang.org。

// scala 變數名在前,類型在後,函數返回類型在括弧後,可以省略
class Fruit(val name: String) {}

class Apple extends Fruit("蘋果") {}

class Orange extends Fruit("橙子") {}

// 榨汁機,T表示泛型,<:表示匹配上界(榨汁機只能榨果汁),-T 表示T支持逆變
class Juicer[-T <: Fruit] {
  def accept(fruit: T) = s"${fruit.name}汁"
}

val appleJuicer: Juicer[Apple] = Juicer[Fruit]()
println(appleJuicer.accept(Apple()))

// 編譯不通過,因為appleJuicer的類型是Juicer[Apple]
// 雖然聲明appleJuicer時傳遞的值是水果榨汁機,但是編譯器只做類型檢查,Juicer[Apple]類型不能接受其他水果
println(appleJuicer.accept(Orange()))

榨汁機 is-a 榨蘋果汁機,因為榨汁機可以榨蘋果。

逆變難以理解的點就在於逆變考慮的是函數的功能,而不是函數具體的參數。

參數傳參原則上都可以支持逆變,因為對於純函數而言,參數值並不可變。

再舉一個例子,Java8 中stream的map方法需要的參數就是一個函數:

// map方法聲明
<R> Stream<R> map(Function<? super T, ? extends R> mapper);

// 此時方法的參數就是T,我們傳遞的mapper的入參可以為T的父類, 因為mapper支持參數逆變
// 如下程式可以運行
// 你可以對任意一個Stream<T>流使用map(Object::toString),因為在Java中所有類都繼承自Object。
Stream.of(1, 2, 3).map(Object::toString).forEach(System.out::println);

問題可以再複雜一點,如果函數的參數為集合類型,還可以支持逆變嗎?

當然可以,如前所述,逆變考慮的是函數的功能,傳入一個更為一般的函數也可以處理具體的問題。

// Scala中可以使用 ::: 運算符合併兩個List, 下一行是List中對方法:::的聲明
// def ::: [B >: A](prefix: List[B]): List[B]
// 這個方法在Java很難實現,你可以看看ArrayList::addAll的參數, 然後想想曲線救國的方案,下一篇文章我會詳細討論

// usage
val list: List[Fruit] = List(Apple()) ::: (List(Fruit("水果")))
println(list)
// output: List(Playground$Apple@74046e99, Playground$Fruit@8f0fecd)

總結一下:函數的入參可以支持逆變,即參數的繼承關係和函數的繼承關係相反,逆變的函數更通用。

不知道你是否還記得Effective Java中提出的PECS法則,Provider-extends, Consumer-super,這個法則和本文所說的協變、逆變又有什麼關係呢?

其實PECS法則從使用角度實現了許多泛型方法,下一篇文章再詳細說吧。


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

-Advertisement-
Play Games
更多相關文章
  • 業務需求 最近公司在做養老相關的業務,現在需要子女從小程式端對家裡的老人通過家庭終端交互屏進行實時看護。 解決方案 第三方的一些現成的服務:騰訊音視頻通話、直播功能; 阿裡的音視頻通信;兩者都挺好的,但是需要收費因此放棄決定自己搭建一套直播流服務; 先看效果(自己伺服器配置低有延遲、放到公司伺服器上 ...
  • 26名學生,每個人可以填寫10個交談對象: 10輪匹配結果: 1、pom.xml <dependency> <groupId>junit</groupId> <artifactId>junit</artifactId> <version>4.11</version> <scope>test</sco ...
  • 教程說明 C++高性能網路服務保姆級教程 首發地址 day02 真正的高併發還得看IO多路復用 本節目的 使用epoll實現一個高併發的伺服器 從單進程講起 上節從一個基礎的socket服務說起我們實現了一個基本的socket伺服器,並留了個思考題 先啟動server,然後啟動一個client,不輸 ...
  • LinkedList底層結構 說明 LinkedList底層實現了雙向鏈表和雙端隊列特點 可以添加任意元素(元素可以重覆),包括null 線程不安全,沒有實現同步 操作機制: LInkedList底層維護了一個雙向鏈表 LinkedList中維護了兩個屬性first和last分別指向首節點和尾節點 ...
  • 可改不可改? 常量表達式是指在編譯器編譯時期就可以的到值的表達式,例如:const int a = 3,雖然聽起來很簡單,但實際上很容易犯錯。本文簡要總結它們的不同: 關於const:頂層const和底層const 一般,我們聲明const用來存儲不變的恆定量: const int MAXN = i ...
  • 前言 Python 在程式並行化方面多少有些聲名狼藉。撇開技術上的問題,例如線程的實現和 GIL,我覺得錯誤的教學指導才是主要問 題。常見的經典 Python 多線程、多進程教程多顯得偏"重"。而且往往隔靴搔癢,沒有深入探討日常工作中最有用的內容。 傳統的例子 簡單搜索下"Python 多線程教程" ...
  • 上一篇 https://www.cnblogs.com/redwinter/p/16196359.html 介紹了BeanFactoryPostProcessor的執行過程,這篇文章介紹Spring中配置的註解是如何通過ConfigurationClassPostProcessor解析的,另外分析下 ...
  • 前言 一天不寫代碼,這手就閑的發慌,好難受啊… 前面幾篇都是講一些基礎的知識,沒具體的案例,小伙伴看起來比較枯燥,有不少小伙伴給小編提建議以後多出一些具體的 案例。本篇就是拿部落論壇作為測試項目,寫一個簡單的登錄測試腳本。 在寫登錄腳本的時候呢,先要保證流程能跑起來,然後才是去想辦法優化代碼,讓自己 ...
一周排行
    -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.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...