同事回報一起案例,某WCF服務的[OperationContract]方法宣告為void Blah(ref int i, ref string s),以傳址方式(By Reference)傳遞參數(延伸閱讀:Self Test - Value Type vs Reference Type),程式運作多時,詢問我-WCF可以使用 ref 傳參數嗎?

一隻黑天鵝默默在我面前飛過,看得我瞠目結舌…

令人訝異之處在於,依我的理解WCF的Client/Server身處不同程序(Process),不管輸入參數還是傳回結果,都得序列化、反序列化才能正確傳遞,既然不屬於同一程序,記憶體地址空間不同,何來傳「址」?

爬文後,在MSDN找到一段說明

Out and Ref Parameters

In most cases, you can use in parameters (ByVal in Visual Basic) and out and ref parameters (ByRef in Visual Basic). Because both out and ref parameters indicate that data is returned from an operation, an operation signature such as the following specifies that a request/reply operation is required even though the operation signature returns void.

由此可知,WCF真的支援在參數使用 ref、out!而隨後的說明暗示,使用 out 或 ref 時,即使傳回型別為 void,實際仍會傳回資料。意味WCF在背後做了一些處置,偷偷傳回標為 out、ref 的參數到客戶端,模擬傳值參數行為。

哼!我冷笑一聲,這種「偽」傳值的做法,應該三兩下就破功吧?為什麼WCF要提供容易踩雷的黑心規格?

那就做個實驗踢爆「偽」傳值參數的黑暗面吧!

我設計以下的資料物件,共有三個成員,屬性 PubProp 標註 [DataMember] 可序列化,NonSerProp 則是一般欄位,預期不在序列化範圍,與 PubProp 形成對照。另外, Check() 方法可印出 PubProp 及 NonSerProp 偵查資料內容。

using System.Runtime.Serialization;
 
namespace WcfDto
{
    [DataContract]
    public class Foo
    {
        private string _PubProp;
        [DataMember]
        public string PubProp {
            get
            {
                return _PubProp;
            }
            set
            {
                _PubProp = value;
            }
        }
 
        public string NonSerProp = "Default";
 
        public string Check()
        {
            return string.Format("PubProp={0}, NonSerProp={1}",
                PubProp, NonSerProp);
        }
    }
}

建立一個 ByRefCall.svc,豪邁地使用 ref 傳址宣告 Test1(ref int i, ref stirng s) 及 Test2(ref Foo f) 兩個作業方法,被呼叫時修 i、s 及 f 值。

using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.Serialization;
using System.ServiceModel;
using System.Text;
using WcfDto;
 
namespace WcfWas
{
    [ServiceContract]
    public interface IByRefCall
    {
        [OperationContract]
        void Test1(ref int i, ref string s);
 
        [OperationContract]
        void Test2(ref WcfDto.Foo f);
    }
 
    public class ByRefCall : IByRefCall
    {
        public void Test1(ref int i, ref string s)
        {
            i = 32767;
            s = "Darkthread";
        }
 
        public void Test2(ref Foo f)
        {
            f.PubProp = "Server";
            f.NonSerProp = "Server";
        }
    }
}

呼叫端長這樣,分別執行 Test1 及 Test2,並比對變數如何變化:

        static void Main(string[] args)
        {
            BRC.ByRefCallClient c = new BRC.ByRefCallClient();
            int i = 1;
            string s = "Jeffrey";
            Console.WriteLine("Before: i={0}, s={1}", i, s);
            c.Test1(ref i, ref s);
            Console.WriteLine("After: i={0}, s={1}", i, s);
 
            WcfDto.Foo f = new Foo();
            f.PubProp = "Client";
            f.NonSerProp = "Client";
            Console.WriteLine("Before: {0}", f.Check());
            c.Test2(ref f);
            Console.WriteLine("After: {0}", f.Check());
 
            Console.Read();
        }

執行結果如下:

Before: i=1, s=Jeffrey
After: i=32767, s=Darkthread
Before: PubProp=Client, NonSerProp=Client
After: PubProp=Server, NonSerProp=

Test1(ref i, ref s) 沒什麼意外,i 與 s 都被正確更新。但 Test2(ref f) 就精彩了,原本 PubProp 跟 NonSerProp 都是"Client",呼叫 Test2 後,PubProp 正確換成"Server",但 NonSerProp 被改掉,既不是"Client",也不是"Server",而呈現空白。實際應用時,這種未預期結果已可認定為Bug,可能導致系統出錯。

但,事情是怎麼發生的?

為進一步調查,我修改 Foo.PubProp ,在資料被外界修改時輸出 Environment.StackTrace 追查呼叫來源:

        [DataMember]
        public string PubProp {
            get
            {
                return _PubProp;
            }
            set
            {
                Debug.WriteLine(string.Format("Set PubProp=>{0}", value));
                Debug.WriteLine(Environment.StackTrace.ToString());
                _PubProp = value;
            }
        }

追蹤結果如下:(省略部分冗長內容)

Set PubProp=>Client
   於 System.Environment.GetStackTrace(Exception e, Boolean needFileInfo)
   於 System.Environment.get_StackTrace()
   於 WcfDto.Foo.set_PubProp(String value) 於 X:\WorkRoom\WCFLab\WcfDto\Foo.cs: 行 24
   於 WCFClient.Program.Main(String[] args) 於 X:\WorkRoom\WCFLab\WCFClient\Program.cs: 行 89
   … 省略 …
   於 System.Threading.ThreadHelper.ThreadStart()
Set PubProp=>Client
   於 System.Environment.GetStackTrace(Exception e, Boolean needFileInfo)
   於 System.Environment.get_StackTrace()
   於 WcfDto.Foo.set_PubProp(String value) 於 X:\WorkRoom\WCFLab\WcfDto\Foo.cs: 行 24
   於 ReadFooFromXml(XmlReaderDelegator , XmlObjectSerializerReadContext , XmlDictionaryString[] , XmlDictionaryString[] )
   於 System.Runtime.Serialization.ClassDataContract.ReadXmlValue(XmlReaderDelegator xmlReader, XmlObjectSerializerReadContext context)
   於 System.Runtime.Serialization.XmlObjectSerializerReadContext.ReadDataContractValue(DataContract dataContract, XmlReaderDelegator reader)
    … 省略 …
   於 System.ServiceModel.Dispatcher.DataContractSerializerOperationFormatter.DeserializeParameterPart(XmlDictionaryReader reader, PartInfo part, Boolean isRequest)
   於 System.ServiceModel.Dispatcher.DataContractSerializerOperationFormatter.DeserializeParameter(XmlDictionaryReader reader, PartInfo part, Boolean isRequest)
   於 System.ServiceModel.Dispatcher.DataContractSerializerOperationFormatter.DeserializeParameters(XmlDictionaryReader reader, PartInfo[] parts, Object[] parameters, Boolean isRequest)
   … 省略 …
   於 System.ServiceModel.Channels.HttpPipeline.EmptyHttpPipeline.BeginProcessInboundRequest(ReplyChannelAcceptor replyChannelAcceptor, Action dequeuedCallback, AsyncCallback callback, Object state)
   於 System.ServiceModel.Channels.HttpChannelListener`1.HttpContextReceivedAsyncResult`1.ProcessHttpContextAsync()
   於 System.ServiceModel.Channels.HttpChannelListener`1.BeginHttpContextReceived(HttpRequestContext context, Action acceptorCallback, AsyncCallback callback, Object state)
   …省略…
Set PubProp=>Server
   於 System.Environment.GetStackTrace(Exception e, Boolean needFileInfo)
   於 System.Environment.get_StackTrace()
   於 WcfDto.Foo.set_PubProp(String value) 於 X:\WorkRoom\WCFLab\WcfDto\Foo.cs: 行 24
   於 WcfWas.ByRefCall.Test2(Foo& f) 於 X:\WorkRoom\WCFLab\WcfWas\ByRefCall.svc.cs: 行 31
   於 SyncInvokeTest2(Object , Object[] , Object[] )
   於 System.ServiceModel.Dispatcher.SyncMethodInvoker.Invoke(Object instance, Object[] inputs, Object[]& outputs)
   … 省略 …
Set PubProp=>Server
   於 System.Environment.GetStackTrace(Exception e, Boolean needFileInfo)
   於 System.Environment.get_StackTrace()
   於 WcfDto.Foo.set_PubProp(String value) 於 X:\WorkRoom\WCFLab\WcfDto\Foo.cs: 行 24
   於 ReadFooFromXml(XmlReaderDelegator , XmlObjectSerializerReadContext , XmlDictionaryString[] , XmlDictionaryString[] )
   … 省略 …
   於 System.ServiceModel.Channels.ServiceChannelProxy.InvokeService(IMethodCallMessage methodCall, ProxyOperationRuntime operation)
   於 WCFClient.BRC.ByRefCallClient.WCFClient.BRC.IByRefCall.Test2(Test2Request request) 於 X:\WorkRoom\WCFLab\WCFClient\Service References\BRC\Reference.cs: 行 152
   於 WCFClient.BRC.ByRefCallClient.Test2(Foo& f) 於 X:\WorkRoom\WCFLab\WCFClient\Service References\BRC\Reference.cs: 行 158
   於 WCFClient.Program.Main(String[] args) 於 X:\WorkRoom\WCFLab\WCFClient\Program.cs: 行 92
   … 省略 …

由 StackTrace 的程式位置,PubProp 總共被設定四次:

第一次,Main() f.PubProp = "Client"
第二次,有個 ReadFooFromXml() 函式由SOAP傳送內容讀出Foo,轉成輸入參數交給 ByRefCall.Test2
第三次,ByRefCall.Text2() 執行 f.PubProp = "Server"
第四次,Test2() 執行結果傳回客戶端,再用 ReadFooFromXml() 由 SOAP 讀取結果更新回Foo物件

第四次 StackTrace 有兩個程式行數值得注意。Reference.cs 158行呼叫152行,追到原始碼(如下圖),謎底揭曉。
使用 ref 時,WCF Proxy 會在背後宣告一個 inValue,將 Foo 放進 inValue.f,呼叫 Test2() 取得 retVal,再將原本的 inVal.f 換成 retVal.f。

在這種邏輯,retVal.f 有可能是另一顆新建立的 Foo,也有可能是原來的 f 物件,透過 ReadFooFromXml() 將屬性更新為 SOAP傳回內容。試過為 Foo 加入建構式,發現在 Client 端 Foo 只被建立一次,故可排除另建 Foo 的假設,物件 f 應該還是同一顆,藉由 ReadFooFromXml() 更新 PupProp 屬性,至於為什麼 NonSerProp 會被改成空白,得深入 ReadFooFromXml 邏輯一探究竟。至少,我們得到一項結論:

使用 By Reference 傳遞參數時,不在序列化範圍的屬性或欄位有可能出現非預期結果。

基於以上行為,我認為用 ref 傳遞傳遞Value Type(int、string、decimal…)還好,不致出問題。但用 ref 傳送物件參數是件危險的事,序列化範圍以外的屬性、欄位就有錯亂的風險,跟大家想像的傳統 ref 傳址行為不完全相同,再加上 By Referecne 傳遞違反 WCF 傳輸的本質,不如大家就忘掉「WCF 可以用 ref」這件事吧!


Comments

# by CHC

大大,看到 ref 後太開心,我來幫忙補充一下吧! 首先,使用 WCF,是屬於遠端呼叫,即使不使用 ref 的定義,同樣的狀況也是會發生,主要是因為 class Foo 直接使用 [DataMember] 定義,這樣就不會自動化的轉換了,序列化的自動轉換,有明確定義與沒有明確定義,哪些可以轉換,哪些不能自動,這在微軟文件中有說明,所以依據程式碼,會被轉換的只有 PubProp,本來也會被轉換的 NonSerProp,就因為明確定義而不轉換了,所以能夠重遠端回來的,就只有可以被序列化的部分,其他數值,會是型態預設值,這裡 String 的數值會是 default(String) [使用 By Reference 傳遞參數時,不在序列化範圍的屬性或欄位有可能出現非預期結果。],所以其實非虛列化的部分的值會是 default(x) 那 ref 有何用處呢,如其名,使用傳址,就是不重新建立新的物件,所以如果不使用 ref,屬於 Value Type 的,會重新建立,占據新的記憶體空間,屬於 Reference Type 的物件,會重新建立 [指向相同物件的一個參數] 之物件,這耗用的記憶體很小 (會不會建立,牽涉到 Scope 與編譯器設定,最佳化時,不見得真的會建立傳入區域的參數物件) 那何時使用 ref 比較好呢? 如果是一個物件,使用 ref 的話,好處很少,因為物件已經是 Reference Type 了 如果是一個大的字串,因為是 Value Type,所以不需要重新建立該字串,直接使用,速度會快一點,有多快呢?以一般程式功力的人所寫的效能而言,相對得到的速度提升,感覺不出來。 所以使用場合很少吧!,建議是,需要才使用。 若是要在更嚴格而論,這其實有幾種組合,例如,如果 WCF Server 與 Client 運行在同一個執行緒時,狀況又會不同,常常新手,都用同一個專案來練習,首先會發生的就是死結,再來就是這個序列化,回傳資料不如預期,到這裡,就不再多說,離題了,可自行 Google AppDomain serializ wcf 之類。

Post a comment