第八章 理解值和引用

1.复制值类型的变量和类
C#大多数基本数据类型(包括int, float, double等)都是值类型。将变量声明为值类型,编译器会生成代码来分配足以容纳这种值的内存块。
例如,声明int类型的变量会导致编译器分配4字节(32位)内存块。在visual studio,调试 -> 窗口 -> 即时,右下角有个小窗口,即时窗口,调试时查看变量的内存地址。

int i = 2;
int j = 90;
....

变量i,j在内存中:

类类型则以不同的方式处理。声明Circle变量时,编译器不生成代码来分配足以容纳一个Circle的内存块。相反,它唯一做的事情就是分配一小块内存,其中刚好可以容纳一个地址。
以后,Circle实际占用内存块的地址会填充到这里。该地址称为对内存块的引用。

int i = 2;
int j = 90;
Drink drink = new Drink;
int l = j;
Drink anotherDrink = drink;

class Drink
{
      double price;
}

变量 i, j, drink, l, anotherDrink在内存中:

值类型变量,在栈内存,且内存块大小固定。复制值类型,将栈内存块中的数据复制给另一个栈内存块;
引用类型变量,在栈内存,而实际数据存储在堆内存。复制引用类型,将栈内存块中的引用(堆内存地址)复制给另外一个栈内存块。


new关键字创建对象,在堆内存中动态分配内存。引用类型的变量也在栈内存中分配内存,只不过变量存储的是一个堆内存地址。
也就是说,值类型变量存储的数据就在这个变量内存中,引用类型变量存储的是一个"电话号码",实际数据存储在堆内存中。
值类型变量是基本数据类型的变量,如: int, long, double, char......编译期间就能确定变量的大小。如:int(32位),4个字节,0-2147483647之间的整数都能装下。
而引用类型因为设计的类不尽相同,程序运行期间实体化的对象也不同,对应的需要分配多大的内存也不确定。所以。new关键字创建对象时,是动态分配内存。
string类型实际上是类类型。由于字符串大小不固定,所以更高效的策略是在程序运行时动态分配内存,而不是在编译时静态分配。


2.复制对象

PlayStation ps1 = new PlayStation();
PlayStation ps2 = ps1; // 类似于基本数据类型变量的复制
PlayStation ps3 = ps1.Clone(); // 提供克隆对象方法

class PlayStation
{
      doubel price;
      ......
      public PlayStation Clone()
      {
            PlayStation ps = new PlayStation();
            ps.price = this.price;
            return ps;
      } 
}

引用变量 ps1, ps2, ps3在内存中:

ps2 = ps1; 和值类型变量的复制不同,引用类型变量复制,相当于复制"电话号码",而不是复制实际数据。
为了真正的复制数据,可以提供克隆方法,将创建新对象,复制旧对象中的所有数据,返回新对象。新对象和旧对象数据相同,但它们有不同的"电话号码"。
只复制引用(复制"电话号码"),称为"浅拷贝"。提供Clone()方法,能够复制引用的对象,称为"深拷贝"。
引用可以理解为"电话号码"实际对应的数据,引用类型变量不直接存储数据,而是间接的存储内存地址。


创建同一个类的两个对象,它们分别能访问对方的私有数据。还是理解不了!!
"私有"实际是指类的级别上私有,而非对象级别上私有。字段声明为私有,类的每个实例都有一份自己的数据。声明为静态,每个实例都共享一份数据。

3.空条件操作符
可用空条件操作符更简洁地测试空值,使用它需为变量名附加问好(?)前缀。

Calculate c = null;
if (c != null)
{
  Console.WriteLine($"result: {c.add()}");
}

Console.WriteLine($"result: {c?.add()}"); // 输出:result: 
// 使用if语句先检测Calculate对象是否为空,与使用空条件操作符还是有区别。

4.使用可空类型
null值在初始化引用类型时非常有用,但null本身就是引用,不能把它赋给值类型。可空值类型在行为上与普通值类型相似,但可将null值赋给它。
可空类型实际上是引用类型。

int ? i = null; // 合法
int j = null; // 不合法

i = 9; // 合法
int k =i; // 不合法。不可将可空变量赋给普通值类型变量。

// 可空类型的属性
int ? i = null;
if(!i.HasValue)
{
  // i为null,就将99赋给它 
  i = 99;
}
else
{
  // i不为null,就显示它的值
  Console.WriteLine(i.Value); // 可空类型的Value属性是只读的。可用该属性读取变量的值,但不能修改。
}

5.创建ref参数
为参数(形参)附加ref前缀,C#编译器将生成代码传递对实参的引用,而不是传递实参的拷贝。使用ref参数,作用于参数的所有操作都会作用于原始参数,
因为参数和实参引用同一个对象。作为ref参数传递的实参也必须附加ref前缀。这个语法明确告知开发人员实参可能改变。

static void doIncrement(ref int param)
{
  param++;
}

static void Main()
{
  int arg = 42;
  doIncrement(ref arg);
  Console.WriteLine(arg);
}

6.创建out参数
编译器会在调用方法之前验证其ref参数已被赋值。但有时希望由方法本身初始化参数,所以希望向其传递未初始化的实参。这时要用到out关键字。

static void doInitialize(out int param)
{
  param = 42;
}

static void Main()
{
  int arg;
  doInitialize(out arg);
  Console.WriteLine(arg);
}

7.Object类
a: 所有类都是System.Object的派生类。
b: System.Object类型的变量能引用任何对象。
object关键字是System.Object的别名,实际写代码时,既可以写object,也可以写System.Object,两者没有区别。

Tree c;
c = new Tree();
object o;
o = c;

变量c,o在内存中:

8.装箱
object类型的变量能引用任何引用类型的任何对象。此外,object类型的变量也能引用值类型的实例。

int i = 100;
object o = i; // 修改变量i的原始值,o所引用的堆上的值不变。类似的,修改堆上的值,变量的原始值也不变。

如果o直接引用i,那么引用的将是栈。然而,所有引用都必须引用堆上的对象;引用栈上的数据项,会严重损害“运行时”的健壮性,并造成潜在的安全漏洞,所以是不允许的。
实际发生的事情是“运行时”在堆中分配一小块内存,然后i的值被复制到这块内存中,最后,让o引用该拷贝。这种将数据项从栈复制到堆的行为称为装箱。

9.拆箱
由于object类型的变量可引用已装箱的数据项的拷贝,所以通过该变量也应该能获取装箱的值。从装箱的数据项中取出值,该过程称为拆箱。
为了访问已装箱的值,必须进行强制类型转换,简称转型。这个操作会先检查是否能将一种类型安全转换成另一种类型,然后才执行转换。

int i = 1;
object o = i; // 装箱
i = (int)o; // 拆箱

然而,如果o引用的不是已装箱的int,就会出现类型不匹配的情况,造成转型失败。编译器生成的代码将在运行时抛出InvalidCastException。

10.is,as 操作符
在对象类型不符合预期的情况下捕捉异常,试图恢复应用程序顺利执行,这是一个相当繁琐的过程。is操作符和as操作符能以更得体的方式执行转型。

WrappedInt wi = new WrappedInt()
...
object o = wi;
if(o is WrappedInt) // 只有确定转型能成功,才真的将变量o转型为WrappedInt
{
    WrappedInt temp = (WrappedInt)o; // 转型是安全的
}

as操作符充当了和is操作符类似的角色,只是功能上稍微进行了删减。

WrappedInt wi = new WrappedInt()
...
object o = wi;
WrappedInt temp = o as WrappedInt;
if(temp != null)
{
  .... // 只有转型成功,这里的代码才会执行
}
posted @ 2021-02-08 07:51  葡式蛋挞  阅读(29)  评论(0)    收藏  举报