参考书:《 visual C# 从入门到精通》
第二部分 理解C#对象模型
第8章 理解值和引用
文章目录
8.1 复制值类型的变量和类
C#大多数基元类型(不包括string)都是值类型。将变量声明为值类型,编译器会生成代码来分配足以容纳这种值的内存块。
而类类型不同,声明类类型时,编译器不生成代码来分配足以容纳一个类的内存块。它会分配一小块刚好可以容纳一个地址的内存,以后该类实际占用的内存是在使用new
关键字创建对象时分配的。类是应用类型的一个例子。引用类型容纳了对内存块的引用。我们要注意值类型和引用类型的区别。
对于值类型:
int i=42;
int copyi=i;//copyi为i的拷贝
i++;//i递增不会影响copyi的值
而类类型:
Circle c=new Circle(42);
Circle refc=c;//refc和c将容纳和c一样的地址
想要复制引用类型可以提供一个方法用来拷贝。
8.2 理解null
值和可空类型
考虑下面的情况:
Circle c=new Circle(42);
Circle copy=new Circle(99);
copy=c;
c赋给copy以后,copy原来引用的实例就不存在对它的任何引用。此时在运行时会通过垃圾回收机制来回收内存。但要知道垃圾回收是比较耗时的操作,所以不要创建不需要用到的对象。
C#允许将null
赋值给任意引用变量,表明该变量不引用内存中的任何对象。
空条件操作符?
:Console.WriteLine($"The area of circle c is {c?.Area()};")
,运行时若变量为null
则忽略当前语句。
8.2.1 使用可空类型
注意:null
本身时引用,不能赋值给值类型。int i=null;//非法
但可以将变量声明为可空值。可空值行为上与普通值类似,但可以将null
赋给它。用?
指定可空值:int? i=null;//合法
。
可将值类型表达式直接赋给可空变量,但不可将可空变量赋给值类型。
如果一个方法接受一个普通值类型的参数,就不能将一个可空变量作为实参传给它。
8.2.2 理解可空类型的属性
可空类型公开两个属性:判断类型是否实际包含非空的值(HasValue
),和该值是什么(Value
)。
int? i=null;
...;
if(!i.HasValue){
i=99;
}else{
Console.WriteLine(i.Value);
}
8.3 使用ref
和out
参数
向方法传递实参时,通常传递的时实参的拷贝,这样方法中对参数的修改并不会改变传入实参的原始值。这个机制本身是没问题的,但有时我们希望通过方法来实际的修改传入的参数。为此,C#提供了ref
和out
。
8.3.1 创建ref
参数
通过附加关键字ref
,将传递实参的引用而不是拷贝。
static void doIncreament(ref int param){
param++;
}
static void Main(){
int arg=42;
doIncreament(ref arg);
Console.WriteLine(arg);
}
注意变量需要先初始化。
8.3.2 创建out
参数
不同于ref
,当需要传递未初始化的实参时就需要用到out
。
out
参数必须要在方法中赋值。
static void doInitialize(out int param){
param=42;
}
static void Main(){
int arg;
doInitialize(out arg);
Console.WriteLine(arg);
}
8.4 计算机内存的组方式
操作系统和“运行时”通常将用于容纳数据的内存划分为两个独立的区域,每个区域都以不同方式管理。这两个区域通常称为栈和堆。栈和堆的设计目标完全不同。
调用方法时,参数和局部变量所需的内存总是从栈中获取。方法结束后,参数和局部变量分配的内存将自动归还给栈,并在另一个方法调用时重新使用。
使用new
创建对象时,创建对象所需内存总是从堆中获取。
栈和堆两个词来源于运行时的内存管理方式:
- 栈内存就像一系列堆得越来越高的箱子。
- 堆内存则像散布在房间里的一大堆箱子。
8.5 System.Object类
所有类都是System.Object
类的派生类。C#提供object
关键字作为它的别名。
Circle c;
c=new Circle(42);
object o;
o=c;
8.6 装箱
object
类型的变量能引用任何类型的任何对象,也能引用值类型的实例。
int i=42;
object o=i;
上述代码在运行时在堆中分配了一小块内存,然后i
的值被复制到这块内存中,最后让o
引用该拷贝,这种将数据项从栈自动复制到堆的行为称为装箱。
8.7 拆箱
为了访问已装箱的值,必须进行强制类型转换,简称转型。这个操作会检查是否能将一种类型安全转换成另一种类型,然后执行准换。
int i=42;
object o=i;
i=(int)o;
上述代码,o引用的是一个已装箱的 int
,转型成功执行,编译器生成的代码会从装箱的int
中提取出值,这个过程称为拆箱。
8.8 数据的安全转型
C#提供两个有用的操作符,帮助我们在转型时能更加顺利的进行:is
操作符和as
操作符。
8.8.1 is
操作符
is
操作符验证对象的类型是不是自己希望的。
WrappedInt wi=new WrappedInt();
...;
object o=wi;
if (o is WrappedInt){
WrappedInt temp=(WrappedInt)o;
...;
}
is
左边是对对象引用,右边是类型名称。
8.8.2 as
操作符
WrappedInt wi=new WrappedInt();
...;
object o=wi;
WrappedInt temp=o as WrappedInt;
if(temp!=null){
...;//转型成功代码才能执行
}
运行时as
表达式尝试将对象转换成指定类型,成功则返回转换成功的结果,失败则表达式结果为null
。
另外补充一个细节:
熟悉C
和C++
的人应该很熟悉一个重要的元素:指针。指针在使用时是非常容易因为使用不当而引发错误,造成系统的不安全。C#通过添加引用变量的方式使得指针的使用不那么必须,不用指针就不用担心误用指针引发不必要的麻烦了。但C#中还是可以像C++那样继续使用指针,但必须将代码标记为unsafe
。
public static void Main(string[] args){
int x=99,y=100;
unsafe{
swap(&x,&y);
}
Console.WriteLine($"x is now {x}, y is now {y}");
}
public static unsafe void swap(int *a,int *b){
int temp;
temp=*a;
*a=*b;
*b=temp;
}
编译包含unsafe
代码的程序时,必须右击项目
->属性
->生成
->允许不安全代码
(打勾)。