08 理解值和引用

1.复制值类型的变量和类

C#大多数基元类型(包括int,float,double和char等,但不包括string,原因稍后解释)都是值类型。将变量声明为值类型,编译器会生成代码来分配足以容纳这种值的内存块。例如,声明int类型的变量会导致编译器分配4字节(32位)内存块。向int变量赋值(例如42),将导致值被复制到内存块中。

类类型(比如上一篇博客讲的Circle类)则以不同方式处理。声明Circle变量时,编译器不生成代码来分配足以容纳一个Circle的内存块。相反,它唯一做的事情就是分配一小块内存,其中刚好可以容纳一个地址。以后,Circle实际占用内存块的地址会填充到这里。该地址称为对内存块的引用。Circle对象实际占用的内存是在使用new关键字创建对象时分配的。类是引用类型的一个例子。

注意:C#的string实际是类类型。由于字符串大小不固定,所以更高效的策略是在程序运行时动态分配内存,而不是在编译时静态分配。事实上,C#的string关键字是System.String类的别名。

值类型举例(修改i的值不会改变copyi的值):

int i = 42;      //声明并初始化i

int copyi = i;        //copyi包含i中的数据的拷贝,i和copyi都包含值42

i++;        //i递增不影响copyi;i现在包含43,copyi仍然包含42

引用类型举例(refc和c都引用同一个Circle对象):

Circle c = new Circle(42);

Circle refc = c;

(@代表引用,容纳的是内存地址)

 

深拷贝和浅拷贝

上述的引用类型的复制其实就是浅拷贝(只复制了引用)

当然我们可以提供一个Clone()方法,去复制对象

例:

class Circle

{

    private int radius;

    //省略了构造器和其他方法

    …

    public Circle Clone()

    {

        //创建新的Circle对象

        Circle clone = new Circle();

        //将私有数据从this复制到clone

        clone.rafius = this.radius;

        //返回包含克隆数据的新Circle对象

        return clone;

    }

}

提供的Clone方法,就是“深拷贝”,复制的是引用的对象

 

2.理解null值和可空类型

C#允许将null值赋给任意引用变量。值为null的变量表明该变量不引用内存中的任何对象。

Circle c = new Circle(42);

Circle copy = null;//声明的同时进行初始化,这是最好的编程实践

…

if(copy == null)

{

    copy = c;     //copy和c引用同一个对象

    …

}

如果下列代码在Circle对象包含空值的时候调用其Area方法:

Circle c = null;

Console.WriteLine($"The area of circle c is {c.Area()}");

这会造成c.Area方法抛出一个NullReferenceException。为避免出现该异常,我们对代码进行优化,如下:

if(c != null)

{

    Console.WriteLine($"The area of circle c is {c.Area()}");

}

当然我们还可以用一种更简单的方法,使用空条件操作符:

Console.WriteLine($"The area of circle c is {c?.Area()}");

空条件操作符告诉“运行时”在操作符所应用的变量为null的前提下忽略当前语句。

 

2.1使用可空类型

null本身就是引用,不能把它赋给值类型

举例:

int i = null; //非法 

当然我们可以将变量声明为可空值类型,此时可以将null值赋给它

例如:

int? i = null;   //合法

 

注:不能将可空的值赋给普通的值类型的变量

 

2.2理解可空类型的属性

可空类型公开了两个属性,用于判断类型是否实际包含非空的值,以及该值是什么。其中,HasValue属性判断可空类型是包含一个值,还是包含null。如果包含值,可用Value属性获取该值。如下所示:

int? i = null;

…

if(!i.HasValue)

{

    //如果i为null,就将99赋给它

    i=99;

}

else

{

    //如果i不为null,就显示它的值

    Console.WriteLine(i.Value);

}

3.使用ref和out参数

先来举个例子:

static void doIncrement(int param)

{

    param++;

}

static void Main()

{

    int arg = 42;

    doIncrement(arg);

    Console.WriteLine(arg);  //输出42,而不是43

}

doIncrement方法递增的只是实参(arg)的拷贝,原始实参不递增

3.1创建ref参数

为参数(形参)附加ref前缀,C#编译器将生成代码传递对实参的引用,而不是传递实参的拷贝

static void doIncrement(ref int param)

{

    param++;

}

static void Main()

{

    int arg = 42;

    doIncrement(arg);

    Console.WriteLine(arg);  //输出43

}

注意:“变量使用前必须赋值”规则同样适合方法实参。不能将未初始化的值作为实参传给方法,即便是ref实参。

 

3.2创建out参数

编译器在调用方法之前,验证它的ref参数已被赋值。但有时希望由方法本身初始化参数,所以希望向其传递未初始化的实参。out关键字正是针对这一目的设计的。

 

关键字out是output(输出)的简称。向方法传递out参数之后,必须在方法内部对其进行赋值,否则无法编译

static void DoInitialize(out int param)

{

    param = 42;//在方法中初始化,如果未初始化就会编译不通过

}

4.计算机内存的组织方式

为了理解值类型和引用类型的区别,有必要理解数据在内存中是如何组织的。

操作系统和“运行时”通常将用于容纳数据的内存划分为两个独立的区域,每个区域都以不同方式管理。这两个区域通常称为栈和堆。栈和堆的设计目标完全不同。

(1)调用方法时,它的参数和局部变量所需的内存总是从栈中获取。方法结束后,由于要么正常返回,要么抛出异常,所以为参数的局部变量分配的内存将自动归还给栈,并可在另一个方法调用时重新使用。栈上的方法参数和局部变量具有良好定义的生存期。方法开始时进入生存期,方法结束时结束生存期。

例如:

while()

{

    int i = …  ;//这时i在栈上创建

    …
    
}

//这时i就从栈中消失了

(2)使用new关键字创建对象(类的实例)时,构造对象所需的内存总是从堆中获取。前面讲过,使用引用变量,可以从多个地方引用同一个对象。对象的最后一个引用消失之后,对象占用的内存就可供重用(虽然不一定被立即回收)。后面的博客中将进一步讨论堆内存是如何回收的。

 

“栈”和“堆”这两个词来源于“运行时”的内存管理方式。

(1)栈(Stack)内存就像一系列堆得越来越高的箱子。调用方法时,它的每一个参数都被放入一个箱子,并将这个箱子放到栈的最顶端。每个局部变量也同样分配到一个箱子,并同样放到栈顶。方法结束后,它的所有箱子都从栈中移除。

(2)堆(Heap)内存则像散布在房间里的一大堆箱子, 不像栈那样每个箱子都严格地堆在另一个箱子上。每个箱子都有一个标签,标记了这个箱子是否正在使用。创建新对象时,“运行时” 查找空箱子,把它分配给对象。对对象的引用则存储在栈上的一个局部变量中。“运行时” 跟踪每个箱子的引用数量(记住,两个变量可能引用同一个对象)。一旦最后一个引用消失,运行时就将箱子标记为“未使用”。将来某个时候,会清除箱子里的东西,使之能被重用。

 

使用栈和堆

思考调用以下方法会发生什么:

void Method(int param)

{

    Circle c;

    c = new Circle(param);

}

假定传给param的值是42。调用方法时,栈中将分配- - 小块内存(刚够存储一个int),并用值42初始化。在方法内部,还要从栈中分配出另一小块内存,它刚够存储.一个引用(一个内存地址),只是暂时不进行初始化(它是为Circle类型的变量C准备的)。接着,要从堆中分配一个足够大的内存区域来容纳一个Circle对象。这正是new关键字所执行的操作:它运Circle构造器,将这个原始的堆内存转换成Circle对象。对这个Circle对象的引用将存储到变量c中。下图对此进行了演示。

这时应注意以下两点。

(1)虽然对象本身存储在堆中, 但对象引用(变量c)存储在栈中。

(2)堆内存是有限的资源。堆内存耗尽,new操作符抛出OutOfMemoryException,对象创建失败。

方法结束后,参数和局部变量将离开作用域。为c和param分配的内存会被自动回收到栈。“运行时”发现已不存在对Circle对象的引用,所以会在将来某个时候,安排垃圾回收器回收它的内存(后面博客会讲到)。

 

5.System.Object类

.NET Framework最重要的引用类型之一是 System命名空间中的Object类。要完全理解System.object类的重要性,需要先理解继承(后面博客会单独讲继承)。就目前来说,请暂时接受这样的说法:所有类都是System.object的派生类:另外,System.object 类型的变量能引用任何对象。由于System.object 相当重要,所以C#提供了object 关键字来作System.object的别名。实际写代码时,既可以写成object,也可以写成System.object,两者没有区别。

 

下例的变量c和。引用同一个Circle对象。C的类型是Circle, 。的类型是object(System.object的别名),它们从不同角度观察内存中的同一个东西:

Circle c;

c = new Circle(42);

object o;

o = c;

内存图:

6.装箱

定义:将数据项自动赋值到堆的行为称为装箱

如前所述,object 类型的变量能引用任何引用类型的任何对象。除此之外,object类型的变量也能引用值类型的实例。例如,以下两个语句将int类型(一个值类型)的变量i初始化为42,并将object类型(一个引用类型)的变量。初始化为i:

int i = 42;

object o =i;

执行第二个语句所发生的事情需要仔细思考-下。1是值类型,所以它在栈中。如果直接引用i,那么引用的将是栈。然而,所有引用都必须引用堆上的对象:如果引用栈上的数据项,会严重损害“运行时”的健壮性,并造成潜在的安全漏洞,所以是不允许的。实际发生的事情是“运行时”在堆中分配一小块内存,然后i的值被复制到这块内存中,最后让o引用该拷贝。

内存图如下:

7.拆箱

首先我们先说以下转型的定义:为了访问已装箱的值,必须进行强制类型转换。这个操作会先检查是否能将一种类型安全转换成另一种类型,然后才执行转换。

比如:

int i = 42;

object o = 1;//装箱

i = (int)o;//成功编译   拆箱

拆箱:编译器生成的代码会从装箱的值类型中提取出值

内存图:

当然如果引用的不是已装箱的int,就会出现类型不匹配的情况,如下例所示:

Circle c = new Circle(42);

object o=c;// 不装箱,因为c是引用类型的变量,而不是值类型的变量

int i=(int)o;//编译成功,但在运行时抛出异常

内存图:

装箱有一定用处,但滥用会严重影响性能。后面博客中将介绍与装箱异曲同工的另一种技术—— 泛型。

 

8.数据的安全转型

从上面可以看出,进行强制类型转换时,如果内存中的对象的类型与指定的类型不匹配。“运行时”会抛出InvalidCastException异常。

下面介绍两个操作符,能以更得体的方式执行转型。

8.1is操作符

用is操作符验证对象的类型是不是自己希望的,如下所示:

WrappedInt wi = new WrappedInt();

…

object o =  wi:

if (o is WrappedInt)

{

    WrappedInt temp = (WrappedInt)o; // 转型是安全的:o确定是一个WrappedInt

    …

}

is操作符取两个操作数:左边是对对象引用,右边是类型名称。如果左边的对象是右边的类型,则is表达式的求值结果为true,反之为false.换言之,上述代码只有在确定转型能成功的前提下,才真的将引用变量。转型为WrappedInt.

 

8.2as操作符

as操作符充当了和is操作符类似的角色,只是功能稍微进行了删减。可以像下面这

样使用as操作符:

WrappedInt wi = new WrappedInt();

…

object 0 =  wi;

WrappedInt temp = o as WrappedInt;

if(temp != null)

{

    //转型成功,这里的代码才会执行

}

和is操作符一样,as 操作符取对象和类型作为左右操作数。“ 运行时”尝试将对象转换成指定类型。若转换成功,就返回转换成功的结果。在本例中,这个结果被赋给WrappedInt类型的变量temp.相反,若转换失败,as表达式的求值结果为null,这个值也会被赋给temp。

后面博客也会进一步讨论is和as操作符。

参考书籍:《Visual C#从入门到精通》

发布了46 篇原创文章 · 获赞 53 · 访问量 3714

猜你喜欢

转载自blog.csdn.net/qq_38992372/article/details/104983233
08