適合C# Actor的訊息執行方式(2):C# Actor的尷尬

iDotNetSpace發表於2009-07-20

  在上一篇文章中,我們簡單解讀了Erlang在執行訊息時候的方式。而現在,我們就一起來看看,C# Actor究竟出現了什麼樣的尷尬。此外,我還打算用F#進行補充說明,最終我們會發現,雖然F#看上去很美,但是在實際使用過程中依舊有些遺憾。

Erlang中的Tag Message

  老趙在上一篇文章裡提到,Erlang中有一個“約定俗成”,使用“原子(atom)”來表示這條訊息“做什麼”,並使用“繫結(binding)”來獲取做事情所需要的“引數”。Erlang大拿,《Programming Erlang》一書的主要譯者jackyz同學看了老趙的文章後指出,這一點在Erlang程式設計規範中有著明確的說法,是為“Tag Message”:

5.7 Tag messages

All messages should be tagged. This makes the order in the receive statement less important and the implementation of new messages easier.

Don’t program like this:

loop(State) ->
 
receive
    ...
    {
Mod, Funcs, Args} -> % Don't do this
     
apply(Mod, Funcs, Args},
     
loop(State);
    ...
 
end.

The new message {get_status_info, From, Option} will introduce a conflict if it is placed below the {Mod, Func, Args} message.

If messages are synchronous, the return message should be tagged with a new atom, describing the returned message. Example: if the incoming message is tagged get_status_info, the returned message could be tagged status_info. One reason for choosing different tags is to make debugging easier.

This is a good solution:

loop(State) ->
 
receive
    ...
    {
execute, Mod, Funcs, Args} -> % Use a tagged message.
     
apply(Mod, Funcs, Args},
     
loop(State);
    {
get_status_info, From, Option} ->
     
From ! {status_info, get_status_info(Option, State)},
     
loop(State);   
    ...
 
end.

  第一段程式碼使用的模式為擁有三個“繫結”的“元組”。由於Erlang的弱型別特性,任何擁有三個元素的元組都會被匹配到,這不是一個優秀的實踐。在第二個示例中,每個模式使用一個“原子”來進行約束,這樣可以獲取到相對具體的訊息。為什麼說“相對”?還是因為Erlang的弱型別特性,Erlang無法對From和Option提出更多的描述。同樣它也無法得知execute或get_status_info這兩個tag的來源——當然,在許多時候,它也不需要關心是誰傳送給它的。

在C#中使用Tag Message

  在C#中模擬Erlang裡的Tag Message很簡單,其實就是把每條訊息封裝為Tag和引數列表的形式。同樣的,我們使用的都是弱型別的資料——也就是object型別。如下:

public class Message
{
    public object Tag { get; private set; }

    public ReadOnlyCollection<object> Arguments { get; private set; }

    public Message(object tag, params object[] arguments)
    {
        this.Tag = tag;
        this.Arguments = new ReadOnlyCollection<object>(arguments);
    }
}

  我們可以使用這種方式來實現一個乒乓測試。既然是Tag Message,那麼定義一些Tag便是首要任務。Tag表示“做什麼”,即訊息的“功能”。在乒乓測試中,有兩種訊息,共三個“含義”。Erlang使用原子作為tag,在.NET中我們自然可以使用列舉:

public enum PingMsg
{ 
    Finished,
    Ping
}

public enum PongMsg
{ 
    Pong
}

  在這裡,我們使用簡單的ActorLite進行演示(請參考ActorLite的使用方式)。因此,Ping和Pong均繼承於Actor類,並實現其Receive方法。

  對於Ping物件來說,它會維護一個計數器。每當收到PongMsg.Pong訊息後,會將計數器減1。如果計數器為0,則回覆一條PingMsg.Finished訊息,否則就回復一個PingMsg.Ping:

public class Ping : Actor<Message>
{
    private int m_count;

    public Ping(int count)
    {
        this.m_count = count;
    }

    public void Start(Actor<Message> pong)
    {
        pong.Post(new Message(PingMsg.Ping, this));
    }

    protected override void Receive(Message message)
    {
        if (message.Tag.Equals(PongMsg.Pong))
        {
            Console.WriteLine("Ping received pong");

            var pong = message.Arguments[0] as Actor<Message>;
            if (--this.m_count > 0)
            {
                pong.Post(new Message(PingMsg.Ping, this));
            }
            else
            {
                pong.Post(new Message(PingMsg.Finished));
                this.Exit();
            }
        }
    }
}

  對於Pong物件來說,如果接受到PingMsg.Ping訊息,則回覆一個PongMsg.Pong。如果接受的訊息為PingMsg.Finished,便立即退出:

public class Pong : Actor<Message>
{
    protected override void Receive(Message message)
    {
        if (message.Tag.Equals(PingMsg.Ping))
        {
            Console.WriteLine("Pong received ping");

            var ping = message.Arguments[0] as Actor<Message>;
            ping.Post(new Message(PongMsg.Pong, this));
        }
        else if (message.Tag.Equals(PingMsg.Finished))
        {
            Console.WriteLine("Finished");
            this.Exit();
        }
    }
}

  啟動乒乓測試:

new Ping(5).Start(new Pong());

  結果如下:

Pong received ping
Ping received pong
Pong received ping
Ping received pong
Pong received ping
Ping received pong
Pong received ping
Ping received pong
Pong received ping
Ping received pong
Finished

  從上述程式碼中可以看出,由於沒有Erlang的模式匹配,我們必須使用if…else…的方式來判斷訊息的Tag,接下來還必須使用麻煩而危險的cast操作來獲取引數。更令人尷尬的是,與Erlang相比,在C#中使用Tag Message沒有獲得任何好處。同樣是弱型別,同樣得不到靜態檢查。那麼好處在哪裡?至少我的確看不出來。

  有朋友可能會說,C#既然是一門強型別的語言,為什麼要學Erlang的Tag Message?為什麼不把Ping定義為Actor,同時把Pong定義為Actor呢?

  呃……我承認,在這裡使用Tag Message的確有種“畫虎不成反類犬”的味道。不過,事情也不是您想象的那麼簡單。因為在實際情況中,一個Actor可能與各種外部服務打交道,它會接受到各式各樣的訊息。例如,它先向Service Locator傳送一個請求,用於查詢資料服務的位置,這樣它會接受到一個ServiceLocatorResponse訊息。然後,它會向資料服務傳送一個請求,再接受到一個DataAccessResponse訊息。也就是說,很可能我們必須把每個Actor都定義為Actor,然後對訊息進行型別判斷,轉換,再加以處理。

  誠然,這種方法相對於Tag Message擁有了一定的強型別優勢(如靜態檢查)。但是如果您選擇這麼做,就必須為各種訊息定義不同的型別,在這方面會帶來額外的開發成本。要知道,訊息的數量並不等於Actor型別的數量,即使是如Ping這樣簡單的Actor,都會傳送兩種不同的訊息(Ping和Finished),而且每種訊息擁有各自的引數。一般來說,某個Actor會接受2-3種訊息都是比較正常的狀況。在面對訊息型別的汪洋時,您可能就會懷念Tag Message這種做法了。到時候您可能就會發牢騷說:

  “弱型別就弱型別吧,Erlang不也用的好好的麼……”

F#中的模式匹配

  提到模式匹配,熟悉F#的同學們可能會歡喜不已。模式匹配是F#中的重要特性,它將F#中靜態型別系統的靈活性體現地淋漓盡致。而且——它還很能節省程式碼(這點在老趙以前的文章中也有所提及)。那麼我們再來看一次F#在乒乓測試中的表現。

  首先還是定義PingMsg和PongMsg:

type PingMsg = 
    | Ping of PongMsg Actor
    | Finished
and PongMsg = 
    | Pong of PingMsg Actor

  這裡體現了F#型別系統中的Discriminated Unions。簡單地說,它的作用是把一種型別定義為多種表現形式,這個特性在Haskell等程式語言中非常常見。Discriminated Unions非常適合模式匹配,現在的ping物件和pong物件便可定義如下(在這裡還是使用了ActorLite,而不是F#標準庫中的MailboxProcessor來實現Actor模型):

let (<let 
ping = let count = ref 5 { new PongMsg Actor() with override self.Receive(message) = match message with | Pong(pong) -> printfn "Ping received pong" count := !count - 1 if (!count > 0) then pong << Ping(self) else pong << Finished self.Exit() } let pong = { new PingMsg Actor() with override self.Receive(message) = match message with | Ping(ping) -> printfn "Pong received ping" ping << Pong(self) | Finished -> printf "Fininshed" self.Exit() }

  例如在pong物件的實現中,我們使用模式匹配,減少了不必要的型別轉換和賦值,讓程式碼變得簡潔易讀。還有一點值得順帶一提,我們在F#中可以靈活的定義一個操作符的作用,在這裡我們便把“<

ping << Pong(pong)

  至於結果則與C#的例子一模一樣,就不再重複了。

F#中的弱型別訊息

  可是,F#的世界就真的如此美好嗎?試想,我們該如何實現一個需要接受多種不同訊息的Actor物件呢?我們只能這樣做:

let another = 
    { new obj Actor() with
        override self.Receive(message) =
            match message with
            
            | :? PingMsg as pingMsg ->
                // sub matching
                match pingMsg with
                | Ping(pong) -> null |> ignore
                | Finished -> null |> ignore
                
            | :? PongMsg as pongMsg ->
                // sub matching
                match pongMsg with
                | Pong(ping) -> null |> ignore
                
            | :? (string * int) as m ->
                // sub binding
                let (s, i) = m
                null |> ignore
                
            | _ -> failwith "Unrecognized message" }

  由於我們必須使用object作為Actor接受到的訊息型別,因此我們在對它作模式匹配時,只能進行引數判斷。如果您要更進一步地“挖掘”其中的資料,則很可能需要進行再一次的模式匹配(如PingMsg或PongMsg)或賦值(如string * int元組)。一旦出現這種情況,在我看來也變得不是那麼理想了,我們既沒有節省程式碼,也沒有讓程式碼變得更為易讀。與C#相比,唯一的優勢可能就是F#中相對靈活的型別系統吧。

  C#不好用,F#也不行……那麼我們又該怎麼辦?

來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/12639172/viewspace-609711/,如需轉載,請註明出處,否則將追究法律責任。

相關文章