閱讀968 返回首頁    go 小米 go 小米5


WCF技術剖析之一:通過一個ASP.NET程序模擬WCF基礎架構

細算起來,已經有好幾個月沒有真正的寫過文章了。近半年以來,一直忙於我的第一本WCF專著《WCF技術剖析》的寫作,一直無暇管理自己的Blog。到目前為止《WCF技術剖析(卷1)》的寫作暫告一段落,初步預計於下個月由武漢博文視點出版。在《WCF技術剖析》寫作期間,對WCF又有了新的感悟,為此以書名開始本人的第三個WCF係列。本係列的目的在於對《WCF技術剖析》的補充,會對書中的一些內容進行展開講述,同時會囊括很多由於篇幅的原因忍痛割棄的內容。

本係列的第一篇,我將會對WCF的基本架構作一個大致的講解。不過,一改傳統對WCF的工作流程進行平鋪直敘,我將另辟蹊徑,借助於我們熟悉的ASP.NET作為請求處理平台,通過一個簡單的托管程序模擬整個WCF客戶端和服務端的架構。Source Code下載:Artech.WcfFrameworkSimulator.zip

我們的模擬程序將你搭建一個迷你版的WCF框架,為了展示WCF整個處理流程中使用到一些特殊組件。我們首先來簡單介紹一下對於一個簡單的WCF服務調用,WCF的客戶端和服務端框架的處理流程,和該流程的每一個階段都使用那些重要組件。

下麵的列表列出了WCF服務端框架對於處理一個簡單的WCF服務調用請求所提供的功能,以及相應的功能承載的組件:

  • 請求消息的接收和回複消息的發送:服務端在傳輸層監聽與接收來自客戶的請求,並將經過編碼後的回複消息通過傳輸層發送到客戶端
  • 請求消息的解碼和回複消息的編碼:將接收到的字節數組通過解碼生成請求消息對象,並將回複消息通過編程轉化成字節組。消息的編碼和解碼通過MessageEncoder完成,而MessageEncoderFactory負責創建該對象
  • 請求消息的反序列化和回複消息的序列化:對請求消息進行反序列化,為服務操作的執行生成相應的輸入參數,以及將服務操作執行的結果(返回值或者ref/out參數)序列化,並生成回複消息。序列化和反序列化通過DispatchMessageFormatter完成
  • 服務對象的創建:創建或者激活服務對象實例,InstanceProvider用於服務對象的創建或獲取
  • 服務操作的執行:調用創建的服務對象的操作方法,並傳入經過反序列化生成的輸入參數。OperationInvoker完成對服務操作的最終執行

較之服務端的流程,客戶端的流程顯得相對簡單,僅僅包含以下三個必需的階段:

  • 請求消息的序列化和回複消息的反序列化:生成請求消息並將輸入參數序列化到請求消息中,以及對回複消息進行反序列化,轉化成方法調用的返回值或者ref/out參數。序列化和反序列化通過ClienthMessageFormatter完成
  • 請求消息的編碼和回複消息的解碼:對請求消息進行編碼生成字節數組供傳輸層發送,以及將傳輸層接收到的字節數組解碼生成恢複消息。消息的編碼和解碼通過MessageEncoder完成,而MessageEncoderFactory負責創建該對象
  • 請求消息的發送和回複消息的接收:在傳輸層將經過編碼的請求消息發送到服務端,以及將接收來自服務端的恢複消息

clip_image002

圖1 精簡版WCF客戶端與服務端組件

圖1反映了進行服務調用的必要步驟和使用到的相關WCF組件。在本案例演示中,我們需要做的就是手工創建這些組件,並通過我們自己的代碼利用它們搭建一個簡易版的WCF框架。如果讀者能夠對本案例的實現有一個清晰的理解,相信對於整個WCF的框架就不會感到陌生了。

圖2顯示了本案例解決方案的基本結構,總共分三個項目。Contracts用於定義服務契約,被服務端和客戶端引用。客戶端通過一個Console應用模擬,而服務端則通過一個ASP.NET Website實現。

clip_image002[5]

圖2 WCF框架模擬案例應用結構

步驟一、通過服務契約類型創建相關組件

WCF在整個服務調用生命周期的不同階段,會使用到不同的組件。我們通過一個方法將服務端和客戶端所需的所有組件都創建出來,為此,我們在Contracts項目中添加了一個Utility類型,在Create<T>方法中創建所有的組件並通過輸出參數的形式返回,泛型類型T表示的是服務契約類型。在該方法中,輸出參數encoderFactory被服務端和客戶端用於消息的編碼和解碼,clientFormatters和dispatchFormatters以字典的形式包含了基於服務操作的IClientMessageFormatter和IDispatchMessageFormatter,其中clientFormatters和dispatchFormatters的Key分別為操作名稱和操作對應的Action。同樣通過字典形式返回的operationInvokers和methods用於在服務端執行相應的操作方法,Key同樣為操作對應的Action。

   1: public static class Utility
   2: {
   3:     public static void Create<T>(out MessageEncoderFactory encoderFactory,
   4:         out IDictionary<string, IClientMessageFormatter> clientFormatters,
   5:         out IDictionary<string, IDispatchMessageFormatter> dispatchFormatters,
   6:         out IDictionary<string, IOperationInvoker> operationInvokers,
   7:         out IDictionary<string, MethodInfo> methods)
   8:     {
   9:         //省略實現
  10:     }
  11: }
具體的實現如下,由於在WCF框架中使用的MessageEncoderFactory(TextMessageEncoderFactory)、MessageFormatter(DataContractSerializerOperationFormatter)和OperationInvoker(SyncMethodInvoker)都是一些內部類型,所以隻能通過反射的方式創建它們。而操作名稱和Action也主要通過反射的原理解析應用在服務方法上的OperationContractAttribute得到。
   1: public static void Create<T>(out MessageEncoderFactory encoderFactory,
   2:     out IDictionary<string, IClientMessageFormatter> clientFormatters,
   3:     out IDictionary<string, IDispatchMessageFormatter> dispatchFormatters,
   4:     out IDictionary<string, IOperationInvoker> operationInvokers,
   5:     out IDictionary<string, MethodInfo> methods)
   6: {
   7:     //確保類型T是應用了ServiceContractAttribute的服務契約
   8:     object[] attributes = typeof(T).GetCustomAttributes(typeof(ServiceContractAttribute), false);
   9:     if (attributes.Length == 0)
  10:     {
  11:         throw new InvalidOperationException(string.Format("The type \"{0}\" is not a ServiceContract!", typeof(T).AssemblyQualifiedName));
  12:     } 
  13:  
  14:     //創建字典保存IClientMessageFormatter、IDispatchMessageFormatter、IOperationInvoker和MethodInfo
  15:     clientFormatters = new Dictionary<string, IClientMessageFormatter>();
  16:     dispatchFormatters = new Dictionary<string, IDispatchMessageFormatter>();
  17:     operationInvokers = new Dictionary<string, IOperationInvoker>();
  18:     methods = new Dictionary<string, MethodInfo>(); 
  19:  
  20:     //MessageEncoderFactory
  21:     string encoderFactoryType = "System.ServiceModel.Channels.TextMessageEncoderFactory,System.ServiceModel, Version=3.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089";
  22:     encoderFactory = (MessageEncoderFactory)Activator.CreateInstance(Type.GetType(encoderFactoryType), MessageVersion.Default, Encoding.UTF8, int.MaxValue, int.MaxValue, new XmlDictionaryReaderQuotas()); 
  23:  
  24: //得到OperationDecription列表
  25: string defaultNamespace = "https://tempuri.org/";
  26:     ServiceContractAttribute serviceAttribute = (ServiceContractAttribute)attributes[0];
  27:     string serviceNamepace = string.IsNullOrEmpty(serviceAttribute.Namespace) ? defaultNamespace : serviceAttribute.Namespace;
  28:     string serviceName = string.IsNullOrEmpty(serviceAttribute.Name) ? typeof(T).Name : serviceAttribute.Name;
  29:     var operations = ContractDescription.GetContract(typeof(T)).Operations; 
  30:  
  31:     //得到具體的IClientMessageFormatter、IDispatchMessageFormatter和IOperationInvoker的具體類型
  32:     //IClientMessageFormatter+IDispatchMessageFormatter:DataContractSerializerOperationFormatter
  33:     //IOperationInvoker:SyncMethodInvoker
  34:     string formatterTypeName = "System.ServiceModel.Dispatcher.DataContractSerializerOperationFormatter,System.ServiceModel, Version=3.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089";
  35:     Type formatterType = Type.GetType(formatterTypeName);
  36:     ConstructorInfo formatterConstructor = formatterType.GetConstructor(new Type[] { typeof(OperationDescription), typeof(DataContractFormatAttribute), typeof(DataContractSerializerOperationBehavior) });
  37:     string operationInvokerTypeName = "System.ServiceModel.Dispatcher.SyncMethodInvoker,System.ServiceModel, Version=3.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089";
  38:     Type operationInvokerType = Type.GetType(operationInvokerTypeName); 
  39:  
  40:     foreach (MethodInfo method in typeof(T).GetMethods())
  41:     {
  42:         attributes = method.GetCustomAttributes(typeof(OperationContractAttribute), true);
  43:         if (attributes.Length > 0)
  44:         {
  45:             OperationContractAttribute operationAttribute = (OperationContractAttribute)attributes[0];
  46:             string operationName = string.IsNullOrEmpty(operationAttribute.Name) ? method.Name : operationAttribute.Name;
  47:             //通過OperationContractAttribute得到Action
  48:             string action;
  49:             if (string.IsNullOrEmpty(operationAttribute.Action))
  50:             {
  51:                 action = string.Format("{0}{1}/{2}", serviceNamepace, serviceName, operationName);
  52:             }
  53:             else
  54:             {
  55:                 action = operationAttribute.Action;
  56:             } 
  57:  
  58:             OperationDescription operation = operations.Where(op => op.Name == operationName).ToArray<OperationDescription>()[0];
  59:             //通過反射創建DataContractSerializerOperationFormatter對象
  60:             object formatter = formatterConstructor.Invoke(new object[] { operation, new DataContractFormatAttribute(), null });
  61:             clientFormatters.Add(operationName, formatter as IClientMessageFormatter);
  62:             dispatchFormatters.Add(action, formatter as IDispatchMessageFormatter); 
  63:  
  64:             //通過反射創建SyncMethodInvoker對象
  65:             IOperationInvoker operationInvoker = (IOperationInvoker)Activator.CreateInstance(operationInvokerType, method);
  66:             operationInvokers.Add(action, operationInvoker);
  67:             methods.Add(action, method);
  68:         }
  69: }

步驟二、創建服務契約和實現服務

接下來為本案例創建一個服務契約和實現該契約。服務契約定義在Contracts項目,具體的服務實現在模擬服務端的ASP.NET Web站點中。簡單起見,依然沿用計算服務的例子。

   1: [ServiceContract(Namespace = "https://www.artech.com/")]
   2:     public interface ICalculator
   3:     {
   4:         [OperationContract]
   5:         double Add(double x, double y);
   6:     }

 

   1: public class CalculatorService : ICalculator
   2: {
   3:     public double Add(double x, double y)
   4:     {
   5:         return x + y;
   6:     }
   7: }

步驟三、實現服務端對服務調用請求的處理

我們通過一個ASP.NET的Web Page來模擬WCF服務端對服務請求的處理,下麵的Calculator類型相關的代碼實際上就是Calculator.aspx的後台代碼(Code Behind)。整個處理流程不算複雜。在構造函數中,調用Utility的Create<ICalculator>方法,將所需的組件進行初始化,而具體的服務調用請求處理的邏輯在直接寫在Web Page的Load事件中。

首先,通過MessageCoderFactory創建MessageEncoder對接收到的以HttpRequest形式體現的服務調用請求進行解碼,並生成請求消息。通過請求消息得到當前服務操作的Action屬性後,在初始化過程中得到的基於服務契約所有MethodInfo列表中,根據該Action得到當前操作對應的MethodInfo對象。借助於MethodInfo對象得到操作方法的輸入參數和輸出參數數量後,創建兩個對象數組,分別用於保存通過DispatchMessageFormatter對象對於請求消息進行反序列化得到的輸入參數,和通過OperationInvoker執行操作方法得到的輸出參數。在OperationInvoker執行操作方法之前,通過反射的方式直接創建服務對象,這一步在真正的WCF框架中是通過InstanceProvider實現的。

通過OperationInvoker執行操作方法的結果有兩種形式:返回值和輸出參數(包括引用參數)。它們通過被傳入DispatchMessageFormatter被序列化並生成回複消息對象。回複消息通過MessageCoderFactory創建MessageEncoder進行編碼後通過HttpResponse返回。

   1:  
   2: ublic partial class Calculator : System.Web.UI.Page
   3:  
   4:    private static MessageVersion messageversion = MessageVersion.Default;
   5:    private static MessageEncoderFactory encoderFactory;
   6:    private static IDictionary<string, IDispatchMessageFormatter> dispatchFormatters;
   7:    private static IDictionary<string, IOperationInvoker> operationInvokers;
   8:    private static IDictionary<string, MethodInfo> methods;
   9:  
  10:    protected Calculator()
  11:    {
  12:        IDictionary<string, IClientMessageFormatter> clientFormatters;
  13:        Utility.Create<ICalculator>(out encoderFactory, out clientFormatters, out dispatchFormatters, out operationInvokers, out methods);
  14:    }
  15:  
  16:    protected void Page_Load(object sender, EventArgs e)
  17:    {
  18:        //對HttpPRequest進行解碼生成請求消息對象
  19:        Message request = encoderFactory.Encoder.ReadMessage(this.Request.InputStream, int.MaxValue, "application/soap+xml; charset=utf-8");
  20:  
  21:        //通過請求消息得到代表服務操作的Action
  22:        string action = request.Headers.Action;
  23:  
  24:        //通過Action從MethodInfo字典中獲取服務操作對應的MethodInfo對象
  25:        MethodInfo method = methods[action];
  26:  
  27:        //得到輸出參數的數量
  28:        int outArgsCount = 0;
  29:        foreach (var parameter in method.GetParameters())
  30:        {
  31:            if (parameter.IsOut)
  32:            {
  33:                outArgsCount++;
  34:            }
  35:        }
  36:  
  37:        //創建數組容器,用於保存請求消息反序列後生成的輸入參數對象
  38:        int inputArgsCount = method.GetParameters().Length - outArgsCount;
  39:        object[] parameters = new object[inputArgsCount];
  40:        dispatchFormatters[action].DeserializeRequest(request, parameters);
  41:  
  42:        List<object> inputArgs = new List<object>();
  43:        object[] outArgs = new object[outArgsCount];
  44:        //創建服務對象,在WCF中服務對象通過InstanceProvider創建
  45:        object serviceInstance = Activator.CreateInstance(typeof(CalculatorService));
  46:        //執行服務操作
  47:        object result = operationInvokers[action].Invoke(serviceInstance, parameters, out outArgs);
  48:        //將操作執行的結果(返回值或者輸出參數)序列化生成回複消息
  49:        Message reply = dispatchFormatters[action].SerializeReply(messageversion, outArgs, result);
  50:        this.Response.ClearContent();
  51:        this.Response.ContentEncoding = Encoding.UTF8;
  52:        this.Response.ContentType = "application/soap+xml; charset=utf-8";
  53:        //對回複消息進行編碼,並將編碼後的消息通過HttpResponse返回
  54:        encoderFactory.Encoder.WriteMessage(reply, this.Response.OutputStream);
  55:        this.Response.Flush();
  56:    }
  57:  

步驟四、實現客戶端對服務調用請求的處理

由於在客戶端對服務請求的處理是通過一個RealProxy(ServiceChannelFactory)實現的,為了真實模擬WCF處理框架,在這裏通過一個自定義RealProxy來實現客戶端相關的服務調用請求的處理。下麵代碼中定義的ServiceRealProxy<IContract>就是這樣一個自定義RealProxy。

用於處理服務調用請求的相關組件對象,比如MessageEncoderFactory和IClientMessageFormatter字典,以及所需的屬性,比如消息的版本和服務的目的地址,通過構造函數指定。而具體的請求處理實現在重寫的Invoke方法之中。首先通過解析應用在當前方法的上麵的OperationContractAttribute得到服務操作的名稱,以此為Key從IClientMessageFormatter字典中得到當前服務操作對應的IClientMessageFormatter對象。當前操作方法調用的輸入參數通過IClientMessageFormatter對象進行序列化後生成請求消息。為請求消息添加必要的尋址報頭後,通過MessageEncoderFactory創建的MessageEncoder對請求消息進行編碼。經過編碼的消息以HttpRequest的形式發送到服務端,從而完成了服務調用請求的發送。

服務調用的結果通過HttpResponse的形式返回後,先通過MessageEncoder對其解碼,並生成回複消息。回複消息通過IClientMessageFormatter進行反序列化後,在消息中以XML InfoSet實行體現的結果被轉化成具體的對象,這些對象被最終影射為方法調用的返回值和輸出參數(包含引用參數)。

   1: namespace Artech.WcfFrameworkSimulator.Client
   2: {
   3:     public class ServiceRealProxy<IContract> : RealProxy
   4:     {
   5:         private Uri _remoteAddress;
   6:         private IDictionary<string, IClientMessageFormatter> _messageFormatters;
   7:         private MessageVersion _messageVersion = MessageVersion.Default;
   8:         private MessageEncoderFactory _messageEncoderFactory;
   9:  
  10:         public ServiceRealProxy(MessageVersion messageVersion, Uri address, IDictionary<string, IClientMessageFormatter> messageFormaters, MessageEncoderFactory messageEncoderFactory)
  11:             : base(typeof(IContract))
  12:         {
  13:             object[] attribute = typeof(IContract).GetCustomAttributes(typeof(ServiceContractAttribute), false);
  14:             if (attribute.Length == 0)
  15:             {
  16:                 throw new InvalidOperationException(string.Format("The type \"{0}\" is not a ServiceContract!", typeof(IContract).AssemblyQualifiedName));
  17:             }
  18:             this._messageVersion = messageVersion;
  19:             this._remoteAddress = address;
  20:             this._messageFormatters = messageFormaters;
  21:             this._messageEncoderFactory = messageEncoderFactory;
  22:         }
  23:  
  24:         public override IMessage Invoke(IMessage msg)
  25:         {
  26:             IMethodCallMessage methodCall = (IMethodCallMessage)msg;
  27:  
  28:             //Get Operation name.
  29:             object[] attributes = methodCall.MethodBase.GetCustomAttributes(typeof(OperationContractAttribute), true);
  30:             if (attributes.Length == 0)
  31:             {
  32:                 throw new InvalidOperationException(string.Format("The method \"{0}\" is not a valid OperationContract.", methodCall.MethodName));
  33:             }
  34:             OperationContractAttribute attribute = (OperationContractAttribute)attributes[0];
  35:             string operationName = string.IsNullOrEmpty(attribute.Name) ? methodCall.MethodName : attribute.Name;
  36:  
  37:             //序列化請求消息
  38:             Message requestMessage = this._messageFormatters[operationName].SerializeRequest(this._messageVersion, methodCall.InArgs);
  39:  
  40:             //添加必要的WS-Address報頭
  41:             EndpointAddress address = new EndpointAddress(this._remoteAddress);
  42:             requestMessage.Headers.MessageId = new UniqueId(Guid.NewGuid());
  43:             requestMessage.Headers.ReplyTo = new EndpointAddress("https://www.w3.org/2005/08/addressing/anonymous");
  44:             address.ApplyTo(requestMessage);
  45:  
  46:             //對請求消息進行編碼,並將編碼生成的字節發送通過HttpWebRequest向服務端發送
  47:             HttpWebRequest webRequest = (HttpWebRequest)HttpWebRequest.Create(this._remoteAddress);
  48:             webRequest.Method = "Post";
  49:             webRequest.KeepAlive = true;
  50:             webRequest.ContentType = "application/soap+xml; charset=utf-8";
  51:             ArraySegment<byte> bytes = this._messageEncoderFactory.Encoder.WriteMessage(requestMessage, int.MaxValue, BufferManager.CreateBufferManager(long.MaxValue, int.MaxValue));
  52:             webRequest.ContentLength = bytes.Array.Length;
  53:             webRequest.GetRequestStream().Write(bytes.Array, 0, bytes.Array.Length);
  54:             webRequest.GetRequestStream().Close();
  55:             WebResponse webResponse = webRequest.GetResponse();
  56:  
  57:             //對HttpResponse進行解碼生成回複消息.
  58:             Message responseMessage = this._messageEncoderFactory.Encoder.ReadMessage(webResponse.GetResponseStream(), int.MaxValue);
  59:  
  60:             //回複消息進行反列化生成相應的對象,並映射為方法調用的返回值或者ref/out參數
  61:             object[] allArgs = (object[])Array.CreateInstance(typeof(object), methodCall.ArgCount);
  62:             Array.Copy(methodCall.Args, allArgs, methodCall.ArgCount);
  63:             object[] refOutParameters = new object[GetRefOutParameterCount(methodCall.MethodBase)];
  64:             object returnValue = this._messageFormatters[operationName].DeserializeReply(responseMessage, refOutParameters);
  65:             MapRefOutParameter(methodCall.MethodBase, allArgs, refOutParameters);
  66:  
  67:             //通過ReturnMessage的形式將返回值和ref/out參數返回
  68:             return new ReturnMessage(returnValue, allArgs, allArgs.Length, methodCall.LogicalCallContext, methodCall);
  69:         }
  70:  
  71:         private int GetRefOutParameterCount(MethodBase method)
  72:         {
  73:             int count = 0;
  74:             foreach (ParameterInfo parameter in method.GetParameters())
  75:             {
  76:                 if (parameter.IsOut || parameter.ParameterType.IsByRef)
  77:                 {
  78:                     count++;
  79:                 }
  80:             }
  81:             return count;
  82:         }
  83:  
  84:         private void MapRefOutParameter(MethodBase method, object[] allArgs, object[] refOutArgs)
  85:         {
  86:             List<int> refOutParamPositionsList = new List<int>();
  87:             foreach (ParameterInfo parameter in method.GetParameters())
  88:             {
  89:                 if (parameter.IsOut || parameter.ParameterType.IsByRef)
  90:                 {
  91:                     refOutParamPositionsList.Add(parameter.Position);
  92:                 }
  93:             }
  94:             int[] refOutParamPositionArray = refOutParamPositionsList.ToArray();
  95:             for (int i = 0; i < refOutArgs.Length; i++)
  96:             {
  97:                 allArgs[refOutParamPositionArray[i]] = refOutArgs[i];
  98:             }
  99:         }
 100:     }
 101: }

在真正的WCF客戶端框架下,客戶端通過ChannelFactory<T>創建服務代理對象進行服務的調用,在這裏我們也創建一個完成相似功能的工廠類型: SerivceProxyFactory<T>,泛型類型T代表服務契約類型。

用於創建服務代理的Create方法很簡單:先通過Utility.Create<T>方法創建客戶端進行服務調用必須的相關組件對象,通過這些對象連同該方法的參數(消息版本和服務目的地址)創建ServiceRealProxy<T>對象,最終返回的是該RealProxy的TransparentProxy。

   1: namespace Artech.WcfFrameworkSimulator.Client
   2: {
   3:     public static class SerivceProxyFactory<T>
   4:     {
   5:         public static T Create(MessageVersion messageVersion, Uri remoteAddress)
   6:         {
   7:             MessageEncoderFactory encoderFactory; 
   8:             IDictionary<string, IClientMessageFormatter> clientFormatters; 
   9:             IDictionary<string, IDispatchMessageFormatter> dispatchFormatters; 
  10:             IDictionary<string, IOperationInvoker> operationInvokers; 
  11:             IDictionary<string, MethodInfo> methods; 
  12:             Utility.Create<T>(out encoderFactory, out clientFormatters, out dispatchFormatters, out operationInvokers, out methods); 
  13:             ServiceRealProxy<T> realProxy = new ServiceRealProxy<T>(messageVersion, remoteAddress, clientFormatters, encoderFactory); 
  14:             return (T)realProxy.GetTransparentProxy();
  15:         }
  16:     }
  17: }

那麼在最終的客戶端代碼中就可以借助SerivceProxyFactory<T>創建服務代理進行服務調用了,而這裏服務的目標地址實際上是上麵用於模擬WCF服務端框架的.aspx Web Page的地址。

   1: namespace Artech.WcfFrameworkSimulator.Client
   2: {
   3:     class Program
   4:     {
   5:         static void Main(string[] args)
   6:         {
   7:             ICalculator calculator = SerivceProxyFactory<ICalculator>.Create(MessageVersion.Default, new Uri("https://localhost/Artech.WcfFrameworkSimulator/Calculator.aspx")); 
   8:             double result = calculator.Add(1, 2); 
   9:             Console.WriteLine("x + y = {2} when x = {0} and y = {1}", 1, 2, result);
  10:         }
  11:     }
  12: }

執行結果:

   1: x + y = 3 when x = 1 and y = 2

作者:蔣金楠
微信公眾賬號:大內老A
微博:www.weibo.com/artech
如果你想及時得到個人撰寫文章以及著作的消息推送,或者想看看個人推薦的技術資料,可以掃描左邊二維碼(或者長按識別二維碼)關注個人公眾號(原來公眾帳號蔣金楠的自媒體將會停用)。
本文版權歸作者和博客園共有,歡迎轉載,但未經作者同意必須保留此段聲明,且在文章頁麵明顯位置給出原文連接,否則保留追究法律責任的權利。
原文鏈接

最後更新:2017-10-30 14:34:43

  上一篇:go  如何解決打開VS2010後沒有UI界麵的問題
  下一篇:go  WCF技術剖析之二:再談IIS與ASP.NET管道