为啥可以在没有 new 关键字的情况下实例化结构?
Posted
技术标签:
【中文标题】为啥可以在没有 new 关键字的情况下实例化结构?【英文标题】:Why is it possible to instantiate a struct without the new keyword?为什么可以在没有 new 关键字的情况下实例化结构? 【发布时间】:2011-12-07 17:38:40 【问题描述】:为什么我们不强制实例化一个结构,就像使用一个类时一样?
【问题讨论】:
有例外。如果您有一个在结构中带有参数的构造函数,则需要使用“new”。 @thewpfguy:在 C# 中,myVar=new MyStruct(5)
;` 大致相当于MyStruct temp; myStruct.ctor(out temp, 5); myVar = temp;
或myStruct..ctor(ref myVar, 5);
,编译器在认为时使用后者(更快) 相当于前者。在 vb.net 中,它相当于myVar = new MyStruct; myStruct..ctor(myVar, 5);
。在任何情况下,new
语法都不负责在myVar
中创建结构实例。
@ColonelPanic default(T)
【参考方案1】:
为什么我们不用像使用类一样强制实例化一个带有“new”的结构?
当你“新建”一个引用类型时,会发生三件事。首先,内存管理器从长期存储中分配空间。其次,对该空间的引用被传递给初始化实例的构造函数。第三,将该引用传回给调用者。
当您“新建”一个值类型时,会发生三件事。首先,内存管理器从短期存储中分配空间。其次,向构造函数传递对短期存储位置的引用。构造函数运行后,将短期存储位置中的值复制到该值的存储位置,无论该位置发生在何处。请记住,值类型的变量存储实际值。
(请注意,如果编译器可以确定这样做永远不会向用户代码公开部分构造的结构,则允许编译器将这三个步骤优化为一个步骤。也就是说,编译器可以生成简单地传递引用的代码到构造函数的final存储位置,从而保存一个分配和一个副本。)
所以现在我们可以解决您的问题,实际上您已经反问过了。最好问一下:
为什么我们不得不用“new”来分配一个类,而不是像使用结构一样简单地初始化字段?
由于列表中的这三件事,您必须使用“新”分配一个类。您需要从长期存储中分配的新内存,并且需要将对该存储的引用传递给构造函数。 “新”是知道如何做到这一点的操作员。
您不必在结构上调用“new”,因为不需要分配“最终”存储; 最终存储已存在。新值将被传送到某处,并且您已经通过其他方式获得了该存储空间。 值类型不需要重新分配;他们只需要初始化。您需要做的就是确保存储已正确初始化,而且您通常可以在不调用构造函数的情况下做到这一点。 这样做当然意味着您冒着拥有值类型变量的风险,该变量可以被用户代码观察到处于部分初始化状态。
总结:调用一个ctor对于值类型来说是可选的,因为在初始化一个值类型的实例时不需要分配新的内存并且因为跳过构造函数调用意味着你得到跳过短期分配和副本。您为提高性能付出的代价是用户代码可以看到部分初始化的结构。
【讨论】:
我明白在创建例如某个值类型的数组,或具有值类型字段的结构或类,数组、类、结构的所有项在构造函数被调用之前都将存在。另一方面,我很好奇在允许结构声明常量字段初始化值时会存在哪些基本困难(如果有的话)?这意味着它将复制一个常量模板,而不是用零填充结构。例如,合法值 -1000000000 到 1000000000 的字段可以初始化为 -2147483648。 惊人的答案。我也将其作为here 的答案(希望得到您的许可)。 是否应该尽可能跳过构造函数调用以避免短期分配和复制? 我可以假设“长期”是指堆,“短期”是指堆栈吗? @JamesPoulose:C# 或 CLR 的实现不需要使用堆进行长期存储,也不需要使用堆栈进行短期存储。例如,一些短期存储存储在寄存器中;寄存器既不是堆也不是栈。你为什么要做出这样的假设?更重要的是:如果违反该假设,您打算编写哪段代码错误?【参考方案2】:为什么很简单 - 因为规范是这么说的。 如何是确保整个内存块被“明确分配”的问题,这意味着:为结构的每个字段分配一个值。然而,这需要 2 件讨厌的事情:
公共字段(几乎总是坏的) 可变字段(通常在结构中不好)所以在大多数最佳实践案例中,您确实需要使用new(...)
语法来调用构造函数(或将内存归零,对于无参数构造函数)正确的类型。
【讨论】:
无论您是否调用构造函数,内存都会自动归零。根据定义,new StructType()
与 default(StructType)
相同。 “因为规范是这样说的”并不是真正的原因。从规范中获取的重要信息是内存自动归零。
@Zenexer 你能指出我的规范参考吗?对于字段:当然 - 但通常不适用于结构;它们具有相同的明确分配规则,但有一种特殊情况,即如果您分别写入所有字段,这也算作分配。在较低级别,有一个标志控制方法的堆栈空间是否为零;目前编译器确实设置了这个标志,但这是一个实现细节(不是语言之一);明确分配的规则意味着它不是绝对必要的
在沙盒程序中测试两者是否相等即可。我不太确定您想要规范参考的哪一部分;我只是指出你的结构。这就是结构被描述为如何工作的方式。
@MarcGravell:必须明确分配所有结构成员,然后才能将结构作为out
参数传递给外部代码(因为外部代码可能将out
和ref
参数视为等效) . C# 必须确保在将结构作为 out
参数传递之前未写入的任何成员都将被清零,因为无法保证外部代码不会尝试读取它们。
@supercat 确实 - 没有惊喜【参考方案3】:
因为结构是值类型。当你声明它的一个变量时,这个实例就在那里。
因此,构造函数(new
运算符)对于结构来说是可选的。
考虑
struct V public int x;
class R public int y = 0;
void F()
V a; // a is an instance of V, a.x is unassigned
R b; // b is a reference to an R
a.x = 1; // OK, the instance exists
//b.y = 2; // error, there is no instance yet
a = new V(); // overwrites the memory of 'a'. a.x == 0
b = new R(); // allocates new memory on the Heap
b.y = 2; // now this is OK, b points to an instance
【讨论】:
好吧,只有部分在那里;如果您在a.x = 1;
行中添加Console.WriteLine(a.x);
above,它将无法编译。
@Marc:正确但与我想说的没有直接关系。 a.x
存在但未明确分配。
我不知道这是否是因为可能是更高版本的 c# 但是....您的第 1 行无法编译,结构中不能有实例属性或字段初始值设定项
@Stark - 不确定这是否有效,我将删除 = 0;
。 a.x
将以“未明确分配”开头。【参考方案4】:
因为结构是值类型,类是引用类型。所以结构与 int、double 等属于同一类别。
【讨论】:
【参考方案5】:一年半之后...
struct
通常按值传递,而class
总是按引用传递。您可能对通过引用传递对象时会发生什么有很好的理解。当一个对象通过值传递时,它的内容,而不是对该对象的引用,被传递。对程序员来说,看起来好像制作了对象的浅拷贝。改变一个实例不会改变另一个。
所有变量(包括字段和属性)只要存在,就总是为它们分配空间。需要注意的是,local 变量只有在较新版本的 C# 中为它们分配值时才存在。在class
类型变量的情况下,分配的空间将包含对对象内容的引用。对于struct
类型的变量,分配的空间将包含对象的实际内容。
所以,假设您有一个“空”class
-type 变量。它将有一个默认参考。该引用将等同于null
。但是,struct
-type 变量不是引用:它是对象的实际内容。当保留为“空”时,它的所有字段(以及由幕后字段支持的自动实现的属性)都包含默认值——换句话说,它们也是“空的”。如果它们是引用类型,它们将是null
;如果它们是值类型,它们将为 0,或归零的结构(并且链继续)。
这也是structs
不能有默认构造函数的原因。正如您无法覆盖 class
在 null
时的样子一样,您也无法覆盖 struct
在归零时的样子。
有一个未充分利用的运算符来获取任何类型的默认值--class
、struct
或内部。那就是default()
运算符。例如:
class ClassType
struct StructType
//
// ...
//
var classA = default(ClassType);
var classB = (ClassType)null;
if (classA == classB)
// This will execute, because both equal null.
var structA = default(StructType);
var structB = new StructType();
if (structA == structB)
// This will execute, because both are zeroed.
//
// ...
//
/// <summary>
/// An example use case for the <c>default()</c> operator.
/// </summary>
/// <returns>
/// <c>null</c> if <c>T</c> is a reference type, a zeroed instance <c>T</c> is a
/// <c>struct</c>, or <c>0</c> if <c>T</c> is an intrinsic type.
/// </returns>
private static T GetDefault<T>()
// This line wouldn't compile, because T could be a value type.
//return null;
// This line wouldn't compile, because T could be a reference type without a default or accessible constructor.
//return new T();
// This will work!
return default(T);
// In newer versions of C#, when the type is known from the context, it can be omitted:
//return default;
【讨论】:
我真的不喜欢“类通过引用‘传递’”这句话。类类型的值可以像结构一样通过值或引用传递;不同的是结构类型的值是一个实例,而类类型的值是一个实例标识符。语句foo = new bar[3];
创建了三个新的空间来保存bar
类型的值。如果bar
是结构类型,则每个空格中的值将是一个默认值实例。如果是类类型,则每个空格中的值都是空实例标识符。
@supercat 通常会说它们是“通过引用传递”而不是“通过值传递”。标准程序员行话。话虽如此,你在正确的轨道上。类的实例化产生一个引用,而结构的实例化产生一个值。让我们不要用数组混淆水域:new Bar[3]
不一定创建三个空格;它似乎就是这样做的。这涉及到 CLR 的内部。 stackalloc Bar[3]
明确地为 3 个 Bar 值创建一个连续的空间,前提是 Bar 是一个结构。
如果Bar
是一个类,那么New Bar[3]
定义空间来保存三个引用。这不是“实现细节”——它就是这样做的。这三个引用是否在任何给定时间引用三个不同对象取决于对三个不同对象的引用是否存储在其中。
@supercat 在功能上,是的。在实施中,不一定。例如,一种语言可以支持稀疏数组,很多都支持。您正在考虑 C/++ 等低级语言。
虽然New Bar[1048576]
不需要物理地分配 内存来保存1048576 个对象引用,但它在语义上定义了 1024 个以前不存在的新存储位置。构造数组的行为不需要分配后备存储,但它定义了数组中的每个槽,这样访问该数组的元素 12345 的两个方法将访问相同的存储位置,访问元素 54321 的方法将看到一个不同的。【参考方案6】:
正如 David Heffernan 和 Henk Holterman 所说,因为结构是值类型,因此在声明它时会实例化。为了更好地了解ValueType和ReferenceType,请查看this linkP Daddy已经很好地解释了它。
【讨论】:
【参考方案7】:除了发布的内容之外:请注意,结构不能有无参数构造函数,也不能有任何实例字段的初始值设定项。默认值是将所有值类型字段设置为其默认值(例如,整数为 0,布尔值为 false 等),所有引用类型字段为 null。
其次,初始化结构,例如通过调用构造函数或使用default()
。
【讨论】:
Bzzz 在 C# 中为真,在 .NET 中为假 msmvps.com/blogs/jon_skeet/archive/2008/12/10/… 重要引用:Yes, you can write a parameterless constructor for a value type in .NET
@xanatos 没错,我指的是 C# 规范。以上是关于为啥可以在没有 new 关键字的情况下实例化结构?的主要内容,如果未能解决你的问题,请参考以下文章
为什么我不能在没有完整命名空间的情况下从字符串中实例化新对象
JavaScript中使用new操作符实例化对象时构造函数有返回值的情况分析