在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是只出不进!