工作中的趣事:聊聊ref/out和方法参数的传递机制
0x00 前言
我在之前的游戏公司工作的时候,常常是作为一只埋头实现业务逻辑的码农。在工作之中不常有同事会对关于编程的话题进行交流,而工作之余也没有专门的时间进行技术分享。所以对我而言上家虽然是一家游戏公司,但是工作却鲜有乐趣可言。不过还好,现在来到了一家同样做游戏的公司,但是有技术交流也有技术分享,虽然还不是那么成熟,但却能够让人感到工作的乐趣。这不,上周和同事聊到了C#语言的ref/out关键字在处理多态时的问题,仔细想想这个话题,又能引申到另一个更好玩的题目,C#语言的方法参数的传递机制。那么,本文就来聊聊这个事情吧。
0x01 复习
在开始正式的话题之前,我们先来复习一下C#的类型基础吧。C#语言的类型大体上可以分为两种,其一是值类型,另一种是引用类型。很多人都知道,值类型的值是它本身,而引用类型的值是一个引用,但是很多朋友对这句话又不完全的理解。下面,我们就通过几个小例子来明确一下相关的概念吧。
值类型(value type)
常见的值类型包括一些简单的类型例如int,float,long以及枚举类型和使用struct声明的类型。而很多接触C#语言不久的人经常会误认为string类型也是值类型,事实上string是一种引用类型。只不过它的实例一旦创建,就无法再次更改了。因此它也被称为不可变类型。也正是因为这一点,很多人常常会把它的行为和值类型的行为混淆。
值类型变量的值就是其本身,值类型变量的赋值事实上是一次值的复制过程。我们可以通过一个小例子来看一下这个过程。
public struct ValueTypeTest
{
public int intValue;
}
static void Main(string[] args)
{
ValueTypeTest valueOne = new ValueTypeTest();
valueOne.intValue = 1;
ValueTypeTest valueTwo = valueOne;
valueOne.intValue = 2;
Console.WriteLine(valueTwo.intValue);
}
输出结果我想大家一定都清楚,结果是1。
这是因为valueOne 在向valueTwo赋值时valueOne的值是1,valueOne将1复制给valueTwo之后两者便再无联系了。
引用类型(reference type)
诸如类(class)、委托(delegate)、接口(interface)、数组等等是一些常见的引用类型。引用类型的值是对某个对象的引用,而非对象本身。这一点,我想很多人都了解。
例如下面这句代码:
Object obj = new Object();
“Object obj”将作为一个引用类型变量出现,而“new Object”则会在堆上创建一个Object对象,obj的值是对Object对象的一个引用,而非Object对象本身。
我们还通过上文中的小例子来看看这个过程,唯一的不同是这次我们使用class取代struct来定义我们的类型。
public class RefTypeTest
{
public int intValue;
}
static void Main(string[] args)
{
RefTypeTest refOne = new RefTypeTest();
refOne.intValue = 1;
RefTypeTest refTwo = refOne;
refOne.intValue = 2;
Console.WriteLine(refTwo.intValue);
Console.ReadLine();
}
在这里我们声明了一个名为refOne的变量,并创建了一个RefTypeTest类的对象,之后将这个RefTypeTest类的对象的引用赋值给变量refOne。接着,我们将refOne的值赋值给新声明的refTwo变量。这样,refOne和refTwo都指向了同一个RefTypeTest类的对象。如果我们通过refOne修改了被引用的对象的内容,则通过refTwo再次去访问同一个对象,那么看到的自然也是修改后的内容了。所以此次的输出是2。
但是,我们要注意的(也是很多人忽略的)是,refOne和refTwo虽然引用了同一个对象,但是它们自身是独立的、无关的。
修改refOne的值,不会影响refTwo。例如,我们可以将refOne的值变成对一个null对象的引用,但是refTwo不会因此也指向null对象。
RefTypeTest refOne = new RefTypeTest();
refOne.intValue = 1;
RefTypeTest refTwo = refOne;
refOne.intValue = 2;
refOne = new RefTypeTest();
refOne.intValue = 3;
Console.WriteLine(refTwo.intValue);
Console.ReadLine();
0x02 方法参数
在C#语言中,方法的参数传递默认是按值传递的。当然,使用一些关键字可以改变这种参数传递行为,例如使用ref/out关键字可以使方法参数按引用传递。
但是一提到值、引用这样的字眼,很多人都会立马想到值类型和引用类型。而这也是一个常见的误区:把方法参数传递的概念和类型的概念搞混。
这两种概念是不同的,默认情况下无论是值类型还是引用类型参数都是按照值来传递的,而使用了ref/out参数时,值类型也可以按照引用来传递。所以值类型既可以按值传递,也可以按引用传递(而且不存在装箱的问题);引用类型既可以按值传递,也可以按引用传递。
这一部分就来聊聊方法参数的传递机制吧。
值类型按值传递
方法的参数传递默认是按值传递的,值类型变量按值传递给方法简单的说就是传递一份值类型变量的拷贝给方法。在声明方法时,会默认为参数分配一块新的内存空间用来保存参数的拷贝。
因此,方法内对参数的修改不会影响最初的值。
下面这个小例子可以演示这一点:
static void SquareIt(int x)
{
x *= x;
Console.WriteLine("inside: {0}", x);
}
static void Main()
{
int n = 5;
Console.WriteLine("before: {0}", n);
SquareIt(n);
Console.WriteLine("after: {0}", n);
}
输出的值依次是:before: 5、inside: 25、after:5。
简单来分析一下这段代码,变量n是一个值为5的值类型变量。当调用SquareIt 方法时,n的值便会被拷贝给参数x。而在Main函数中,n的值在调用SquareIt 方法前后是不变的。而在SquareIt 方法中求平方的操作仅仅影响了x。
引用类型按值传递
同样,默认情况下引用类型的参数也是按值传递的。所以,和值类型变量按值传递类似,引用类型变量按值传递同样是将变量的值拷贝给方法。
回忆一下前文的内容,引用类型变量的值是什么呢?对,是对一个对象的引用。所以我们可以在方法内修改引用类型参数所引用的对象,但是在方法内对参数的修改同样不会影响最初的值。即我们无法在方法内部改变原来的引用类型变量对对象的引用。
下面这个小例子可以演示这一点:
static void Change(int[] pArray)
{
pArray[0] = 888;
pArray = new int[5] { -3, -1, -2, -3, -4 };
Console.WriteLine("inside: {0}", pArray[0]);
}
static void Main()
{
int[] arr = { 1, 4, 5 };
Console.WriteLine("before: {0}", arr[0]);
Change(arr);
Console.WriteLine("after: {0}", arr[0]);
Console.ReadKey();
}
输出的结果依次是:before:1、inside:-3、after:888。
简单来分析一下这段代码,arr是一个引用类型变量,它引用了一个Array的对象。在这里,arr的值被拷贝给参数pArray,因此pArray也引用了同一个Array对象。此时在方法内的修改都是对同一个Array对象的修改。但是之后,使用new关键字又创建了一个新的Array对象,同时pArray的值变成了对新对象的引用。因此,之后的操作就变成了对新对象的操作。方法外的arr变量的值(对老Array对象的引用)并不会被修改。
值类型按引用传递
按引用传递参数,简单的说并非传递变量的值,而是传递对变量的引用,同时方法内操作的也不是变量的值,而是通过引用直接操作变量本身。因此方法内部不会再为参数分配一块内存空间,相反,方法会直接操作变量所在的那块内存空间。
我们还可以通过上面的那个例子来看看值类型变量按引用传递给方法。
static void SquareIt(ref int x)
{
x *= x;
Console.WriteLine("inside: {0}", x);
}
static void Main()
{
int n = 5;
Console.WriteLine("before: {0}", n);
SquareIt(ref n);
Console.WriteLine("after: {0}", n);
Console.ReadKey();
}
在这个例子中,n的值没有被传递给方法。相反,这次传递是对变量n的引用。因此参数x并非是int,而是一个对int变量的引用,在这个例子中x是对变量n的引用。
于是这次在方法内对x求平方的结果就是对n求平方,输出的结果也相应的变成了:before:5、inside:25、after:25。
引用类型按引用传递
最后,我们来看看引用类型变量按引用传递的例子。类似的,我们仍然使用“引用类型按值传递”部分用到的小例子,只不过这次我们使用ref关键字来改变参数的传递方式。
static void Change(ref int[] pArray)
{
pArray[0] = 888;
pArray = new int[5] { -3, -1, -2, -3, -4 };
Console.WriteLine("inside: {0}", pArray[0]);
}
static void Main()
{
int[] arr = { 1, 4, 5 };
Console.WriteLine("before: {0}", arr[0]);
Change(ref arr);
Console.WriteLine("after: {0}", arr[0]);
Console.ReadKey();
}
在这个例子中,arr的引用被传递给了方法。因此pArray是对arr的引用,修改pArray事实上就是对arr的修改。
因此,这次的输出结果也变成了:before:1、inside:-3、after:-3。
0x03 ref/out不支持多态
好了,聊完了类型和方法参数传递的话题最后终于要来聊聊我们在工作讨论的那个小问题了。使用了ref/out关键字的方法是否支持多态呢?如果不支持的话又是为什么呢?
对于ref/out是否支持多态的问题,答案很简单。
static void Foo(ref A a)
{
}
public class A { };
public class B : A { };
static void Main()
{
B b = new B();
Foo(ref b);
}
ref/out不支持多态。
但是为什么呢?
假设我们有两个类型T和U,则T和U的关系大概可以分为以下几种:
- T比U的派生程度更小
- T比U的派生程度更大
- T和U相同
- T和U无关
具体来说,我们可以通过下图来作为例子:
从图中我们可以看到:B类的派生程度比D类更小,同时比A类更大,但是又和C类、F类无关。
我们都知道变量对应着一个存储位置。而在C#语言中所有的存储位置都有与它们相关联的类型,在运行时我们只能将一个和该类型相同或比它的派生程度更大的实例分配在该存储位置。也就是说B类型的存储位置可以保存一个D类型的实例,但是C类型的实例不能保存在这个存储位置。
好了,再让我们把关注点转移到ref/out和方法的参数传递上来。
通过上文我们已经知道了使用ref/out的方法参数是按照引用来传递的,方法的参数指向了方法外部的变量所在的内存位置。假设我们有一个方法“void Foo(ref B b)”,那么我们可以向该方法传递一个A类型的变量吗?
答案是不能。因为A变量的存储位置保存的可能是一个F类型的实例,但是F类型和B类型没关系啊!这种情况就是所谓的类型不安全。
那么我们可以传递一个D类型的变量吗?感觉应该是可以的吧,因为D比B的派生程度更大啊。但是答案同样是不能!
的确,如果只看方法签名的话类型D的派生程度的确要大于类型B。但是在Foo方法内,我们可以修改类型D的变量啊!如果Foo方法的功能是下面这样呢?
static void Foo(ref B b)
{
b = new E();
}
要知道,类型D和类型E是无关的。因此这样也会产生类型安全的问题。
所以为了解决这种类型安全问题所导致的隐患,在编译时就会报出类似下面这样的异常。
一切都是为了类型安全啊。
工作中的趣事:聊聊ref/out和方法参数的传递机制的更多相关文章
- J2EE开发工作中遇到的异常问题及解决方法总结
参考博文:http://blog.csdn.net/rchm8519/article/details/41624381
- Activity中使用Intent实现页面跳转与参数的传递(转)
新建一个FirstAvtivity.java package com.zhuguangwei; import android.app.Activity; import android.content. ...
- C#中一个关于不同窗体间的颜色参数的传递
1目标是 在弹出菜单中选择颜色,在主菜单中对控件进行操作(弹出菜单选择的颜色就是主菜单控件的颜色) 2颜色属性需要来回转换(也许不用转换,暂时还不会,有会的提醒下,TKS) 3用到一个颜色控件(col ...
- [工作中的设计模式]原型模式prototype
一.模式解析 提起prototype,最近看多了js相关的内容,第一印象首先是js的原型 var Person=function(name){ this.name=name; } Person.pro ...
- js--前端开发工作中常见的时间处理问题
前言 在前端开发工作中,服务端返回的时间数据或者你传递给服务端的时间参数经常会遇到时间格式转换及处理问题.这里分享一些我收集到的一些处理方法,方便日后工作中快速找到.先附上必须了解的知识内置对象传送门 ...
- c# 方法参数(传值,传引用,ref,out,params,可选参数,命名参数)
一.方法参数的类型----值类型和引用类型 当方法传递的参数是值类型时,变量的栈数据会完整地复制到目标参数中即实参和形参中的数据相同但存放在内存的不同位置.所以,在目标方法中对形参所做的更改不会 ...
- IOS OS X 中集中消息的传递机制
1 KVO (key-value Observing) 是提供对象属性被改变是的通知机制.KVO的实现实在Foundation中,很多基于 Foundation 的框架都依赖与它.如果只对某一个对象的 ...
- 聊聊JavaScript在工作中常用的方法(一)
一.字符串转数组(split方法) 废话少说,直接上代码: //例子1 var str="abc,def,ghi"; var strArray=str.split(",& ...
- 工作中那些提高你效率的神器(第二篇)_Listary
引言 无论是工作还是科研,我们都希望工作既快又好,然而大多数时候却迷失在繁杂的重复劳动中,久久无法摆脱繁杂的事情. 你是不是曾有这样一种想法:如果我有哆啦A梦的口袋,只要拿出神奇道具就可解当下棘手的问 ...
随机推荐
- 正则表达式中的match,test,exec,search的返回值
今天突然被问到了正则表达式,因为长时间不用突然不知道怎么用了,只知道有这么个东西.然后去网上查了一下,感觉写的不少,但解释的有点模糊,今天我来浅谈一下. 1,match的用法 A,在不加全局“g”的情 ...
- R语言爬虫 rvest包 html_text()-html_nodes() 原理说明
library(rvest) 例子网页:http://search.51job.com/jobsearch/search_result.php?fromJs=1&jobarea=010000% ...
- iframe的优缺点?
1.`<iframe>`优点: 解决加载缓慢的第三方内容如图标和广告等的加载问题 Security sandbox 并行加载脚本 2.`<iframe>`的缺点: *ifram ...
- SpringBoot实践 - SpringBoot+mysql
关于springBoot是个神马东西以及优缺点,请自行搜索了解. LZ看到很多关于SpringBoot的Demo,单看一篇总是没法整合SpringBoot与Mysql.没法子,还是自己操刀来一发为妙. ...
- IOS设备对position的支持性
最近在开发一个网页. 要嵌套在微信里 大家都知道 IOS版微信和安卓版微信还是一定的差距 IOS版微信在打开网页的时候回调取自己的浏览器以及内核 但是安卓版微信不会,他会默认使用自己的QQ浏览器和X5 ...
- DbUtils类基本使用
一.commons-dbutils简介 commons-dbutils 是 Apache 组织提供的一个开源 JDBC工具类库,它是对JDBC的简单封装,学习成本极低,并且使用dbutils能极大简化 ...
- spring源码浅析——IOC
=========================================== 原文链接: spring源码浅析--IOC 转载请注明出处! ======================= ...
- java 完全二叉树的构建与四种遍历方法
本来就是基础知识,不能丢的太干净,今天竟然花了那么长的时间才写出来,记一下. 有如下的一颗完全二叉树: 先序遍历结果应该为:1 2 4 5 3 6 7 中序遍历结果应该为:4 2 5 ...
- SESSION 与 COOKIE的区别是
有以下几点 1.session是存在服务器端,cookie是存在客户端 2.cookie不是很安全,别人可以分析存放在本地的COOKIE并进行COOKIE欺骗,所以session安全性要比cookie ...
- jQuery后续...
jQuery 选择器 1.jQuery选择器的简介 (1). Jquery中的选择器完全继承了CSS的风格,利用Jquery选择器,可以非常便捷和快速的找出特定的Dom元素,然后为他们添加相应的行为, ...