C#本质论6.0第八章:值类型

值类型与引用类型:

值类型:

值类型的变量直接包含值,变量引用的位置就是值在内存中实际存储的位置。因此,将一个原始变量的值赋给另一个变量,会在新变量的位置创建原始变量的值的一个内存副本。两个变量不可能引用同一个内存位置。同样的,在方法内部对参数值进行任何修改都不会影响调用者中的原始值。由于值类型需要有一个内存副本,所以定义时通常不要让它消耗太多内存。

值类型的值一般只是短时间存在,很多情况下,这样的值只是作为表达式的一部分,或用于激活方法,在这些情况下,值类型的变量和临时值经常是存储在成为栈的临时存储池中。

临时池清理的代价低于需要垃圾回收的堆,不过,值类型要比引用类型更频繁的复制,这种复制操作会增加性能的开销。

引用类型:

引用类型的值是对一个对象实例的引用,通常是内存地址,要去那个位置找到对象实例的数据,为了访问数据,运行时要从变量中读取引用,然后对它进行解引用,从而到达包含实例数据的内存地址。对于引用类型的变量,它的值要么是null,要么是对需要进行垃圾回收的堆上的一个存储位置的引用。

结构:

定义自定义值类型使用的是和定义类和接口相似的语法,区别在于值类型使用关键字struct。

结构的初始化:

除了属性和字段,结构还可包含方法和构造器。结构不允许包含用户定义的默认构造器。在没有提供默认的构造器时,C#编译器自动的产生一个默认的构造器将所有字段初始化为各自的默认值。

为了确保值类型的局部变量能被完全初始化,结构的每个构造器都必须初始化结构中的所有字段。如果对结构的所有数据初始化失败,会造成编译时错误。当实例化一个包含了未赋值的值类型字段的引用类型,或者实例化一个没有初始化器的值类型数组时,应该显式的初始化。

在初始化好所有的字段之前,访问this是非法的。

装箱:

将值类型转换成它实现的某个接口或者object,转换的结果必然是对一个存储位置的引用,该变量表面上包含引用类型的实例,但实际上包含值类型的值。这种转换称为装箱转换。

  1. 在堆上分配内存:它将用于存储值类型的数据以及少许额外开销。
  2. 接着发生一次内存复制,当前存储位置的值类型数据被复制到堆上分配好的位置。
  3. 转换结果是对堆上新存储位置的引用。

相反的过程称之为拆箱,先检查已装箱的值的类型兼容于要拆箱的值的类型,然后复制堆中存储的值。

    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; }
    }
}

值类型的变量就像是上面写了值的纸,对值进行装箱,相当于对纸进行复印,并把复印件装到箱子里。

对值进行拆箱,相当于对箱子里的纸进行复印。编辑第二份复印件不会对箱子里的副本造成影响。

  1. 为了调用MoveTo(),编译器对objectAngle进行拆箱,并且创建值的副本。值类型根据值进行复制,虽然结果值在执行时被成功修改,但是值的这个副本会被丢弃。objectAngle引用的堆位置没有发生任何改变。
  2. 第二次,将值类型转换成IAngle。将值类型转换为接口类型会进行装箱。运行时将angle的值复制到堆上,并提供对箱子的引用,接着,方法调用会修改被引用的箱子中的值,存储在angle变量中的值保持未修改状态。
  3. 第三次,向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

时间: 2024-11-12 12:28:34

C#本质论6.0第八章:值类型的相关文章

【转】C#详解值类型和引用类型区别

通用类型系统 值类型 引用类型 值类型和引用类型在内存中的部署 1 数组 2 类型嵌套 辨明值类型和引用类型的使用场合 5 值类型和引用类型的区别小结 首先,什么是值类型,什么是引用类型? 在C#中值类型的变量直接存储数据,而引用类型的变量持有的是数据的引用,数据存储在数据堆中. 值类型(value type):byte,short,int,long,float,double,decimal,char,bool 和 struct 统称为值类型.值类型变量声明后,不管是否已经赋值,编译器为其分配内

引用类型和值类型

CLR支持两种类型:引用类型和值类型. 虽然FCL中大多数都是引用类型,但开发人员用的最多的还是值类型.引用类型总是在托管堆上分配的,C#的new操作符会返回对象的内存地址——也就是指向对象数据的内存地址. 使用引用类型必须注意到一些性能问题,首先考虑一下事实: 1)内存必须从托管堆上分配. 2)对上分配的每个对象都有一些额外的成员(比如前面提到过得"类型对象指针"和"同步块索引"),这些成员必须初始化. 3)对象中的其他字节(为字段而设)总是设为零. 4)从托管堆

第5章 基元类型、引用类型和值类型

5.1编程语言的基元类型 编译器(Compiler)直接支持的数据类型称为基元类型(primitive type). 我希望编译器根本不要提供基元类型名称,强制开发人员使用FCL(Framework类库)类型名称: 许多开发人员都困惑于到底应该使用string还是String.由于C#的string直接映射到System.String,所以两者是没有区别的.int始终映射到System.Int32,所以不管在什么操作系统上运行,代表的都是32位整数. 5.2引用类型和值类型 虽然FCL中大多数类

C# 中的值类型和引用类型

原文 C# 中的值类型和引用类型 值类型(value type):int,long,float,double,decimal,char,bool 和 struct 统称为值类型.值类型变量声明后,不管是否已经赋值,编译器为其分配内存. 引用类型(reference type):string 和 class统称为引用类型.当声明一个类时,只在栈中分配一小片内存用于容纳一个地址,而此时并没有为其分配堆上的内存空间.当使用 new 创建一个类的实例时,分配堆上的空间,并把堆上空间的地址保存到栈上分配的

EffectiveC#8--确保0对于值类型数据是有效的(初始化问题)

1.决不要创建一个不包括0在内的枚举类型 2.举例如下: public enum Planet { Mercury = 1, Venus = 2, Earth = 3, Mars = 4, Jupiter = 5, Saturn = 6, Neptune = 7, Uranus = 8, Pluto = 9 } Planet sphere = new Planet(); sphere此时的值就是0,而这并不是一个有效的值.这也会使得包含(Planet)这一类型的其它类型很难创建. 假设某个结构体

《Effective C#》条款8:确保0为值类型的有效状态

.NET系统的默认初始化机制会将所有的对象设置为0[14].对于值类型来讲,我们无法阻止其他程序员将其所有的成员都初始化为0[15].因此,我们应该将0作为值类型的默认值. 枚举类型就是一种典型的情况.我们创建的枚举类型决不应该将0视为无效状态.我们知道,所有的枚举类型都继承自System.ValueType.默认的枚举值从0开始,但是我们可以更改这种默认行为. public enum Planet { // 显式赋值. // 否则将默认从0开始. Mercury = 1, Venus = 2,

值类型与引用类型的区别

值类型 基本类型  4类8种 整型  byte   字节型   1长度   0~255 short  短整型   2长度 int      整型      4长度 long   长整型   8长度 浮点型  float     单精度浮点型 double 双精度浮点型 字符型  char  单字符型  数据必须包裹在单引号之间 波尔型  bool  逻辑型  true 真.对 /  false  假.错 枚举类型 结构体 引用类型               字符串   string  多个字符

C# 引用类型和值类型

1.引用类型 FCL(Framework)中的大多数类型都是引用类型,引用类型总是在托管堆中分配的,C#的new操作符会返回对象的内存地址,也就是指对象数据的内存地址.在使用引用类型时,存在以下性能问题,这是我们在开发中必须要注意的: a.内存必须从托管堆上分配(也就是说每new一个对象,会占用内存,对象过多就会导致内存占用) b. 堆上分配的每个对象都有一些额外的成员,这些成员必须初始化 c.对象中的其他字节(为字段而设),总是设为0 d.从托管堆中每分配一个对象,可能强制执行一次垃圾回收操作

定义类+类实例化+属性+构造函数+匿名类型var+堆与栈+GC回收机制+值类型与引用类型

为了让编程更加清晰,把程序中的功能进行模块化划分,每个模块提供特定的功能,而且每个模块都是孤立的,这种模块化编程提供了非常大的多样性,大大增加了重用代码的机会. 面向对象编程也叫做OOP编程 简单来说面向对象编程就是结构化编程,对程序中的变量结构划分,让编程更清晰. 类的概念: 类实际上是创建对象的模板,每个对象都包含数据,并提供了处理和访问数据的方法. 类定义了类的每个对象(称为实例)可以包含什么数据和功能. 类中的数据和函数称为类的成员:数据成员        函数成员 数据成员: 数据成员