值类型与引用类型:
值类型:
值类型的变量直接包含值,变量引用的位置就是值在内存中实际存储的位置。因此,将一个原始变量的值赋给另一个变量,会在新变量的位置创建原始变量的值的一个内存副本。两个变量不可能引用同一个内存位置。同样的,在方法内部对参数值进行任何修改都不会影响调用者中的原始值。由于值类型需要有一个内存副本,所以定义时通常不要让它消耗太多内存。
值类型的值一般只是短时间存在,很多情况下,这样的值只是作为表达式的一部分,或用于激活方法,在这些情况下,值类型的变量和临时值经常是存储在成为栈的临时存储池中。
临时池清理的代价低于需要垃圾回收的堆,不过,值类型要比引用类型更频繁的复制,这种复制操作会增加性能的开销。
引用类型:
引用类型的值是对一个对象实例的引用,通常是内存地址,要去那个位置找到对象实例的数据,为了访问数据,运行时要从变量中读取引用,然后对它进行解引用,从而到达包含实例数据的内存地址。对于引用类型的变量,它的值要么是null,要么是对需要进行垃圾回收的堆上的一个存储位置的引用。
结构:
定义自定义值类型使用的是和定义类和接口相似的语法,区别在于值类型使用关键字struct。
结构的初始化:
除了属性和字段,结构还可包含方法和构造器。结构不允许包含用户定义的默认构造器。在没有提供默认的构造器时,C#编译器自动的产生一个默认的构造器将所有字段初始化为各自的默认值。
为了确保值类型的局部变量能被完全初始化,结构的每个构造器都必须初始化结构中的所有字段。如果对结构的所有数据初始化失败,会造成编译时错误。当实例化一个包含了未赋值的值类型字段的引用类型,或者实例化一个没有初始化器的值类型数组时,应该显式的初始化。
在初始化好所有的字段之前,访问this是非法的。
装箱:
将值类型转换成它实现的某个接口或者object,转换的结果必然是对一个存储位置的引用,该变量表面上包含引用类型的实例,但实际上包含值类型的值。这种转换称为装箱转换。
- 在堆上分配内存:它将用于存储值类型的数据以及少许额外开销。
- 接着发生一次内存复制,当前存储位置的值类型数据被复制到堆上分配好的位置。
- 转换结果是对堆上新存储位置的引用。
相反的过程称之为拆箱,先检查已装箱的值的类型兼容于要拆箱的值的类型,然后复制堆中存储的值。
public class Program
{
static void Main()
{
int totalCount;
System.Collections.ArrayList list = new System.Collections.ArrayList();
Console.Write("Enter a number between 2 and 1000:");
totalCount = int.Parse(Console.ReadLine());
list.Add((double)0); // box
list.Add((double)1); // box
for (int count = 2; count < totalCount; count++)
{
list.Add((double)list[count - 1] + (double)list[count - 2]);
// two unbox and one box per command
}
foreach(double count in list)
{
Console.Write("{0}, ", count);
// box per command
}
}
}
每次装箱操作都涉及内存分配和复制,每个拆箱操作都涉及类型检查和复制。如果用已拆箱的类型做同样的事情,就可以避免内存分配与类型检查。
拆箱时的类型
要在运行时成功拆箱值类型,被拆箱的项必须是对一个对象的引用,该对象是先前通过装箱该值类型的实例创建的。尝拆箱 null
会导致 NullReferenceException。 尝试拆箱对不兼容值类型的引用会导致InvalidCastException。
int number;
object thing;
double bigNumber;
number = 42;
thing = number;
// bigNumber = (double)thing; InvalidCastException 这里的thing先前通过装箱int类型的实例创建,与double不兼容
bigNumber = (double)(int)thing;
避免可变的值类型:
using System;
namespace learnCsharp
{
public class Program
{
static void Main()
{
Angle angle = new Angle(25, 58, 23);
object objectAngle = angle; // box
Console.Write(((Angle)objectAngle).Degrees);
//1.unbox,modify unbox value, and discard value;
((Angle)objectAngle).MoveTo(26, 58, 23);
Console.Write(", " + ((Angle)objectAngle).Degrees);
//2.box,modify boxed value, and discard reference to box
((IAngle)angle).MoveTo(26, 58, 23);
Console.Write(", " + ((Angle)angle).Degrees);
//3.Modify boxed value directly
((IAngle)objectAngle).MoveTo(26, 58, 23);
Console.WriteLine(", " + ((Angle)objectAngle).Degrees);
}
}
interface IAngle
{
void MoveTo(int degrees, int minutes, int seconds);
}
struct Angle : IAngle
{
public Angle(int degrees,int minutes,int seconds)
{
Degrees = degrees;
Minutes = minutes;
Seconds = seconds;
}
public void MoveTo(int degrees, int minutes, int seconds)
{
Degrees = degrees;
Minutes = minutes;
Seconds = seconds;
}
public int Degrees { get; set; }
public int Minutes { get; set; }
public int Seconds { get; set; }
}
}
值类型的变量就像是上面写了值的纸,对值进行装箱,相当于对纸进行复印,并把复印件装到箱子里。
对值进行拆箱,相当于对箱子里的纸进行复印。编辑第二份复印件不会对箱子里的副本造成影响。
- 为了调用MoveTo(),编译器对objectAngle进行拆箱,并且创建值的副本。值类型根据值进行复制,虽然结果值在执行时被成功修改,但是值的这个副本会被丢弃。objectAngle引用的堆位置没有发生任何改变。
- 第二次,将值类型转换成IAngle。将值类型转换为接口类型会进行装箱。运行时将angle的值复制到堆上,并提供对箱子的引用,接着,方法调用会修改被引用的箱子中的值,存储在angle变量中的值保持未修改状态。
- 第三次,向IAngle的转型为引用转换(在实现类与接口之间转换)而非装箱转换,值已经通过向objectAngle的转换装箱了,这次不会发生值的复制,对MoveTo的调用将更新箱子中的对应值,代码的行为符合预期。
如何在方法调用期间避免装箱:
每次进行拆箱后调用,不管方法是否真的要修改变量,都会重复以下过程:
对已装箱的值进行类型检查,拆箱以生成已装箱的值的存储位置,分配临时变量,将值从箱子复制到临时变量,再调用方法并传递临时存储位置。如果方法不修改变量,那么很多工作都可以避免。
在已装箱的值类型上调用接口方法,所有这些开销都可以避免。这种情况下,预期接收者是箱子中的存储位置,如果接口方法要修改存储位置,修改的是箱子的位置。
int number;
object thing;
number = 42;
// boxing
thing = number;
// No unboxing conversion
string text = ((IFormattable)thing).ToString("X",null);
Console.WriteLine(text);
如果将值类型的实例作为接收者来调用object声明的虚方法ToString():
- 如果接收者已拆箱,而且结构重写了ToString(),将直接调用重写的方法。因为方法不能被更深的派生类重写了。
- 如果接收者已拆箱,而且结构没有重写ToString(),就必须调用基类的实现,该实现预期的接收者是一个对象引用,所以接收者被装箱。
- 如果接收者已装箱,而且结构重写了ToString(),就将箱子的存储位置传给重写的方法,不对其进行拆箱。
- 如果接收者已装箱,而且结构没有重写ToString(),就将对箱子的引用传给基类的实现,该实现预期的正是一个引用。
枚举:
枚举值实际是作为整数常量实现的,默认第一个枚举值是0,后续每一项都递增1,然而,可以显式的为枚举赋值。
枚举值的基础类型可以是除了char之外的任意整型。默认基础类型为int,可以使用继承语法指定其他类型:
enum ConnectionState : short
{
Disconnected,
Connecting = 10
}
枚举间的类型兼容性:
C#不支持不同枚举数组之间的直接转型,但CLR允许,前提是两个枚举具有相同的基础类型。为了避免C#的限制,一个可用的技巧是先转型为System.Array。
枚举和字符串之间的转换:
- 枚举->字符串:枚举可以使用ToString()方法,输出枚举值标识符。
- 字符串->枚举:使用System.Enum提供的一个静态方法。
ThreadPriorityLevel priority = (ThreadPriorityLevel)Enum.Prase(typeof(ThreadPriorityLevel),"Idle");
枚举作为标志使用:
如果希望枚举值可以自由组合,那么需要每个枚举值都是不重复的一位。
如果决定使用位标志枚举,枚举的声明应该用FlagsAttribute进行标记。这个特性应包含在一对方括号中,并放在枚举声明之前。
原文地址:https://www.cnblogs.com/zhang-mo/p/9834068.html