天天看點

16.4.3 使用消息進行通信

16.4.3 使用消息進行通信

    清單 16.13 建立一個郵箱處理器,statee,類型為 MailboxProcessor<DrawingMessage>。注意,我們建立的 state 方法,是同名的非泛型類的一個成員。我們在 C# 中用來進行類型推斷的就是同樣的這種模式,在開始将其內建到我們的代碼之前,會看到郵箱處理器支援哪些操作。表 16.2 顯示了這個泛型類型最重要的執行個體方法。

Table 16.2 The most important methods provided by the MailboxProcessor<'Msg> type

郵箱函數 函數的描述
Post 發送消息到郵箱處理器,而無需等待任何回複。如果郵箱處理器忙,消息會儲存在隊列中。
PostAndReply 發送一條 AsyncReplyChannel <'T> 參數的消息,到郵箱處理器,阻塞調用線程,直到郵箱處理器調用通道的 Reply 方,然後,處理器傳回值,發送給通道。
PostAndAsyncReply 類似于 PostAndReply,但它是以異步方式運作的。它通常是從異步工作流中,使用 let! 調用的。是以,在消息等待進行處理期間,不阻止調用線程。
Receive 當建立郵箱處理器,異步地從隊列中接收下一條消息時,我們使用這個方法,這樣可以在工作流内對其進行處理。在郵箱處理器之外,不應使用此方法。
Scan 像 Receive 一樣,這個方法不應該用在郵箱處理器以外,當處理器處于無法處理所有類型的消息的狀态時,它可以使用。提供的 lambda 函數傳回一個選項類型,當包含異步工作流處理消息時,它可以傳回 Some,不能處理消息時,傳回 None。未處理的消息保留在隊列中,供以後處理。

    我們已經看到如何使用 Receive,稍後将談論 Scan,餘下的三個方法可以從任何線程使用。雖然處理器可以将消息發送到自身,有時很有用,但是,更典型的情況是,消息從不同的過程(例如,工作異步流實作圖形使用者界面的互動,或者背景的工作線程),發送給處理器。

改進繪圖程序

    現在我們知道什麼是可用的,可以改變繪圖程序,将儲存使用者已繪制的所有矩形,并允許使用者選擇不同的顔色。首先,我們需要更改繪圖代碼。原本 drawRectangle 函數會擦除整個螢幕,如果我們想要繪制多個矩形,這不是适當的。我們仍需要清除螢幕,但隻是一次,而不是為每個矩形。清單 16.14 顯示所有了一新函數,它繪制指定清單中的所有矩形。drawRectangle 函數沒有顯示,唯一的改變隻是删除第一個調用 Clear。

Listing 16.14 Utility function for drawing rectangles (F#)

let redrawWindow(rectangles) =

  use gr = form.CreateGraphics()

  gr.Clear(Color.White)

  for r in rectangles do

    drawRectangle(r)

    這個函數清除窗體的内容,然後,周遊給定清單中的所有元素,使用 drawRectangle 繪制每個單獨的矩形。清單中儲存的矩形,是有三個元素(顔色和兩個對角)的元組,相容于 drawRectangle 函數所預期的元組。

    現在,我們終于準備修改處理使用者互動的程序。因為整個代碼實作為一個異步工作流,我們想從儲存狀态的郵箱處理器資訊時,可以使用異步方法 PostAndAsyncReply 。當可以使用它時,這是首選方案,因為它不會阻止調用線程。清單 16.15 中的大部分的代碼與清單  16.11 是相同的,是以,我們已經突出顯示已更改的行。

Listing 16.15 Changes in the drawing process (F#)

let rec drawingLoop(clr, from) = async {

  let! move = Async. AwaitObservable(form.MouseMove)

  if (move.Button &&& MouseButtons.Left) = MouseButtons.Left then

    let! rects = state.PostAndAsyncReply(GetRectangles)

    redrawWindow(rects)

    drawRectangle(clr, from, (move.X, move.Y))

    return! drawingLoop(clr, from)

  else

    return (move.X, move.Y) }

let waitingLoop() = async {

  while true do

    let! down = Async. AwaitObservable(form.MouseDown)

    let downPos = (down.X, down.Y)

    if (down.Button &&& MouseButtons.Left) = MouseButtons.Left then

      let! clr = state.PostAndAsyncReply(GetColor)

      let! upPos = drawingLoop(clr, downPos)

      state.Post(AddRectangle(clr, downPos, upPos)) }

    讓我們首先看一下在 drawingLoop 函數中所做的改變,在這裡,我們更新了視窗。最初,在繪制新的矩形之前,需要擦除所有的内容,但現在,還需要繪制所有的、早先已經繪制的矩形。我們從郵箱處理器獲得矩形的清單,通過發送 GetRectangles 消息。這個消息有一個類型 AsyncReplyChannel<'T> 的參數,将用于郵箱處理器回複調用者,但我們在代碼中沒有顯式指定通道。F# 編譯器會将這個差别聯合的構造函數(GetRectangles),看作是隻有一個參數的函數。我們可以寫同樣的事情,像這樣:

let! rects = state.PostAndAsyncReply(fun chnl -> GetRectangles(chnl))

    如果以這種更長的形式寫代碼,可以很容易看到細節。PostAndAsyncReply 方法為回複建立了一個通道,并使用指定的 lambda 函數建立拾這個通道的消息。郵件然後發送能郵箱處理器,然後,工作流被挂起,直到回複發送給這個通道。一旦我們收到有矩形清單的回複,就可以繪制了。然後,立刻繪制使用者正在繪制的新矩形。注意,回複可以從背景線程發送。完成後,PostAndAsyncReply 方法傳回給調用者線程,是以,工作流的其餘部分将在圖形使用者界面線程上執行。

    第二個變化是在 waitingLoop 函數中,在這裡,使用者開始繪制新的矩形。首先,我們讀目前所選的顔色。我們還沒有實作使用者界面,選擇不同的顔色,但是,這方面很快就實作,是以,我們或許也有準備。對 AwaitObservable 的調用完成後,必須讀取顔色;否則,使用者可能在我們取到顔色後,開始繪圖前,更改顔色。一旦我們有了這個顔色,就可以調用 drawingLoop 函數,處理當使用者一直按下滑鼠按鈕的時間段。我們使用 Post 方法發送有關新建立矩形的所有資訊,給郵箱處理器。

添加使用者界面

    應用程式的使用者界面将會相當簡單,但我們會需要從不同的地方調用該郵箱處理器,處理應用程式的目前狀态。首先,我們将添加 Paint 事件的處理程式,是以,當視窗的任何的一部分無效時,應用程式就重繪矩形。這會發生在應用程式調整大小,或者其他的視窗移動它的前面。其次,我們添加有一個按鈕的工具欄,使用者可以更改目前的顔色。圖 16.6 顯示了運作中的應用程式。

16.4.3 使用消息進行通信

圖 16.6 運作中的應用程式,隻包含繪制的矩形。

    清單 16.16 顯示應用程式其餘部分最有趣的代碼。我們省略了建立工具欄和按鈕代碼,但在本書的網站上有完整源代碼。

Listing 16.16 Implementing the user interface (F#)

let btnColor = new ToolStripButton(...)

btnColor.Click.Add(fun _ –>

  use dlg = new ColorDialog()

  if (dlg.ShowDialog() = DialogResult.OK) then

    state.Post(SetColor(dlg.Color)) )

form.Paint.Add(fun _ –>

  let rects = state.PostAndReply(GetRectangles)

  redrawWindow(rects) )

[<STAThread>]

do

  Async.StartImmediate(waitingLoop())

  Application.Run(form)

    大部分代碼是相當簡單的。我們建立使用者界面,并注冊兩個事件處理程式。我們不需要用這些事件做任何複雜的事情,是以,隻是調用 Add 方法,而沒有使用 Observable 子產品的函數。第一個處理程式顯示 ColorDialog,以便使用者可以選擇新的顔色。如果選擇了一種顔色,釋出有新顔色的消息給郵箱處理器。我們不需要等待任何對這個消息的回複,是以,這個操作的執行不會阻止線程。

    第二個事件處理程式是為 Paint 事件。首先,要擷取已有矩形的清單,使用 PostAndReply 方法實作的。它構造的消息有回複通道,然後等待,直到郵箱發送回複。這個方法阻塞了線程,是以,隻有當無法以異步方式完成操作時,才應使用。請求更新基于 Windows Forms 的應用程式視窗,肯定是這些情況中的一種,是以,這種用法是正确的。

    到目前為止,我們一直在直接使用郵箱處理器對象。在開發的早期階段,這是好的,但一旦應用程式變得更大,或如果我們想要把應用程式的一部分到單獨的庫,把郵箱處理器封裝在對象中,會更好。雖然我們不打算更進一步擴充繪圖應用程式,但是,看一下它包含什麼,還是值得的。

繼續閱讀