在C#语言中,ref与out都是引用传递关键字,旨在将一个对象传递进一个方法后,返回此方法“加工”后的对象,还可用于实际需求需要函数返回多个返回值。那么有没有什么情况下,只能用其一?有的。一般性的面向过程开发的代码编写,两者我认为是可以换用没问题,但在面向对象中,有时只能用其一。下面来看看此情况: 假设我们现在需要一个通信层发送命令,我们将所有命令都缓存到一个队列里面,交由专门的线程发送。首先定义一个基本命令实体,包含公共属性如版本号、会话ID、字节长度等
public class CmdModel { //版本 public byte Version { get; set; } //会话ID public uint SessionId { get; set; } //命令类型 public byte CmdType { get; set; } //整条命令byte数组长度 public int CmdLen { get; set; } //整条命令字节数据 public string CmdData { get; set; } //将所有的子类型的公共属性值,赋值给为CmdModel父类型的外部变量,因为发送命令只需要CmdModel父类型包含的字段就可以了 public virtual void ConvertToCmd(ref CmdModel pCmd) { pCmd.Version = Version; pCmd.SessionId = SessionId; pCmd.CmdType = CmdType; pCmd.CmdLen = CmdLen; pCmd.CmdData = CmdData; } }
由以上ConvertToCmdData方法加了ref我们也知道,外部调用需要的还是我们传递的参数pCmd。接着我们就定义上面提到的发送命令泛型方法:
public void Send_CmdList<T>(IEnumerable<T> pCmdList) where T : CmdModel { try { foreach (var cmd in pCmdList) //逐个将具体的子命令插入到发送队列 { CmdModel Cmd = new CmdModel(); cmd.SessionId = GetSessionID(); //赋值SessionID cmd.ConvertToCmd(ref Cmd); mSendQueue.Enqueue(Cmd); } } catch (Exception ex) { //错误处理 } }
具体子类的发送命令,继承自基本命令实体CmdModel:
/// <summary> /// 变化声道 /// </summary> public class ChangeVoid : CmdModel { //机器编号(4字节) public ulong MachineID { get; set; } //音量等级(4字节) public int VoiceRank { get; set; } //播放类型(4字节) public int VoiceTye { get; set; } //转换为CASCmd public override void ConvertToCmd(ref CmdModel pCmd) { try { StringBuilder BodyData = new StringBuilder(); BodyData.Append(MachineID.ToString("X8")); BodyData.Append(VoiceRank.ToString("X8")); BodyData.Append(VoiceTye.ToString("X8")); pCmd.CmdLen = (BodyData.Length / 2);//字符串长度除2就是数组长度 pCmd.CmdData = BodyData.ToString(); base.ConvertToCmd(ref pCmd); } catch (Exception ex) { return; } } }好了,简单的通信机制层大概好了。下面讲重点,如果将Send_CmdList方法内的ConvertToCmd(ref pCmd)调用,ref改为out。由于带有out参数的函数,必须在函数内部赋值(ref的话可不赋值),那么子类和基类的同名函数ConvertToCmd也要修改,以便满足pCmd在函数内部必须赋值才能传递出去,所以多了下面一行标为红色的代码,出现以下形式:
public virtual void ConvertToCmd(out CmdModel pCmd) { pCmd = new CmdModel(); //基类添加了此行 pCmd.Version = Version; pCmd.SessionId = SessionId; pCmd.CmdType = CmdType; pCmd.CmdLen = CmdLen; pCmd.CmdData = CmdData; } public override void ConvertToCmd(out CmdModel pCmd) { try { pCmd = new CmdModel(); //子类添加了此行 StringBuilder BodyData = new StringBuilder(); BodyData.Append(MachineID.ToString("X8")); BodyData.Append(VoiceRank.ToString("X8")); BodyData.Append(VoiceTye.ToString("X8")); pCmd.CmdLen = (BodyData.Length / 2);//字符串长度除2就是数组长度 pCmd.CmdData = BodyData.ToString(); base.ConvertToCmd(out pCmd); //这里也要改 } catch (Exception ex) { return; } }发现问题了吧,就出在这里,子类调用父类的同名方法,里面又重新构造了一个pCmd,这个时候pCmd已经重新赋值为一个新对象了,具体子类中ConvertToCmd方法的“加工”操作,实际上是没有成功赋值到外部要调用的pCmd对象。 当然,这里我们把base.ConvertToCmd(out pCmd)方法的实现,交给具体子类来实现,是不会有此问题的,只构造一次始终是那一个对象。只是这样就会出现大量重复代码,也不符合面向对象设计思想。另外将ConvertToCmd的所有实现交给基类也不会有问题,即子类中该函数只有一行代码:
public override void ConvertToCmd(out CmdModel pCmd) { base.ConvertToCmd(out pCmd); }
这相当于子类通过base.ConvertToCmd(out pCmd)方法再进行了一次传递而已,只有在这种情况下,不需要在函数实现中为out指代的参数赋值。其实这种直接进行方法传递的情况,意思上可以理解为还是只传递了一次,因为ChangeVoid.ConvertToCmd(out CmdModel pCmd)就是base.ConvertToCmd(out pCmd);
究其原因,还是out指代的参数必须在函数内部实现中赋值导致,而ref只起一个加工传递作用,在函数内部可以赋值也可以不赋值,最后加工传递完成回来的还是那个对象,而out指代的参数,在函数内部必须赋值,除非直接进行方法传递。这里由于是对象就必须重新构造赋值,经历两层以上加工传递(构造),出来后已不是之前那个对象了。
或者换一个经典说法:ref是有进有出,而out是只出不进!