首页 > Windows开发 > 详细

C# in Depth学习笔记-值类型和引用类型

时间:2018-11-26 21:47:06      阅读:217      评论:0      收藏:0      [点我收藏+]

2.3 值类型和引用类型

本节简要讨论了为了深入更高版本C#的世界,C# 1的哪些主题的基本元素是必须理解的。

先来看看在现实世界和在.NET中,值类型和引用类型的基本差异是如何自然体现的。


2.3.1 现实世界中的值和引用

假定你在读一份非常棒的东西,希望一个朋友也去读它。你需要为朋友提供什么才能让他读到文档,这完全取决于阅读的内容。

报纸与值类型

先假设你正在读的是一份真正的报纸。为了给朋友一份,需要影印报纸的全部内容并交给他。届时,他将获得属于他自己的一份完整的报纸。

在这种情况下,我们处理的是值类型的行为。所有信息都在你的手上,不需要从任何其他地方获得。制作了副本之后,你的这份信息和朋友的那份是各自独立的。可以在自己的报纸上添加一些注解,他的报纸根本不会改变。

网址与值类型

再假设你正在读的是一个网页。你需要给朋友的就是网页的URL。这是引用类型的行为,URL代替引用。为了真正读到文档,必须在浏览器中输入URL,并要求它加载网页来导航引用。

另一方面,假如网页由于某种原因发生了变化,你和你的朋友下次载入页面时,都会看到那个改变。

在C#和.NET中,值类型和引用类型的差异与现实世界中的差别类似。

谁是值类型或引用类型

.NET中的大多数类型都是引用类型,除了以下总结的特殊情况,类是引用类型,而结构是值类型。特殊情况包括如下方面:

①数组类型是引用类型,即使元素类型是值类型(所以即便 int 是值类型, int[] 仍是引用类型);

②枚举(使用 enum 来声明)是值类型;

③委托类型(使用 delegate 来声明)是引用类型;

④接口类型(使用 interface 来声明)是引用类型,但可由值类型实现。

理解了引用类型和值类型的基本概念之后,接着要探讨几个最重要的细节。


2.3.2 值类型和引用类型基础知识

学习值类型和引用类型时,要掌握的重要概念是一个特殊表达式的值是什么。为使问题更加具体,我们使用了表达式最常见的例子——变量。但同样的道理也适用于属性、方法调用、索引器(indexer)和其他表达式。


表达式的值

值类型:表达式的值就是表达式的值

前面节说过,大多数表达式都有与其相关的静态类型。对于值类型的表达式,它的值就是表达式的值,这很容易理解。例如,表达式“2+3”的值就是5。

引用类型:表达式的值是一个引用

对于引用类型的表达式,它的值是一个引用,而不是该引用所指代的对象。所以表达式 String.Empty 的值不是一个空字符串——而是对空字符串的一个引用。


二者在内存中的区别

存储了两个整数 x 和 y 的一个 Point 类型,它的一个构造函数能获得两个值。

这个类型可以实现为结构或类。图2-3展示了执行下面两行代码的结果:

Point p1 = new Point(10, 20);
Point p2 = p1;

技术分享图片

图2-3左边的部分指出当 Point 是一个类(引用类型)时所涉及的值,右边的部分展示了当Point 是一个结构(值类型)时的情形。

在两种情况下, p1 和 p2 在赋值后都有相同的值:

①在 Point 是引用类型的情况下,那个值是引用: p1 和 p2 都引用同一个对象。

②在 Point 是值类型的情况下, p1 的值是一个“点”的完整的数据,也就是这个“点”的 x 和 y 值。将 p1 的值赋给 p2 ,会复制 p1 的所有数据。

变量的值在它声明时的位置存储

局部变量的值总是存储在栈(stack)中,实例变量的值总是存储在实例本身存储的地方。引用类型实例(对象)总是存储在堆(heap)中,静态变量也是。

我的理解是:值类型的实例在栈上,那么值类型的实例变量就在栈上,引用类型的实例在堆里,引用类型的实例变量也就在堆里,静态变量和实例变量是一个道理。程序开始运行时,会用到的局部变量都会保存在栈上,至于这个变量是值类型实例还是引用类型实例——无所谓,他都在栈上。没有在代码想中声明实例,实例字段就不会创建,实例字段和局部变量在概念上的差别一定要理解、分清。

值类型不可以派生出其他类型

这将导致的一个结果就是,值不需要额外的信息来描述值实际是什么类型。

把它同引用类型比较,对于引用类型来说,每个对象的开头都包含一个数据块,它标识了对象的实际类型,同时还提供了其他一些信息。

永远都不能改变对象的类型,强制转换改变的是引用的类型

执行简单的强制类型转换时,运行时会获取一个引用,检查它引用的对象是不是目标类型的一个有效对象。如果有效,就返回原始引用,否则抛出异常。

引用本身并不知道对象的类型——所以同一个引用“值”可用于(引用)不同类型的多个变量。例如下面的代码:

Stream stream = new MemoryStream();
MemoryStream memoryStream = (MemoryStream) stream;

第1行创建一个新的 MemoryStream 对象,并将 stream 变量的值设为对那个新对象的引用。

第2行检查 stream 的值引用的是不是一个 MemoryStream (或派生类型)对象,并将MemoryStream 的值设为相同的值。


2.3.3 走出误区

本节将处理一些典型错误,解释真实的情况到底是什么。


误区1:“结构是轻量级的类”

使用值类型不取决于类型是否简单

有人认为值类型不能或不应有方法或其他有意义的行为,它们应作为简单的数据转移类型来使用,只应该有 public 字段或简单的属性。

一个非常典型的反例就是 DateTime 类型:它作为值类型来提供是很有道理的,因为它非常适合作为和数字或字符相似的一个基本单位来使用。另外,它也理应被赋予对它的值执行计算的能力。

总之,具体应该如何决定,应取决于需要的是值类型的语义,还是引用类型的语义,而不是取决于这个类型简单与否。

使用值类型不一定更节省性能

还有一些人认为值类型节省性能,因为它们不需要垃圾回收,除非被装箱,不会因类型标识而产生开销,也不需要解引用。

但在其他方面,引用类型显得更轻。在传递参数、赋值、将值返回和执行类似的操作时,只需复制4或8字节,而不是复制全部数据。

性能问题都不是根据这种判断来决定的。瓶颈从来都不是想当然的,根据性能进行设计之前,需要衡量不同的选择。


误区2:“引用类型保存在堆上,值类型保存在栈上”

第一部分是正确的——引用类型的实例总是在堆上创建的。

但第二部分就有问题了。前面讲过,变量的值是在它声明的位置存储的。

所以,假定一个类中有一个 int 类型的实例变量,那么在这个类的任何对象中,该变量的值总是和对象中的其他数据在一起,也就是在堆上。只有局部变量(方法内部声明的变量)和方法参数在栈上。

对于C# 2及更高版本,很多局部变量并不完全存放在栈中,第5章中的匿名方法会讲到这一点。

 这些概念符合现实吗?

一个较有争议的说法是:写托管代码时,应该让运行时去操心内存的最佳使用方式。事实上,在语言规范中,并没有对什么东西应该存储在什么地方做出硬性规定。在未来的某个版本的运行时中,也许会允许在栈上创建一些对象(前提是运行时知道这样可行)。又或者,C#编译器能生成几乎完全用不到栈的代码。

 下一个误区是由于对术语的理解出现了偏差。


误区3:“对象在C#中默认是通过引用传递的”

CLR默认所有方法参数都传值。

值传递值类型

拷贝值类型的值给栈上的局部变量,修改局部变量不影响原值。

值传递引用类型

拷贝引用类型的值(引用)给栈上的局部变量,在方法里修改局部变量指向的堆中的对象,原值(引用)和其指向的是同一个对象,所以都会改变。

但修改方法里的局部变量为null,只是使该引用指向null,不会对对象造成影响。

引用传递值类型

创建在栈上一个局部变量(引用),其指向栈上的原值的地址,在方法里修改局部变量和值传递引用类型是一样的。

引用传递引用类型

仅当方法返回对方法知道的一个对象的引用时,为引用类型使用out和ref才有意义。

CLR via C#对此做了更好的解释,详情访问我在学习CLR via C#是关于值传递和引用传递的笔记博客

https://www.cnblogs.com/errornull/p/9822570.html

许多人对于装箱和拆箱的理解也存在一定的误区,下一节将对此加以澄清。


 2.3.4 装箱和拆箱

C#和.NET提供了一个名为装箱(boxing)的机制,它允许根据值类型创建一个对象,然后使用对这个新对象的一个引用。

在接触实际的例子之前,先来回顾两个重要的事实:

①对于引用类型的变量,它的值永远是一个引用;

②对于值类型的变量,它的值永远是该值类型的一个值。

基于这两个事实,下面3行代码第一眼看上去似乎没有太多道理:

int i = 5;
object o = i;
int j =(int) o;

装箱和拆箱的过程

这里有两个变量: i 是值类型的变量, o 是引用类型的变量。

o 的值必须是一个引用,而数字5不是引用,它是一个整数值。实际发生的事情就是装箱。

运行时将在堆上创建一个包含值(5)的对象(它是一个普通对象)。o 的值是对该新对象的一个引用。该对象的值是原始值的一个副本,改变 i 的值不会改变箱内的值。

第3行执行相反的操作——拆箱。

必须告诉编译器将 object 拆箱成什么类型。如果使用了错误的类型(比如 o 原先被装箱成 unit 或者 long ,或者根本就不是一个已装箱的值),就会抛出一个 InvalidCastException 异常。

同样,拆箱也会复制箱内的值,在赋值之后, j 和该对象之间不再有任何关系。

另外,将值作为接口表达式使用时——把它赋给一个接口类型的变量,或者把它作为接口类型的参数来传递——也会发生装箱。例如, IComparable x = 5; 语句会对数字5进行装箱。

装箱和拆箱影响性能

之所以要留意装箱和拆箱,是由于它们可能会降低性能。一次装箱或拆箱操作的开销是微不足道的,但假如执行千百次这样的操作,那么不仅会增大程序本身的操作开销,还会创建数量众多的对象,而这些对象会加重垃圾回收器的负担。

CLR via C#对此做了更多的解释,详情访问我在学习CLR via C#是关于值类型的装箱和拆箱的笔记博客

https://www.cnblogs.com/errornull/p/9744474.html


2.3.5 值类型和引用类型小结

本节讨论了值类型和引用类型的差异,还澄清了围绕它们存在的一些误区。下面是一些要点。

①对于引用类型的表达式(如一个变量),它的值是一个引用,而非对象。

②引用就像URL——是允许你访问真实信息的一小片数据。

③对于值类型的表达式,它的值是实际的数据。

④有时,值类型比引用类型更有效,有时恰好相反。

⑤引用类型的对象总是在堆上,值类型的值既可能在栈上,也可能在堆上,具体取决于上下文。

⑥引用类型作为方法参数使用时,参数默认是以“值传递”方式来传递的——但值本身是一个引用。

⑦值类型的值会在需要引用类型的行为时被装箱;拆箱则是相反的过程。

既然我们已经讨论了你需要适应的C# 1的所有特性。接着简单看一下每个特性在更高版本的C#中得到了哪些增强。

C# in Depth学习笔记-值类型和引用类型

原文:https://www.cnblogs.com/errornull/p/10022846.html

(0)
(0)
   
举报
评论 一句话评论(0
关于我们 - 联系我们 - 留言反馈 - 联系我们:wmxa8@hotmail.com
© 2014 bubuko.com 版权所有
打开技术之扣,分享程序人生!