5

WCF技术剖析之一:通过一个ASP.NET程序模拟WCF基础架构

 2 years ago
source link: https://www.cnblogs.com/artech/archive/2009/06/18/1506163.html
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

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服务端框架对于处理一个简单的WCF服务调用请求所提供的功能,以及相应的功能承载的组件:

  • 请求消息的接收和回复消息的发送:服务端在传输层监听与接收来自客户的请求,并将经过编码后的回复消息通过传输层发送到客户端
  • 请求消息的解码和回复消息的编码:将接收到的字节数组通过解码生成请求消息对象,并将回复消息通过编程转化成字节组。消息的编码和解码通过MessageEncoder完成,而MessageEncoderFactory负责创建该对象
  • 请求消息的反序列化和回复消息的序列化:对请求消息进行反序列化,为服务操作的执行生成相应的输入参数,以及将服务操作执行的结果(返回值或者ref/out参数)序列化,并生成回复消息。序列化和反序列化通过DispatchMessageFormatter完成
  • 服务对象的创建:创建或者激活服务对象实例,InstanceProvider用于服务对象的创建或获取
  • 服务操作的执行:调用创建的服务对象的操作方法,并传入经过反序列化生成的输入参数。OperationInvoker完成对服务操作的最终执行

较之服务端的流程,客户端的流程显得相对简单,仅仅包含以下三个必需的阶段:

  • 请求消息的序列化和回复消息的反序列化:生成请求消息并将输入参数序列化到请求消息中,以及对回复消息进行反序列化,转化成方法调用的返回值或者ref/out参数。序列化和反序列化通过ClienthMessageFormatter完成
  • 请求消息的编码和回复消息的解码:对请求消息进行编码生成字节数组供传输层发送,以及将传输层接收到的字节数组解码生成恢复消息。消息的编码和解码通过MessageEncoder完成,而MessageEncoderFactory负责创建该对象
  • 请求消息的发送和回复消息的接收:在传输层将经过编码的请求消息发送到服务端,以及将接收来自服务端的恢复消息

图1精简版WCF客户端与服务端组件

图1反映了进行服务调用的必要步骤和使用到的相关WCF组件。在本案例演示中,我们需要做的就是手工创建这些组件,并通过我们自己的代码利用它们搭建一个简易版的WCF框架。如果读者能够对本案例的实现有一个清晰的理解,相信对于整个WCF的框架就不会感到陌生了。

图2显示了本案例解决方案的基本结构,总共分三个项目。Contracts用于定义服务契约,被服务端和客户端引用。客户端通过一个Console应用模拟,而服务端则通过一个ASP.NET Website实现。

图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 = "http://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 = "http://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("http://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("http://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
如果你想及时得到个人撰写文章以及著作的消息推送,或者想看看个人推荐的技术资料,可以扫描左边二维码(或者长按识别二维码)关注个人公众号)。
本文版权归作者和博客园共有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文连接,否则保留追究法律责任的权利。

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK