值类型和引用类型
纸上得来终觉浅,绝知此事要躬行!
1、值类型和引用类型的比较
问题 | 值类型 | 引用类型 |
---|---|---|
这个类型分配在哪里 | 分配在栈上 | 分配在托管堆上 |
变量是怎么表示的 | 值类型变量是本地存储的 | 引用类型变量指向被分配的实例所占的内存 |
基类型是什么 | 必须派生自System.ValueType | 可以派生自除System.ValueType的任何非密封类型 |
这个类型可以作为其他类型的基类吗 | 不能,值类型总是密封的,不能被继承 | 能,如果这个类型不是密封的,它就可以作为其他类型的基类 |
默认的参数传递行为是什么 | 变量是按值传递的(一个变量的副本传入被调用的函数) | 对于值类型,对象按值复制。对于引用类型,引用按值复制 |
这个类型可以重写System.Object.Finalize()【终结器】吗 | 不能,值类型不会放在堆上,因此不需要被终结 | 可以间接地重写(类似于C++析构函数语法) |
可以为这个类型定义构造函数吗 | 可以,但是默认的构造函数被保留(也就是自定义构造函数必须全部带有参数) | 可以 |
这个类型的变量什么时候消亡 | 当它们越出定义的作用域时 | 当托管堆被垃圾回收时 |
2、值类型和引用类型存储
引用类型的变量存储对其数据(对象)的引用,而值类型的变量直接包含其数据。
3、值类型
从下面代码的结果,可以看出,p1初始化的值为(1,2),然后赋值p1的数据给p2,这时候,会在栈上产生两个MutablePoint类型的副本,每一个都可以独立操作。所以在改变p2的Y值的时候,p1的数据不会发生任何改变。
using System;
public struct MutablePoint
{
public int X;
public int Y;
public MutablePoint(int x, int y) => (X, Y) = (x, y);
public override string ToString() => $"({X}, {Y})";
}
public class Program
{
public static void Main()
{
MutablePoint p1 = new MutablePoint(1, 2);
MutablePoint p2 = p1;
p2.Y = 200;
Console.WriteLine($"{nameof(p1)} after {nameof(p2)} is modified: {p1}");
Console.WriteLine($"{nameof(p2)}: {p2}");
MutateAndDisplay(p2);
Console.WriteLine($"{nameof(p2)} after passing to a method: {p2}");
}
private static void MutateAndDisplay(MutablePoint p)
{
p.X = 100;
Console.WriteLine($"Point mutated in a method: {p}");
}
}
// Expected output:
// p1 after p2 is modified: (1, 2)
// p2: (1, 200)
// Point mutated in a method: (100, 200)
// p2 after passing to a method: (1, 200)
4、包含引用类型的值类型
从下面的例子来看,Number字段是值类型,List<string>集合是引用类型,值类型的数据存储在栈上,引用类型的数据存储的是对象的引用,所以number类型产生了n1、n2两个数据的副本,独立操作、互不影响。但是List<string>集合存储在堆上的引用,修改其中一个的值,都会影响另一个相同引用的数据。
using System;
using System.Collections.Generic;
public struct TaggedInteger
{
public int Number;
private List<string> tags;
public TaggedInteger(int n)
{
Number = n;
tags = new List<string>();
}
public void AddTag(string tag) => tags.Add(tag);
public override string ToString() => $"{Number} [{string.Join(", ", tags)}]";
}
public class Program
{
public static void Main()
{
var n1 = new TaggedInteger(0);
n1.AddTag("A");
Console.WriteLine(n1); // output: 0 [A]
var n2 = n1;
n2.Number = 7;
n2.AddTag("B");
Console.WriteLine(n1); // output: 0 [A, B]
Console.WriteLine(n2); // output: 7 [A, B]
}
}
5、按值传递传引用类型(无参数修饰符)
查看对象p的结果,可以看到,调用void PersonValue(Person p)方法,修改了p的Age的值,这是因为参数p传递的是数据的引用,修改这个引用,对象p的数据也会随之发生改变。但是为什么PersonValue方法中的p = new Person(“p2”, 22);没有改变p的数据呢,这是因为这里相当于重新实例化了对象p,但是形参传递的是引用,这里无法修改,当前p的实例化只在当前作用域内可以生效,离开这个作用域以后就会被垃圾回收。所以,如果这里传递的形参为string类型(引用类型),数据的结果你应该也清楚了吧!
class Program
{
static void Main(string[] args)
{
Person p = new Person("p1", 11);
Console.WriteLine("1*************");
p.DisPlay(); //Name="p1",Age=11
PersonValue(p);
Console.WriteLine("2*************");
p.DisPlay(); //Name="p1",Age=99
}
static void PersonValue(Person p)
{
p.Age = 99;
Console.WriteLine("3*************");
p.DisPlay(); //Name="p1",Age=99
p = new Person("p2", 22);
Console.WriteLine("4*************");
p.DisPlay(); //Name="p2",Age=22
}
}
public class Person
{
public string Name { get; set; }
public int Age { get; set; }
public Person(string name, int age)
{
Name = name;
Age = age;
}
public void DisPlay()
{
Console.WriteLine($"Name={Name},Age={Age}");
}
}
结果如下:
6、按引用传递传引用类型
下面这个例子使用ref参数修饰符来进行引用传递,从结果可以很明显地看出,void PersonValue(ref Person p)方法修改了对象p的引用在内存中的指向。
class Program
{
static void Main(string[] args)
{
Person p = new Person("p1", 11);
Console.WriteLine("1*************");
p.DisPlay(); //Name="p1",Age=11
PersonValue(ref p);
Console.WriteLine("2*************");
p.DisPlay(); //Name="p1",Age=99
}
static void PersonValue(ref Person p)
{
p.Age = 99;
Console.WriteLine("3*************");
p.DisPlay(); //Name="p1",Age=99
p = new Person("p2", 22);
Console.WriteLine("4*************");
p.DisPlay(); //Name="p2",Age=22
}
}
public class Person
{
public string Name { get; set; }
public int Age { get; set; }
public Person(string name, int age)
{
Name = name;
Age = age;
}
public void DisPlay()
{
Console.WriteLine($"Name={Name},Age={Age}");
}
}
结果如下:
传递引用类型的黄金规则:
- 如果按引用传递引用类型,被调用者可能改变对象的状态数据的值和引用的对象;
- 如果按值传递引用类型,被调用者可能改变对象的状态数据的值,但不能改变所引用的对象
多写代码少打字!共勉!!!