c#中的引用类型和值类型有啥区别?
Posted
技术标签:
【中文标题】c#中的引用类型和值类型有啥区别?【英文标题】:What is the difference between a reference type and value type in c#?c#中的引用类型和值类型有什么区别? 【发布时间】:2011-06-30 17:54:30 【问题描述】:几个月前有人问过我这个问题,我无法详细解释。 C#中引用类型和值类型有什么区别?
我知道值类型是int
、bool
、float
等,而引用类型是delegate
、interface
等。或者这也错了吗?
你能用专业的方式给我解释一下吗?
【问题讨论】:
作为一个小提示,我认为这个问题是关于 C# 的,但实际上它是关于 C# + .NET 的。不分析 .NET 就无法分析 C#。我不会重新标记这个问题,因为在分析一个而不分析另一个(迭代器和闭包,我在看你)时可能会有一些要点 @xanatos 这是一个关于 C#、VB.Net 和 ,Net 共同点的 CLI 的最恰当的问题。 CLI 应该有一个标签,但 CLI 被用于其他内容。有 CLR,但那是一种实现,而不是标准。 【参考方案1】:您的示例有点奇怪,因为虽然 int
、bool
和 float
是特定类型,但接口和委托是 种类 类型 - 就像 struct
和 enum
是各种值类型。
我已经写了一个对引用类型和值类型in this article 的解释。我很乐意对您感到困惑的任何内容进行扩展。
“TL;DR”版本是考虑特定类型的变量/表达式的值是什么。对于值类型,值就是信息本身。对于引用类型,该值是一个引用,它可能为 null,也可能是导航到包含信息的对象的一种方式。
例如,将变量想象成一张纸。它可以写有值“5”或“false”,但它不能有我的房子……它必须有到我家的路线。这些方向相当于参考。特别是,两个人可能有不同的纸片,其中包含相同的指向我家的方向 - 如果一个人按照这些指示将我的房子涂成红色,那么第二个人也会看到这种变化。如果他们俩都在纸上分别图片我的房子,那么一个人给他们的纸上色根本不会改变另一个人的纸。
【讨论】:
重要的是要注意,事物可以提供三种不同的主要语义类型:不可变语义、可变值语义和可变引用语义。从概念上讲,事物实现的语义类型与它是否存储为独立的堆对象或变量/字段(结构)是正交的。在实践中,虽然不公开其字段的结构可以实现任何类型的语义,但 .net 允许混杂共享堆引用这一事实意味着堆对象无法实现可变值语义。 我没有得到这个位 -while int, bool and float are specific types, interfaces and delegates are kinds of type - just like struct and enum are kinds of value types
。 int, bool 是特定类型是什么意思? C# 中的所有内容,例如int、bool、float、class、interface、delegate 是一种类型(准确地说是数据类型)。数据类型在 C# 中分为“引用类型”和“值类型”。那你为什么说 int 是特定类型,而 interface 是 kind 类型呢?
@RBT:数据类型不只是分为“引用类型”和“值类型”。它们也被分为“类、结构、枚举、委托、接口”。 int
是一个结构,string
是一个类,Action
是一个委托,等等。您的“int、bool、float、class、interface、delegate”列表是一个包含不同种类事物的列表,在就像“10, int”是一个包含不同类型事物的列表一样。
@JonSkeet 可能this post 上的答案有点误导。
@RBT:我会说它的措辞有些糟糕,但并不糟糕。【参考方案2】:
值类型:
保存一些值而不是内存地址
示例:
结构
存储:
TL;DR:变量的值存储在它被清除的任何地方。例如,局部变量存在于堆栈中,但当在类中声明为成员时,它存在于与声明它的类紧密耦合的堆中。更长:因此值类型存储在任何声明它们的地方。
例如:函数内部的int
的值作为局部变量将存储在堆栈中,而声明为类中成员的int
的值将与声明的类一起存储在堆中类上的值类型具有与声明它的类完全相同的生命类型,几乎不需要垃圾收集器的工作。不过它更复杂,我会参考@JonSkeet 的书“C# In Depth”或他的文章“Memory in .NET”以获得更简洁的解释。
优点:
值类型不需要额外的垃圾回收。它与它所在的实例一起收集垃圾。方法中的局部变量在方法离开时被清理。
缺点:
当将大量值传递给方法时,接收变量实际上会复制,因此内存中有两个冗余值。
由于错过了课程。它失去了所有 oop 的好处
参考类型:
保存一个值而不是值的内存地址
示例:
类
存储:
存储在堆上
优点:
当您将引用变量传递给方法并且它发生更改时,它确实会更改原始值,而在值类型中,会获取给定变量的副本并且该值会更改。
当变量的大小越大时,引用类型越好
由于类作为引用类型变量出现,它们提供了可重用性,从而有利于面向对象的编程
缺点:
读取垃圾收集器的 value.extra 重载时分配和取消引用时的更多工作引用
【讨论】:
引用类型不一定会存储在堆上,而值类型不一定会存储在堆栈上。如果您想了解更多信息,请阅读yoda.arachsys.com/csharp/memory.html。 这个答案有很多误解。请通过 C# 阅读 Jeff Richters CLR。值类型存储在线程堆栈中,不受垃圾回收 (GC) 的影响——它们与 GC 无关。引用类型存储在托管堆上,因此受到 GC 的约束。如果一个 Ref 类型有一个根引用,它就不能被收集并被提升到 0、1 和 2 代。如果它没有根引用,它可以被垃圾收集,然后它会经历这个称为复活的过程被杀死并复活,然后最终被收集。【参考方案3】:如果您知道计算机如何在内存中分配内容并知道指针是什么,我发现更容易理解两者的区别。
引用通常与指针相关联。这意味着您的变量所在的内存地址实际上将实际对象的另一个内存地址保存在不同的内存位置。
我即将给出的例子过于简单,所以请谨慎对待。
想象一下计算机内存是一排一排的邮政信箱(从邮政信箱 0001 到邮政信箱 n),可以在其中存放一些东西。如果邮政信箱不适合您,请尝试使用哈希表或字典或数组或类似的东西。
因此,当您执行以下操作时:
var a = "你好";
计算机将执行以下操作:
-
分配内存(比如从内存位置 1000 开始 5 个字节)并放入 H(在 1000)、e(在 1001)、l(在 1002)、l(在 1003)和 o(在 1004)。
在内存中分配某处(例如位置 0500)并将其分配为变量 a。
所以它有点像一个别名(0500 是一个)。
将该内存位置 (0500) 处的值分配给 1000(这是字符串 Hello 在内存中开始的位置)。因此,变量 a 持有对“Hello”字符串的实际起始内存位置的 reference。
值类型将在其内存位置保存实际的东西。
因此,当您执行以下操作时:
var a = 1;
计算机将执行以下操作:
-
在 0500 分配一个内存位置并将其分配给变量 a(相同的别名)
将值 1 放入其中(在内存位置 0500)。
请注意,我们没有分配额外的内存来保存实际值 (1)。
因此 a 实际上持有实际值,这就是它被称为值类型的原因。
【讨论】:
您可能对blogs.msdn.com/b/ericlippert/archive/2009/02/17/…感兴趣 @Jon,好吧,我说的话有点无效,哈哈。但就像我说的那样,在这两种类型之间获得一些理解是非常简单的,在我的情况下,我发现这很有帮助。至少我是这么想的:)。 @JonSkeet 链接失效了,你有工作的吗? @FLonLon:是的,这是一个新链接:docs.microsoft.com/en-us/archive/blogs/ericlippert/…【参考方案4】:这是我大约两年前在另一个论坛上发的帖子。虽然语言是 vb.net(而不是 C#),但值类型与引用类型的概念在整个 .net 中是统一的,并且示例仍然有效。
记住,在 .net 中,所有类型在技术上都派生自基本类型 Object,这一点也很重要。值类型被设计成这样,但最终它们也继承了基类型 Object 的功能。
A.值类型只是-它们代表内存中存储离散值的不同区域。值类型具有固定的内存大小并存储在堆栈中,堆栈是固定大小的地址的集合。
当你做出这样的声明时:
Dim A as Integer
DIm B as Integer
A = 3
B = A
您已完成以下操作:
-
在内存中创建了 2 个足以容纳 32 位整数值的空间。
将值 3 放入分配给 A 的内存分配中
将值 3 放入分配给 B 的内存分配中,方法是为其分配与 A 中保存的相同的值。
每个变量的值离散地存在于每个内存位置。
B.引用类型可以有各种大小。因此,它们不能存储在“堆栈”中(记住,堆栈是固定大小的内存分配的集合吗?)。它们存储在“托管堆”中。指向托管堆上每个项目的指针(或“引用”)都保存在堆栈中(就像地址一样)。您的代码使用堆栈中的这些指针来访问存储在托管堆中的对象。因此,当您的代码使用引用变量时,它实际上是在使用指向托管堆中内存位置的指针(或“地址”)。
假设您创建了一个名为 clsPerson 的类,带有一个字符串 Property Person.Name
在这种情况下,当你做出这样的陈述时:
Dim p1 As clsPerson
p1 = New clsPerson
p1.Name = "Jim Morrison"
Dim p2 As Person
p2 = p1
在上面的例子中,p1.Name 属性将返回“Jim Morrison”,正如您所期望的那样。 p2.Name 属性也将返回“Jim Morrison”,正如您直觉所期望的那样。我相信 p1 和 p2 都代表堆栈上的不同地址。但是,既然您已经为 p2 分配了 p1 的值,那么 p1 和 p2 都指向托管堆上的相同位置。
现在考虑这种情况:
Dim p1 As clsPerson
Dim p2 As clsPerson
p1 = New clsPerson
p1.Name = "Jim Morrison"
p2 = p1
p2.Name = "Janis Joplin"
在这种情况下,您在托管堆上创建了 person 类的一个新实例,并在堆栈上使用指针 p1 来引用该对象,并再次为对象实例的 Name 属性分配一个值“Jim Morrison” .接下来,您在堆栈中创建了另一个指针 p2,并将其指向托管堆上与 p1 引用的地址相同的地址(当您进行分配 p2 = p1 时)。
转折来了。当您将 p2 的 Name 属性赋值为“Janis Joplin”时,您正在更改 p1 和 p2 都引用的对象的 Name 属性,这样,如果您运行以下代码:
MsgBox(P1.Name)
'Will return "Janis Joplin"
MsgBox(p2.Name)
'will ALSO return "Janis Joplin"Because both variables (Pointers on the Stack) reference the SAME OBJECT in memory (an Address on the Managed Heap).
这有意义吗?
最后。如果你这样做:
DIm p1 As New clsPerson
Dim p2 As New clsPerson
p1.Name = "Jim Morrison"
p2.Name = "Janis Joplin"
您现在有两个不同的 Person 对象。但是,在您再次执行此操作的那一刻:
p2 = p1
您现在已将两者都指向“Jim Morrison”。 (我不确定 p2 引用的堆上的对象发生了什么......我认为它现在已经超出了范围。这是希望有人可以让我直截了当的那些领域之一......)。 -编辑:我相信这就是为什么在进行新分配之前你会设置 p2 = Nothing OR p2 = New clsPerson。
再一次,如果你现在这样做:
p2.Name = "Jimi Hendrix"
MsgBox(p1.Name)
MsgBox(p2.Name)
两个 msgBoxes 现在都将返回“Jimi Hendrix”
这可能有点令人困惑,我会在最后一次说,我可能有一些细节错误。
祝你好运,希望比我更了解的其他人会来帮助澄清其中的一些问题。 . .
【讨论】:
至于值类型与引用类型的概念在整个.net中是统一的,,它们实际上是在公共语言基础设施(CLI)规范中定义的,Ecma 标准 335(也ISO 标准)。这是.Net 标准部分的标准。 Ecma 标准 334(也是 ISO 标准)是 C# 语言,它明确指出 C# 实现必须依赖 CLI 或支持获得此 C# 标准所需的最低 CLI 功能的替代方法 .然而 VB.Net 不是标准,它是 Microsoft 专有的。【参考方案5】:值数据类型和引用数据类型
1) 值(直接包含数据) 但 参考(指数据)
2) in value(每个变量都有自己的副本) 但是 in reference(多个变量可以引用一些对象)
3) 在value中(操作变量不能影响其他变量) 但 在参考(变量可以影响其他)
4) 值类型是(int, bool, float) 但 引用类型是(数组、类对象、字符串)
【讨论】:
【参考方案6】:值类型:
固定内存大小。
存储在堆栈内存中。
保持实际值。
Ex. int、char、bool 等...
参考类型:
内存不固定。
存储在堆内存中。
保存实际值的内存地址。
例如字符串、数组、类等...
【讨论】:
【参考方案7】:“基于值类型的变量直接包含值。将一个值类型变量分配给另一个值类型变量会复制包含的值。这与引用类型变量的分配不同,它复制对对象的引用而不是对象本身。 "来自微软的图书馆。
你可以找到更完整的答案here和here。
【讨论】:
我不喜欢这种解释,因为听起来赋值对引用类型和值类型的工作方式不同。它没有。在这两种情况下,它都使“目标”变量的值等于表达式 - 值被复制。 区别 在于该值是什么 - 对于引用类型,被复制的值是一个引用。不过,这仍然是变量的值。 我同意你的观点,我已经知道它可能会有所不同,正如你可以在article 中看到的那样。但是,我只是重温了微软关于该主题的指南以及您通常如何阅读书籍。请不要怪我! :) 哦,当然……有大量的 MSDN 文档可以找到错误:)【参考方案8】:有时解释对初学者没有帮助。您可以将值类型想象为数据文件,将引用类型想象为文件的快捷方式。
因此,如果您复制引用变量,您只会将链接/指针复制到内存中某处的真实数据。如果你复制一个值类型,你就真的克隆了内存中的数据。
【讨论】:
【参考方案9】:这在深奥的方面可能是错误的,但是,为了简单起见:
值类型是通常“按值”传递的值(因此复制它们)。引用类型是“通过引用”传递的(因此给出了指向原始值的指针)。 .NET ECMA 标准不能保证这些“东西”的保存位置。您可以构建一个无堆栈或无堆的 .NET 实现(第二种会非常复杂,但您可能可以使用光纤和许多堆栈)
结构是值类型(int、bool...是结构,或者至少被模拟为...),类是引用类型。
值类型源自 System.ValueType。引用类型来自 System.Object。
现在.. 最后你有值类型,“引用的对象”和引用(在 C++ 中它们将被称为指向对象的指针。在 .NET 中它们是不透明的。我们不知道它们是什么。从我们的观点来看看来它们是对象的“句柄”)。这些楦型类似于值类型(它们通过副本传递)。所以一个对象是由对象(一个引用类型)和零个或多个对它的引用(类似于值类型)组成的。当引用为零时,GC 可能会收集它。
通常(在 .NET 的“默认”实现中),值类型可以放在堆栈上(如果它们是本地字段)或堆上(如果它们是类的字段,如果它们是迭代器函数,如果它们是闭包引用的变量,如果它们是异步函数中的变量(使用较新的 Async CTP)...)。引用的值只能进入堆。引用使用与值类型相同的规则。
在堆上的值类型的情况下,因为它们在迭代器函数、异步函数或被闭包引用,如果你观察编译的文件,你会看到编译器创建了一个类来把这些变量放好,调用函数的时候类就建好了。
现在,我不知道如何写长篇大论,而且我的生活中有更好的事情要做。如果您想要“精确”“学术”“正确”版本,请阅读以下内容:
http://blogs.msdn.com/b/ericlippert/archive/2010/09/30/the-truth-about-value-types.aspx
我正在寻找它 15 分钟!它比 msdn 版本更好,因为它是一篇浓缩的“即用型”文章。
【讨论】:
这不仅仅是深奥的错误。我想说的是根本上是错误的——因为引用类型的值仍然是按值传递的;只是值是引用,而不是对象。见pobox.com/~skeet/csharp/parameters.html。哦,局部变量也可以在堆上结束,例如,如果它们被捕获或者是迭代器块的一部分。 迭代器块被转换为类,所以“在你后面”它们是“类的字段”。闭包也一样。是的...我忘了写“指针”(引用)和“指向”之间的区别 @xanatos:当然,它们是编译后的类的字段 - 但它们仍然是源代码中的局部变量。我也不会将引用本身称为“值类型” - 我想我知道您来自哪里,但我认为以这种方式混淆水域不是一个好主意。 @jon 是的...它们是第三种类型,因为指针在 .net 中是“不透明的”,并且它们不是从 ValueType 派生的。但它们更类似于值类型而不是引用。您可以“引用”和“排除”它们。我不得不搅浑水,因为“有人”不得不挑剔迭代器的工作。 查看我现在指向的那篇文章,我发现:“值分为三种:(1)值类型的实例,(2)引用类型的实例,以及(3)引用。(C# 中的代码不能直接操作引用类型的实例;它总是通过引用进行。在不安全的代码中,指针类型被视为值类型,以确定其值的存储要求。)"。【参考方案10】:考虑引用类型的最简单方法是将它们视为“对象 ID”;对对象 ID 唯一可以做的事情是创建一个、复制一个、查询或操作一个的类型,或者比较两个是否相等。尝试使用 object-ID 执行任何其他操作将被视为使用该 id 引用的对象执行指示的操作的简写。
假设我有两个 Car 类型的变量 X 和 Y——一个引用类型。 Y 恰好持有“对象 ID #19531”。如果我说“X=Y”,那将导致 X 持有“对象 ID #19531”。请注意,X 和 Y 都没有汽车。这辆车,也称为“对象 ID #19531”,存储在其他地方。当我将 Y 复制到 X 时,我所做的只是复制 ID 号。现在假设我说 X.Color=Colors.Blue。这样的语句将被视为查找“对象 ID#19531”并将其涂成蓝色的指令。请注意,即使 X 和 Y 现在指的是蓝色汽车而不是黄色汽车,该语句实际上并不影响 X 或 Y,因为两者仍然引用“对象 ID #19531”,它仍然是同一辆车一直都是。
【讨论】:
【参考方案11】:变量类型和参考值易于应用并很好地应用于领域模型,方便开发过程。
为了消除关于“价值类型”数量的任何神话,我将评论平台上如何处理这一点。 NET,特别是在C#(CSharp)中调用APIS并按值、按引用发送参数时,在我们的方法和函数中以及如何正确处理这些值的段落。
阅读这篇文章 Variable Type Value and Reference in C #
【讨论】:
这是一个只有英文的问答网站,很遗憾=\。不过,感谢您尝试回答。请创建完整的答案,链接仅作为辅助(但不是完整的持续答案)。请看how to answer。【参考方案12】:假设v
是值类型表达式/变量,r
是引用类型表达式/变量
x = v
update(v) //x will not change value. x stores the old value of v
x = r
update(r) //x now refers to the updated r. x only stored a link to r,
//and r can change but the link to it doesn't .
因此,值类型变量存储实际值(5,或“h”)。引用类型变量仅存储指向值所在的隐喻框的链接。
【讨论】:
【参考方案13】:在解释 C# 中可用的不同数据类型之前,重要的是要提到 C# 是一种强类型语言。这意味着每个变量、常量、输入参数、返回类型以及通常每个计算结果为值的表达式都有一个类型。
每种类型都包含编译器将作为元数据嵌入到可执行文件中的信息,公共语言运行时 (CLR) 将使用这些信息在分配和回收内存时确保类型安全。
如果您想知道特定类型分配了多少内存,可以使用 sizeof 运算符,如下所示:
static void Main()
var size = sizeof(int);
Console.WriteLine($"int size:size");
size = sizeof(bool);
Console.WriteLine($"bool size:size");
size = sizeof(double);
Console.WriteLine($"double size:size");
size = sizeof(char);
Console.WriteLine($"char size:size");
输出将显示每个变量分配的字节数。
int size:4
bool size:1
double size:8
char size:2
每种类型的相关信息有:
所需的存储空间。 最大值和最小值。例如,Int32 类型接受 2147483648 和 2147483647 之间的值。 它继承自的基类型。 在运行时分配变量内存的位置。 允许的操作种类。类型包含的成员(方法、字段、事件等)。例如,如果我们检查类型 int 的定义,我们会发现以下结构和成员:
namespace System
[ComVisible(true)]
public struct Int32 : IComparable, IFormattable, IConvertible, IComparable<Int32>, IEquatable<Int32>
public const Int32 MaxValue = 2147483647;
public const Int32 MinValue = -2147483648;
public static Int32 Parse(string s, NumberStyles style, IFormatProvider provider);
...
内存管理 当多个进程在操作系统上运行并且 RAM 的数量不足以容纳所有进程时,操作系统会将部分硬盘映射到 RAM 并开始在硬盘中存储数据。操作系统将使用虚拟地址映射到其对应物理地址的特定表来执行请求。这种管理内存的能力称为虚拟内存。
在每个进程中,可用的虚拟内存分为以下 6 个部分,但为了本主题的相关性,我们将仅关注堆栈和堆。
堆栈 栈是一种 LIFO(后进先出)数据结构,大小取决于操作系统(默认情况下,对于 ARM、x86 和 x64 机器,Windows 预留 1MB,而 Linux 根据版本预留 2MB 到 8MB )。
这部分内存由 CPU 自动管理。每次函数声明一个新变量时,编译器都会在堆栈上分配一个与其大小一样大的新内存块,当函数结束时,该变量的内存块被释放。
堆 这个内存区域不是由 CPU 自动管理的,它的大小比堆栈大。当调用 new 关键字时,编译器开始寻找适合请求大小的第一个空闲内存块。当它找到它时,使用内置的 C 函数 malloc() 将其标记为保留,并返回指向该位置的指针。也可以使用内置的 C 函数 free() 释放一块内存。这种机制会导致内存碎片,并且必须使用指针来访问正确的内存块,它比堆栈执行读/写操作要慢。
自定义和内置类型 虽然 C# 提供了一组表示整数、布尔值、文本字符等的标准内置类型,但您可以使用结构、类、接口和枚举等构造来创建自己的类型。
使用 struct 构造的自定义类型的示例是:
struct Point
public int X;
public int Y;
;
值和引用类型 我们可以将 C# 类型分为以下几类:
值类型 引用类型值类型 值类型派生自 System.ValueType 类,并且此类型的变量在堆栈中的内存分配中包含它们的值。值类型的两大类是struct和enum。
以下示例显示了布尔类型的成员。如您所见,没有显式引用 System.ValueType 类,这是因为该类是由结构继承的。
namespace System
[ComVisible(true)]
public struct Boolean : IComparable, IConvertible, IComparable<Boolean>, IEquatable<Boolean>
public static readonly string TrueString;
public static readonly string FalseString;
public static Boolean Parse(string value);
...
参考类型 另一方面,引用类型不包含存储在变量中的实际数据,而是存储值的堆的内存地址。引用类型的类别有类、委托、数组和接口。
在运行时,当引用类型变量被声明时,它包含值 null,直到使用关键字 new 创建的对象被分配给它。
以下示例显示了泛型类型 List 的成员。
namespace System.Collections.Generic
[DebuggerDisplay("Count = Count")]
[DebuggerTypeProxy(typeof(Generic.Mscorlib_CollectionDebugView<>))]
[DefaultMember("Item")]
public class List<T> : IList<T>, ICollection<T>, IEnumerable<T>, IEnumerable, IList, ICollection, IReadOnlyList<T>, IReadOnlyCollection<T>
...
public T this[int index] get; set;
public int Count get;
public int Capacity get; set;
public void Add(T item);
public void AddRange(IEnumerable<T> collection);
...
如果您想找出特定对象的内存地址,System.Runtime.InteropServices 类提供了一种从非托管内存访问托管对象的方法。在下面的示例中,我们将使用静态方法 GCHandle.Alloc() 为字符串分配句柄,然后使用 AddrOfPinnedObject 方法检索其地址。
string s1 = "Hello World";
GCHandle gch = GCHandle.Alloc(s1, GCHandleType.Pinned);
IntPtr pObj = gch.AddrOfPinnedObject();
Console.WriteLine($"Memory address:pObj.ToString()");
输出将是
Memory address:39723832
参考文献 官方文档:https://docs.microsoft.com/en-us/cpp/build/reference/stack-stack-allocations?view=vs-2019
【讨论】:
【参考方案14】:标准中明确说明了值类型和引用类型之间差异的许多小细节,其中一些不易理解,尤其是对于初学者。
请参阅ECMA 标准 33,通用语言基础架构 (CLI)。 CLI 也由 ISO 标准化。我会提供参考,但对于 ECMA,我们必须下载 PDF,并且该链接取决于版本号。 ISO 标准需要花钱。
一个区别是值类型可以装箱,但引用类型通常不能。也有例外,但它们的技术含量很高。
值类型不能有无参数的实例构造函数或终结器,它们不能引用自己。例如,引用自身意味着如果存在值类型 Node,则 Node 的成员不能是 Node。我认为规范中还有其他要求/限制,但如果是这样,那么它们就不会集中在一个地方。
【讨论】:
以上是关于c#中的引用类型和值类型有啥区别?的主要内容,如果未能解决你的问题,请参考以下文章