值类型的默认封送处理

本文详细介绍了结构化值类型在平台调用及COM互操作中的使用方法,包括如何指定值类型的布局信息,以及不同类型在托管与非托管环境中如何正确封送。

大多数值类型(如整数和浮点数)都是可直接复制到本机结构中的,且不需要进行封送处理。其他非直接复制到本机结构中的类型在托管和非托管内存中具有不同的表示形式,且需要进行封送处理。还有一些其他类型需要跨交互操作边界进行显式格式设置。

本主题提供下列有关格式化值类型的信息:

除描述格式化类型外,本主题还识别具有非常规封送处理行为的系统值类型

格式化类型是一种复杂类型,它包含显式控制其成员在内存中的布局的信息。成员布局信息是使用 StructLayoutAttribute 属性提供的。布局可以是下列 LayoutKind 枚举值之一:

  • LayoutKind.Automatic

    指示公共语言运行库可以随意重新排列该类型的成员的顺序以提高效率。但是,将值类型传递给非托管代码时,成员的布局是可预知的。视图自动封送这样的结构将导致异常。

  • LayoutKind.Sequential

    指示该类型的成员要在非托管内存中以它们在托管类型定义中出现的同一顺序布局。

  • LayoutKind.Explicit

    指示成员被根据随每个字段提供的 FieldOffsetAttribute 布局。

平台调用中使用的值类型

在下面的示例中,Point 和 Rect 类型使用 StructLayoutAttribute 提供成员布局信息。

C#
VB
using System.Runtime.InteropServices;
[StructLayout(LayoutKind.Sequential)]
public struct Point {
   public int x;
   public int y;
}   

[StructLayout(LayoutKind.Explicit)]
public struct Rect {
   [FieldOffset(0)] public int left;
   [FieldOffset(4)] public int top;
   [FieldOffset(8)] public int right;
   [FieldOffset(12)] public int bottom;
}

当封送到非托管代码时,这些格式化类型作为 C 样式的结构封送。这就提供了一种调用具有结构参数的非托管 API 的简单方法。例如,POINT 和 RECT 结构可以通过下面的方式传递给 Microsoft Win32 API PtInRect 函数:

BOOL PtInRect(const RECT *lprc, POINT pt);

可以使用下面的平台调用定义传递结构:

C#
VB
class Win32API {
   [DllImport("User32.dll")]
   public static extern Bool PtInRect(ref Rect r, Point p);
}

Rect 值类型必须通过引用传递,原因是非托管 API 要求将指向 RECT 的指针传递给该函数。而因为非托管 API 要求在堆栈上传递 POINT,因此 Point 值类型通过值传递。这种细微的差异极其重要。引用是作为指针传递给非托管代码的。值在堆栈上传递给非托管代码。

Note注意

当格式化的类型作为结构封送时,只有该类型内的字段是可访问的。如果该类型具有方法、属性或事件,将无法从非托管代码访问它们。

类还可以作为 C 样式的结构封送到非托管代码,条件是它们具有固定的成员布局。类的成员布局信息也是用 StructLayoutAttribute 属性提供的。具有固定布局的值类型与具有固定布局的类之间的主要差异在于将它们封送到非托管代码的方式。值类型通过值传递(在堆栈上),因此被调用方对类型的成员所做的任何更改对于调用方都是不可见的。引用类型通过引用传递(对类型的引用在堆栈上传递);因此,被调用方对类型的可直接复制到本机结构中的类型成员所做的所有更改对于调用方都是可见的。

Note注意

如果引用类型具有非直接复制到本机结构中的类型的成员,则需要进行两次转换:第一次在将参数传递到非托管端时进行,而第二次在从调用返回时进行。由于这项附加的开销,如果调用方希望看见被调用方所做的更改,则必须将 In/Out 参数显式应用于参数。

在下面的示例中,SystemTime 类具有连续的成员布局,并且可以传递给 Win32 API GetSystemTime 函数。

C#
VB
[StructLayout(LayoutKind.Sequential)]
   public class SystemTime {
   public ushort wYear; 
   public ushort wMonth;
   public ushort wDayOfWeek; 
   public ushort wDay; 
   public ushort wHour; 
   public ushort wMinute; 
   public ushort wSecond; 
   public ushort wMilliseconds; 
}

GetSystemTime 函数以下面的方式定义:

void GetSystemTime(SYSTEMTIME* SystemTime);

GetSystemTime 的等效平台调用定义如下所示:

C#
VB
class Win32API {
   [DllImport("Kernel32.dll", CharSet=CharSet.Auto)]
   public static extern void GetSystemTime(SystemTime st);
}

请注意,SystemTime 参数未类型化为引用参数,原因是 SystemTime 是类而不是值类型。与值类型不同,类始终要通过引用来传递。

下面的代码示例显示一个不同的 Point 类,它具有称为 SetXY 的方法。由于该类型具有连续布局,因此可将它传递给非托管代码并作为结构封送。但是,即使通过引用传递对象,仍然不能从非托管代码调用SetXY 成员。

C#
VB
[StructLayout(LayoutKind.Sequential)]
public class Point {
   int x, y;
   public void SetXY(int x, int y){ 
      this.x = x;
      this.y = y;
   }
}

COM Interop 中使用的值类型

还可以将格式化类型传递给 COM Interop 方法调用。事实上,当导出到类型库时,值类型被自动转换为结构。如下面的示例所示,Point 值类型变为名为 Point 的类型定义 (typedef)。类型库中其他地方的所有对 Point 值类型的引用都被替换为 Point typedef。

类型库表示形式

typedef struct tagPoint {
   int x;
   int y;
} Point;
interface _Graphics {
   …
   HRESULT SetPoint ([in] Point p)
   HRESULT SetPointRef ([in,out] Point *p)
   HRESULT GetPoint ([out,retval] Point *p)
}

在通过 COM 接口进行封送处理时,使用的规则与封送值和对平台调用的调用的引用时使用的规则相同。例如,将 Point 值类型的实例从 .NET Framework 传递给 COM 时,Point 通过值传递。如果 Point值类型通过引用传递,则指向 Point 的指针在堆栈上传递。Interop 封送拆收器不支持任何一个方向上的更高级别的间接寻址 (Point **)。

Note注意

将 LayoutKind 枚举值设置为 Explicit 的结构不能在 COM Interop 中使用,原因是导出的类型库不能表示显式布局。

系统值类型

System 命名空间具有若干个表示已装箱形式的运行库基元类型的值类型。例如,值类型 System.Int32 结构表示 ELEMENT_TYPE_I4 的已装箱形式。将这些类型以与封送它们所装箱的基元类型相同的方式封送,而不是像其他格式化类型那样作为结构封送。因此,System.Int32 被封送为 ELEMENT_TYPE_I4 而不是包含一个 long 类型的成员的结构。下表包含 System 命名空间中的值类型(它们是基元类型的装箱表示形式)的列表。

系统值类型 元素类型

System.Boolean

ELEMENT_TYPE_BOOLEAN

System.SByte

ELEMENT_TYPE_I1

System.Byte

ELEMENT_TYPE_UI1

System.Char

ELEMENT_TYPE_CHAR

System.Int16

ELEMENT_TYPE_I2

System.UInt16

ELEMENT_TYPE_U2

System.Int32

ELEMENT_TYPE_I4

System.UInt32

ELEMENT_TYPE_U4

System.Int64

ELEMENT_TYPE_I8

System.UInt64

ELEMENT_TYPE_U8

System.Single

ELEMENT_TYPE_R4

System.Double

ELEMENT_TYPE_R8

System.String

ELEMENT_TYPE_STRING

System.IntPtr

ELEMENT_TYPE_I

System.UIntPtr

ELEMENT_TYPE_U

System 命名空间中的其他一些值类型以不同的方式处理。由于非托管代码对这些类型有现成的格式,因此,封送拆收器具有特殊的用于封送它们的规则。下表列出 System 命名空间中的特殊值类型,以及将其封送到的非托管类型。

系统值类型 IDL 类型

System.DateTime

DATE

System.Decimal

DECIMAL

System.Guid

GUID

System.Drawing.Color

OLE_COLOR

下面的代码显示 Stdole2 类型库中的非托管类型 DATEGUIDDECIMAL 和 OLE_COLOR 的定义。

类型库表示形式

typedef double DATE;
typedef DWORD OLE_COLOR;

typedef struct tagDEC {
    USHORT    wReserved;
    BYTE      scale;
    BYTE      sign;
    ULONG     Hi32;
    ULONGLONG Lo64;
} DECIMAL;

typedef struct tagGUID {
    DWORD Data1;
    WORD  Data2;
    WORD  Data3;
    BYTE  Data4[ 8 ];
} GUID;

下面的代码显示托管 IValueTypes 接口中的相应定义。

C#
VB
public interface IValueTypes {
   void M1(System.DateTime d);
   void M2(System.Guid d);
   void M3(System.Decimal d);
   void M4(System.Drawing.Color d);
}

类型库表示形式

[…]
interface IValueTypes : IDispatch {
   HRESULT M1([in] DATE d);
   HRESULT M2([in] GUID d);
   HRESULT M3([in] DECIMAL d);
   HRESULT M4([in] OLE_COLOR d);
};

请参见



转自:http://msdn.microsoft.com/zh-cn/library/0t2cwe11(VS.80).aspx

<think> 嗯,用户想了解C#中值类型和引用类型的详细信息。根据之前的引用,用户提供的资料覆盖了基本概念、赋值差异、内存存储方式和实际应用场景。 先从内存分配机制切入比较合适。引用[2]提到值类型通常分配在线程栈上,而引用类型分配在托管堆上。这个差异是理解两类类型的基础,需要详细说明栈和堆的特性差异——栈的高效性与作用域局限,堆的灵活性与GC管理成本。 关于分类体系,需要整理完整的类型树。基础类型部分容易理解(int/bool等),但struct作为值类型这点容易被忽视(引用[2]特别标注),而enum本质是整数包装。引用类型中要强调string的特殊性——虽然是类但享不可变性,这点可以和用户后续讨论字符串驻留机制。 赋值差异的演示很关键。引用[4]的数组示例常典型,准备用这个案例说明浅复制特性。但引用[5]提醒要注意特殊字符串处理,不过用户当前的问题更关注基础概念,字符串优化可以放在进阶部分。 装箱拆箱机制是值类型转为引用类型的关键过程。引用[1]提到指针类型,但这个属于不安全代码范畴,用户当前问题可能不需要深入。不过要强调装箱的性能损耗,避免代码中出现意外装箱。 实际应用原则参考引用[3]很到位:值类型适合存储数据,引用类型适合定义行为。struct的轻量级特性适合小型数据结构,这点要结合内存布局说明——连续内存块对CPU缓存友好。 最后典型场景列举需要覆盖数据库实体、数学计算等值类型优势领域,以及业务逻辑类等引用类型场景。性能注意事项要特别标注struct的16字节原则,避免错误使用导致复制开销。 </think> 好的,我们来详细介绍一下 C# 中的值类型 (`Value Types`) 和引用类型 (`Reference Types`)。它们是 C# 类型系统的核心基础,理解它们的区别对于编写高效、正确的代码至关重要。 **核心区别:存储位置与赋值行为** 两者的根本区别在于 **变量存储的内容是什么** 以及 **赋值操作时发生了什么**: 1. **值类型 (`Value Types`):** * **存储内容:** 变量直接包含其**实际数据**。 * **赋值行为 (`=`):** 当将一个值类型变量赋值给另一个值类型变量时,发生的是 **值拷贝**。复制的是数据本身的一个**完整副本**。修改新变量的值**不会影响**原始变量的值。 * **内存位置:** 通常分配在**栈 (`Stack`)** 上(对于局部变量或字段)或作为引用类型的字段**内联 (`Inline`)** 存储在**托管堆 (`Managed Heap`)** 上。 * **特点:** * **高效访问:** 栈分配速度快,访问直接。 * **作用域绑定:** 栈上的变量生命周期与其声明的作用域(如方法调用)紧密绑定。 * **默认值:** 不能为 `null`(除是可空值类型 `Nullable<T>`),总是有默认值(如 `int` 默认为 `0`, `bool` 默认为 `false`)。 * **继承:** 所有值类型都隐式密封 (`sealed`),不能作为基类(但可以实现接口)。 * **派生来源:** 所有值类型直接或间接继承自 `System.ValueType`,而 `System.ValueType` 又继承自 `System.Object`。 2. **引用类型 (`Reference Types`):** * **存储内容:** 变量存储的是一个**引用 (`Reference`)** 或**指针 (`Pointer`)**,指向内存中某个位置,那里才是**实际数据**(对象实例)的所在地。 * **赋值行为 (`=`):** 当将一个引用类型变量赋值给另一个引用类型变量时,发生的是 **引用拷贝**。复制的是内存地址(指针),**而不是**对象数据本身。两个变量现在指向内存中**同一个对象实例**。通过任何一个变量修改对象的状态,都会反映在另一个变量上(因为它们指向同一个对象)。 * **内存位置:** 对象实例本身总是分配在**托管堆 (`Managed Heap`)** 上。变量(存储引用)本身存储在栈上或作为其他对象的字段存储在堆上。 * **特点:** * **支持多态:** 通过继承和虚方法实现多态。 * **可为 `null`:** 默认可以赋值为 `null`,表示不引用任何对象。 * **内存开销:** 堆分配需要管理(由垃圾回收器 `GC` 负责),对象本身有额外的内存开销(如类型对象指针、同步块索引等)。 * **派生来源:** 所有引用类型都继承自 `System.Object`。 **常见类型分类** | 类型 | 值类型 | 引用类型 | | -------------- | --------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------ | | **基本类型** | `bool`,`byte`,`sbyte`,`char`,`decimal`,`double`,`float`,`int`,`uint`,`long`,`ulong`,`short`,`ushort` | `string` (特殊:不可变), `object` | | **复合类型** | `struct` (结构体), `enum` (枚举) | `class` (类), `interface` (接口), `delegate` (委托), `dynamic` | | **数组** | 数组元素是值类型 (`int[]` 的元素是值类型) | 数组本身 (`int[]`, `string[]` 等声明) 是引用类型 | | **泛型类型参数** | 使用 `struct` 约束时 (`where T : struct`) | 使用 `class` 约束时 (`where T : class`)、默认或无约束时通常是引用类型 | **赋值行为示例 (关键区分点)** ```csharp // **值类型示例** int a = 5; // 变量 a 直接包含值 5 int b = a; // 值拷贝:将 a 的值 5 复制一份,赋值给 b。b 现在包含它自己的 5 b = 10; // 修改 b 的值,只影响 b 自己的数据 Console.WriteLine(a); // 输出 5 (a 的值不受 b 修改的影响) Console.WriteLine(b); // 输出 10 // **引用类型示例** int[] arr1 = new int[] { 666 }; // 在堆上创建一个数组对象 [666], arr1 存储指向它的引用 int[] arr2 = arr1; // 引用拷贝:将 arr1 存储的地址复制一份给 arr2。arr2 现在指向同一个数组对象 [666] arr1[0] = 888; // 通过引用 arr1 修改堆上数组对象的第一个元素为 888 Console.WriteLine(arr2[0]); // 输出 888 (因为 arr2 引用的是同一个对象!) Console.WriteLine(arr1[0]); // 输出 888 ``` **内存存储图示** ``` **值类型 (int a = 5; int b = a; b = 10;):** 栈: +------+ +------+ | a=5 | | b=10 | // a 和 b 各自拥有独立的内存空间存储它们的值 +------+ +------+ **引用类型 (int[] arr1 = new int[]{666}; int[] arr2 = arr1; arr1[0]=888;):** 栈: 托管堆: +---------+ +---------+ +-------------------+ | arr1 ---|---->| addr100 | | addr100: [888] | // 修改前是 [666] +---------+ +---------+ +-------------------+ | arr2 ---|-----^ // arr1 和 arr2 存储相同的地址(addr100), +---------+ // 指向堆上的同一个数组对象 ``` **相同点** 1. **继承自 Object:** 所有值类型和引用类型最终都继承自 `System.Object`。这意味着它们都可以调用 `ToString()`, `Equals()`, `GetHashCode()`, `GetType()` 等方法(尽管值类型可能需要装箱后才能通过基类引用调用,见下文)。 2. **可用作字段成员:** 都可以作为类 (`class`) 或结构体 (`struct`) 的成员字段。 3. **可传递性:** 都可以作为方法参数传递和返回值返回(值类型按值传递,引用类型按引用传递地址)。 **重要概念补充** 1. **装箱 (`Boxing`) 与拆箱 (`Unboxing`):** * **装箱:** 将值类型实例转换为 `object` 类型或该值类型实现的任何接口类型。过程:在堆上创建一个对象实例,将值类型数据**拷贝**到这个新对象中,然后返回这个新对象的引用。**有性能开销。** ```csharp int i = 123; // 值类型 object o = i; // 装箱:将 i 的值拷贝到堆上新对象,o 引用该对象 ``` * **拆箱:** 将 `object` 引用(或接口引用)转换回原始的值类型。过程:获取堆上对象中存储的值类型数据的**引用**,并将该值**拷贝**到栈上的值类型变量中。需要显式类型转换,如果类型不匹配会抛出 `InvalidCastException`。 ```csharp int j = (int)o; // 拆箱:检查 o 指向的对象确实是 int 的装箱实例,然后将其包含的值拷贝给 j ``` * **尽量避免:** 频繁的装箱和拆箱会影响性能,特别是在集合操作中(如早期的 `ArrayList`)。使用泛型集合(如 `List<int>`)可以避免值类型的装箱。 2. **`struct` (结构体) 作为值类型:** * 与 `class` 语法相似(可以有字段、属性、方法、构造函数等)。 * **关键区别:** * `struct` 是值类型,`class` 是引用类型。 * `struct` 不支持显式无参构造函数(编译器自动生成将所有字段设为默认值的无参构造器)。 * `struct` 在声明时字段不能直接初始化(必须在构造函数中初始化)。 * `struct` 不能作为其他类的基类,也不能被继承(隐式密封)。 * `struct` 更适合表示轻量级、行为简单(通常是不可变的)、数据聚合的对象。 * 当 `struct` 作为方法参数或返回值时,传递的是其内容的完整拷贝。如果 `struct` 很大,这可能会影响性能。通常建议 `struct` 的大小小于 16 字节。 3. **`string` (字符串) 的特殊性:** * `string` 是引用类型 (`class`)。 * **不可变性 (`Immutability`):** 一旦创建,`string` 对象的内容就不能更改。看似修改字符串的操作(如 `+`, `Substring`, `Replace`)实际上是创建了一个**新的** `string` 对象并返回其引用。原字符串对象保持不变(如果没有其他引用,将被 GC 回收)。 * **字符串驻留 (`String Interning`):** 编译器或运行时可能会将字面量相同(编译时已知)的字符串指向同一个内存对象,以节省内存。但这不应该依赖,运行时创建的相同内容的字符串不一定被驻留。使用 `string.Intern` 可以显式请求驻留。 * **比较:** 通常使用 `==` 或 `Equals` 比较字符串的内容(值语义),因为它们被重写了。比较引用使用 `Object.ReferenceEquals`。 **如何选择:值类型 vs 引用类型 (设计原则参考引用[3])** * **优先考虑定义 `class` (引用类型):** 尤其是当类型: * 代表具有复杂行为的事物。 * 需要支持继承和多态。 * 需要在多个地方共享对同一个实例的访问和修改。 * 逻辑上不应该被复制(如 `Stream`, `DbContext`)。 * 尺寸可能较大(避免拷贝开销)。 * **考虑定义 `struct` (值类型) 当:** * 类型主要表示一个简单的**数据聚合**(如坐标 `Point(x, y)`、复数 `Complex(real, imaginary)`)。 * 类型具有**值语义**:其逻辑相等性基于其所有字段的值是否相等(而不是引用地址),并且期望赋值或传递时创建**独立副本**。 * 类型应该是**不可变**的(所有字段 `readonly`,通过构造函数初始化)。 * 类型实例**尺寸很小**(通常小于 16 字节),避免大量拷贝时的性能问题。 * 类型不需要从其他类型继承(除了接口),也不需要作为其他类型的基类。 * **其他类型:** 使用预定义的基本值类型 (`int`, `double` 等) 或 `enum`。接口 (`interface`) 和委托 (`delegate`) 始终是引用类型。 **典型应用场景** * **值类型:** * 数学计算(坐标、向量、矩阵、复数)。 * 表示离散值(日期 `DateTime`、时间跨度 `TimeSpan`、枚举)。 * 小型、轻量的数据结构(如配置项、键值对 `KeyValuePair<TKey, TValue>`)。 * 数据库实体中的主键类型 (`int`, `Guid`)。[^2] * **引用类型:** * 用户界面控件 (`Button`, `TextBox`)。 * 业务逻辑对象 (`Customer`, `Order`)。 * 集合 (`List<T>`, `Dictionary<TKey, TValue>`)。 * 流 (`FileStream`, `MemoryStream`)。 * 数据库上下文 (`DbContext`)。 * 委托 (`Action`, `Func`) 用于事件和回调。 * 字符串 (`string`) 的处理。 **性能注意事项** * **值类型:** 栈分配快,访问快(无需指针跳转),但大尺寸的值类型在传递赋值时拷贝开销大。避免大的 `struct`。 * **引用类型:** 堆分配和回收(GC)有开销,访问数据需要指针跳转(一次间接寻址),但赋值和传递引用代价小(只是一个指针拷贝)。修改共享对象需注意线程安全。 * **装箱/拆箱:** 尽量避免,特别是在循环或高频调用中,会产生额外的内存分配和拷贝。 **总结** 理解值类型和引用类型的区别是编写高效、正确 C# 程序的基础。牢记: * **值类型 (`Value Types`):** 变量存**数据**,赋值是**拷贝数据**。通常栈分配或内联在堆中。修改副本不影响原件。 * **引用类型 (`Reference Types`):** 变量存**引用(地址)**,赋值是**拷贝引用**。实例在**堆**中。通过任何引用修改对象,所有引用都可见。 * **选择依据:** 优先 `class` 定义行为和多态;考虑 `struct` 定义小型、不可变的数据聚合。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值