C#中的关键字“new”对结构体有什么作用?

77

C#中,结构体是按值来管理的,而对象则是按引用管理的。据我理解,当创建类的一个实例时,关键字new会让C#使用类信息来创建该实例,就像下面这样:

class MyClass
{
    ...
}
MyClass mc = new MyClass();

对于结构体,你不是在创建一个对象,而只是将一个变量设置为一个值:

struct MyStruct
{
    public string name;
}
MyStruct ms;
//MyStruct ms = new MyStruct();     
ms.name = "donkey";

我不理解的是如果通过MyStruct ms = new MyStruct()声明变量,这里的关键字new在语句中的作用是什么?如果结构体不能成为一个对象,那这里的new实例化了什么?


3
一个struct的实例 一个对象。你可能误解的区别在于值类型和引用类型之间的区别。 - Ed S.
但在C语言中没有对象,结构体也不是一个对象。那么在C#中,结构体被实现为对象了吗? - KMC
1
将C#视为C语言是没有帮助的。忽略语法上的差异,它们是完全不同的编程语言。 - Ed S.
1
@KMC 即使在 C 语言中也存在对象。你误解了“对象”的含义——可以理解,因为在不同的上下文中,“对象”有许多不同的含义。例如,在 C++(我认为 C 也类似)中,它只是内存中的一个空间:驻留在内存中的所有内容都是对象。 - Konrad Rudolph
相关答案:https://dev59.com/wG865IYBdhLWcg3wLrhE#3943596。何时不要使用`new`初始化`struct`。 - John Alexiou
6个回答

68

来自MSDN上的struct (C# Reference):当使用new运算符创建一个结构体对象时,它将被创建并调用适当的构造函数。与类不同,可以不使用new实例化结构体。如果不使用new,则字段将保持未分配状态,并且在初始化所有字段之前无法使用该对象。

据我理解,在没有使用new的情况下,你实际上无法正确使用结构体,除非你手动初始化所有字段。如果使用new运算符,则编写正确的构造函数可以为您完成这项任务。

希望这能澄清问题。如果需要进一步解释,请告诉我。


编辑

有一个相当长的评论线程,因此我想在这里添加更多内容。我认为了解它的最好方法是尝试一下。在Visual Studio中创建名为"StructTest"的控制台项目,并将以下代码复制到其中。

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace struct_test
{
    class Program
    {
        public struct Point
        {
            public int x, y;

            public Point(int x)
            {
                this.x = x;
                this.y = 5;
            }

            public Point(int x, int y)
            {
                this.x = x;
                this.y = y;
            }

            // It will break with this constructor. If uncommenting this one
            // comment out the other one with only one integer, otherwise it
            // will fail because you are overloading with duplicate parameter
            // types, rather than what I'm trying to demonstrate.
            /*public Point(int y)
            {
                this.y = y;
            }*/
        }

        static void Main(string[] args)
        {
            // Declare an object:
            Point myPoint;
            //Point myPoint = new Point(10, 20);
            //Point myPoint = new Point(15);
            //Point myPoint = new Point();


            // Initialize:
            // Try not using any constructor but comment out one of these
            // and see what happens. (It should fail when you compile it)
            myPoint.x = 10;
            myPoint.y = 20;

            // Display results:
            Console.WriteLine("My Point:");
            Console.WriteLine("x = {0}, y = {1}", myPoint.x, myPoint.y);

            Console.ReadKey(true);
        }
    }
}

试着玩一下吧。删除构造函数并观察发生了什么。尝试使用只初始化一个变量的构造函数(我已注释掉一个...它不会编译)。使用和不使用new关键字进行尝试(我已注释掉一些示例,请取消注释并尝试)。


结构体可以被实例化,但是结构体不可以作为一个对象,是吗?结构体中的“字段”指的是属性和方法吗?如果结构体不是一个对象,为什么它的字段需要初始化呢?我认为我需要更多的解释。谢谢。 - KMC
8
@joshhendo: 嗯?一个结构体的实例是一个对象,它们当然可以包含方法和私有字段。你正在让初学者感到困惑。 - Ed S.
Haris:我已经仔细检查过了,如果一个结构体的构造函数没有为每个变量分配值,那么你将无法编译程序。因此这个说法是正确的。使用构造函数的主要好处是,如果需要的话,可以设置默认值并重载构造函数,而且看起来更整洁。但对于任何比这更复杂的情况,最好使用类。 - joshhendo
如果您定义自己的构造函数,那么这是正确的。但是如果您使用默认构造函数呢?在那种情况下,“如果您使用new运算符,则构造函数将为您执行此操作”这个语句就不再正确了。这就是我的观点。 - Haris Hasan
啊,很抱歉。是的,你说得对,我刚测试了一下。所以,如果你在一个非默认构造器上使用 new 操作符,你可以保证会有值;但是如果你在默认构造器上使用它,你实际上只是清空了它(我不敢说是创建了一个新的实例,因为这个词是我正在解释的)。垃圾回收应该会清除旧实例。 - joshhendo
显示剩余6条评论

21

以下是Eric Lippert在这个帖子中的精彩答案

当你“new”一个值类型时,会发生三件事情。首先,内存管理器从短期存储中分配空间。其次,构造函数被传递到短期存储位置的引用。构造函数运行后,短期存储位置中的值将被复制到值的存储位置,不管它在哪里。请记住,值类型的变量存储实际值。

(请注意,如果编译器确定这样做永远不会向用户代码公开部分构造的结构体,则编译器允许将这三个步骤优化为一步。也就是说,编译器可以生成代码,只需将最终存储位置的引用传递给构造函数,从而节省一个分配和一个复制。)

因为这确实是一个答案


值得注意的是,由于“out”参数是C#概念,而不是.NET运行时使用的概念,因此将部分构造的结构作为“out”参数传递给外部方法将使其值暴露给外部代码,即使C#编译器认为它不会。例如,可以以这种方式定义结构体,即“myThing = newmyThing(5);”将初始化“myThing”的一个字段,同时保留其他字段不受影响。 - supercat
3
我认为不同语言组可能对.NET应该是什么有自己的看法,并且假装它符合他们的愿景。例如,C#组可能认为.NET 应该有可执行的out参数,如果每个人都使用C#编程,那么它将拥有这样的功能,但是其他语言将把带有out参数的虚方法视为带有ref参数的虚方法。在某些情况下,语言不受其他语言实现者想要实现的最小子集特性的限制是很好的,但也存在危险。 - supercat
1
这应该是答案。 - Monku

5
在结构体中,关键字new是毫无意义的,它只是必须的,如果你想要使用构造函数。它并不执行new操作。
通常new的意义是在堆上分配永久存储空间。例如,C++语言允许new myObject()或者只写myObject()。两者都会调用相同的构造函数。但前者创建一个新对象并返回指针,而后者只是创建了一个临时对象。任何结构体或类都可以使用其中任意一种方式。使用new就是一种选择,并且具有特定含义。
在C#中,没有这个选择。类总是在堆上,结构体总是在栈上。无法对结构体执行真正的new操作。有经验的C#程序员已经习惯了这一点。当他们看到ms=new MyStruct();时,他们知道要忽略new作为语法结构。他们知道它的作用就像ms=MyStruct();一样,只是将值分配给现有对象。
奇怪的是,类需要使用关键字newc=myClass();不被允许(使用构造函数来设置现有对象 c 的值),你需要像c.init(); 这样的方式来实现。所以你真的没有选择,构造函数总是为类分配内存,而从不为结构体分配内存。 new关键字只是装饰。
我认为要求在结构体中使用虚假的new关键字的原因是为了方便地将结构体转换为类(假定你在首次声明时始终使用myStruct=new myStruct();,这是建议的)。

1
仅将实现细节考虑在内是错误的,结构体并不总是分配在堆栈上,例如类上的结构体字段、装箱等等……而且new实际上是有用处的! - eyalalonn
2
混淆的是人们将这个问题理解为“为什么我应该在结构体中使用new”。但如果你认真阅读,这个问题实际上是关于最后一个短语“这里的new实例化了什么”的。这是一个关于为什么他们选择那种有趣的语法的问题。 - Owen Reynolds

3

使用"new MyStuct()"可以确保所有字段都被设置为某个值。在上面的情况下,没有任何不同。如果你尝试读取ms.name而不是设置它,你将会在VS中得到一个"Use of possible unassigned field 'name'"错误。


3
任何时候一个对象或结构体被创建,它的所有字段也会被创建;如果这些字段中有结构体类型,那么所有嵌套字段也会被创建。当创建一个数组时,它的所有元素都会被创建(同样地,如果其中任何元素是结构体,则这些结构体的字段也会被创建)。所有这些都发生在任何构造函数代码有机会运行之前。
在 .net 中,结构体构造函数实际上只是一个以结构体作为“out”参数的方法。在 C# 中,调用结构体构造函数的表达式将分配一个临时结构体实例,在该实例上调用构造函数,然后使用该临时实例作为表达式的值。请注意,这与 vb.net 不同,在 vb.net 中,构造函数的生成代码将首先清零所有字段,但调用者的代码将尝试让构造函数直接操作目标。例如:myStruct = new myStructType(whatever) 在 vb.net 中会在构造函数的第一条语句执行之前清空 myStruct;在构造函数内部,对正在构建的对象进行的任何写入操作都将立即作用于 myStruct

0

ValueType和结构在C#中是一些特殊的东西。在这里,我将向您展示当您新建某些内容时会发生什么。

在这里,我们有以下内容:

  • 代码

    partial class TestClass {
        public static void NewLong() {
            var i=new long();
        }
    
        public static void NewMyLong() {
            var i=new MyLong();
        }
    
        public static void NewMyLongWithValue() {
            var i=new MyLong(1234);
        }
    
        public static void NewThatLong() {
            var i=new ThatLong();
        }
    }
    
    [StructLayout(LayoutKind.Sequential)]
    public partial struct MyLong {
        const int bits=8*sizeof(int);
    
        public static implicit operator int(MyLong x) {
            return (int)x.m_Low;
        }
    
        public static implicit operator long(MyLong x) {
            long y=x.m_Hi;
            return (y<<bits)|x.m_Low;
        }
    
        public static implicit operator MyLong(long x) {
            var y=default(MyLong);
            y.m_Low=(uint)x;
            y.m_Hi=(int)(x>>bits);
            return y;
        }
    
        public MyLong(long x) {
            this=x;
        }
    
        uint m_Low;
        int m_Hi;
    }
    
    public partial class ThatLong {
        const int bits=8*sizeof(int);
    
        public static implicit operator int(ThatLong x) {
            return (int)x.m_Low;
        }
    
        public static implicit operator long(ThatLong x) {
            long y=x.m_Hi;
            return (y<<bits)|x.m_Low;
        }
    
        public static implicit operator ThatLong(long x) {
            return new ThatLong(x);
        }
    
        public ThatLong(long x) {
            this.m_Low=(uint)x;
            this.m_Hi=(int)(x>>bits);
        }
    
        public ThatLong() {
            int i=0;
            var b=i is ValueType;
        }
    
        uint m_Low;
        int m_Hi;
    }
    

而测试类方法的生成IL将是:

  • IL

    // NewLong
    .method public hidebysig static 
        void NewLong () cil managed 
    {
        .maxstack 1
        .locals init (
            [0] int64 i
        )
    
        IL_0000: nop
        IL_0001: ldc.i4.0 // 将0作为int类型压入堆栈
        IL_0002: conv.i8  // 将堆栈中的值转换为long类型
        IL_0003: stloc.0  // 将其弹出并存储到第一个本地变量i中
        IL_0004: ret
    } 
    
    // NewMyLong
    .method public hidebysig static 
        void NewMyLong () cil managed 
    {
        .maxstack 1
        .locals init (
            [0] valuetype MyLong i
        )
    
        IL_0000: nop
        IL_0001: ldloca.s i     // 将i的地址压入堆栈
        IL_0003: initobj MyLong // 弹出i的地址并将其初始化为MyLong类型
        IL_0009: ret
    } 
    
    // NewMyLongWithValue 
    .method public hidebysig static 
        void NewMyLongWithValue () cil managed 
    {
        .maxstack 2
        .locals init (
            [0] valuetype MyLong i
        )
    
        IL_0000: nop
        IL_0001: ldloca.s i  // 将i的地址压入堆栈
        IL_0003: ldc.i4 1234 // 将1234作为int类型压入堆栈
        IL_0008: conv.i8     // 将堆栈中的值转换为long类型
    
        // 调用构造函数
        IL_0009: call instance void MyLong::.ctor(int64) 
    
        IL_000e: nop
        IL_000f: ret
    } 
    
    // NewThatLong
    .method public hidebysig static 
        void NewThatLong () cil managed 
    {
        // 方法从RVA 0x33c8开始
        // 代码大小为8(0x8)
        .maxstack 1
        .locals init (
            [0] class ThatLong i
        )
    
        IL_0000: nop
    
        // 通过调用构造函数创建新对象并将其引用压入堆栈
        IL_0001: newobj instance void ThatLong::.ctor() 
    
        // 弹出堆栈中的对象引用并存储到第一个本地变量i中
        IL_0006: stloc.0
    
        IL_0007: ret
    } 
    

方法的行为在IL代码中有注释。您可能想要查看OpCodes.InitobjOpCodes.Newobj。值类型通常使用OpCodes.Initobj进行初始化,但正如MSDN所说,OpCodes.Newobj也可以使用。

  • OpCodes.Newobj的描述

    通常不使用newobj来创建值类型。它们通常是作为参数或局部变量分配的,使用newarr(用于零基础、一维数组)或作为对象的字段。一旦分配,它们使用Initobj进行初始化。然而,newobj指令可以用于在堆栈上创建值类型的新实例,然后将其作为参数传递、存储在本地等。

对于每个数值类型,从bytedouble,都有一个定义好的操作码。虽然它们被声明为struct,但生成的IL存在一些差异。

这里还有两件事情需要提到:

  1. ValueType本身被声明为一个抽象类

    也就是说,你不能直接new它。

  2. struct不能包含显式的无参构造函数

    也就是说,当你new一个struct时,你会陷入上面提到的NewMyLongNewMyLongWithValue的情况。

总之,对于值类型和结构体来说,new是为了语言概念的一致性。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接