我不明白为什么需要使用“new”关键字

46

我对C#很新,之前了解的是C++。在C++中你可以这样做:

class MyClass{
....
};
int main()
{
   MyClass object; // this will create object in memory
   MyClass* object = new MyClass(); // this does same thing
}

然而,在C#中:

class Program
{
    static void Main(string[] args)
    {
        Car x;
        x.i = 2;
        x.j = 3;
        Console.WriteLine(x.i);
        Console.ReadLine();

    }
}
class Car
{
    public int i;
    public int j;


}

你不能这样做。我想知道为什么汽车x不能完成它的工作。


7
我经常说,从C++转到C#或Java比反过来更容易。如果对C++基础知识有扎实的理解,学习C#或Java就相当简单。但是,这两种语言都在底层做了很多事情,并且传统教授C#和Java的资源通常没有很好地解释这些内容,在从这两种语言转到C++时,总是非常令人沮丧的经验。 - Sam Varshavchik
82
顺便说一下,“Myclass object;”和“Myclass* object = new MyClass();”这两个语句有不同的作用。 - MaciekGrynda
29
第一种方法会在栈上创建对象,你不需要手动释放内存(析构函数会在作用域结束时被调用);第二种方法会在堆上创建对象,你需要手动释放内存。 - MaciekGrynda
25
如果你认为这两行 C++ 代码是等价的,那么你的代码会有大量的内存泄漏。在 C++ 中,它们之间的差异非常基础和重要,以至于我会减弱“我有 C++ 背景”的说法 - 实际上你并没有。 - pipe
4
由于某些原因,我也无法在 Haskell 中执行 Car x; x.j = 3; Console.WriteLine(x.i); - user253751
显示剩余6条评论
6个回答

65

这里有很多误解,既包括问题本身,也包括几个答案。

让我从检查问题的前提开始。问题是“为什么在C#中需要使用new关键字?”问题的动机是C++中的这个片段:

 MyClass object; // this will create object in memory
 MyClass* object = new MyClass(); // this does same thing

我有两个批评意见。首先,这两个在C++中并不相同,所以这个问题基于对C++语言的错误理解。非常重要的是要理解在C++中这两个事物的区别,如果你不清楚这个区别,请找一个导师教你如何了解这个区别,并在什么情况下使用每个东西。

其次,这个问题错误地预设了这两个语法在C++中做了相同的事情,然后奇怪地问“为什么我们需要在C#中使用new?”显然,根据这个错误的假设,正确的问题应该是“为什么我们需要在C++中使用new?”如果这两个语法执行相同的操作——而它们并不——那么为什么一开始就有两种语法呢?

因此,这个问题既基于错误的前提条件,关于C#的问题也没有真正从C++的误解设计中得出。

这是一团糟。让我们放弃这个问题,提出一些更好的问题。而且,让我们将关于C#的问题作为C#本身提出,而不是在C++设计决策的背景下提出。

在C#中,当X是类或结构类型时,new X 运算符有什么作用?(为了讨论的目的,我们忽略委托和数组。)

new运算符:

  • 导致分配给给定类型的新实例;新的实例将所有字段初始化为默认值。
  • 导致执行给定类型的构造函数。
  • 如果对象是引用类型,则产生对分配对象的引用,否则产生该值本身。

好了,我已经能听到C#程序员的反对意见了,那么让我们驳回它们。

反对意见:我听到你说,如果类型是值类型,就不会分配新的存储空间。但是,C#规范与你的看法不同。当你说:

S s = new S(123);
对于某些结构类型S,规范说明会在短期内存池上分配新的临时存储,并将其初始化为默认值,构造函数在this设置为引用临时存储的情况下运行,然后将生成的对象复制s。 但是,编译器允许使用复制省略优化,前提条件是它可以证明,在安全程序中不可能观察到优化。 (练习:计算出不能执行复制省略的情况,并给出一个在省略或不省略时会有不同行为的程序示例。)
异议:您说可以使用default(S)生成有效的值类型实例;没有调用构造函数,我听到了您的声音。 这是正确的。我没有说new是创建值类型实例的唯一方法。
事实上,对于值类型new S()default(S)是相同的东西。
异议:在C#6中是否真的会执行构造函数,例如new S()的情况,如果没有在源代码中存在。 这是一个“如果树倒在森林里而没有人听到它,它会发出声音吗?”问题。调用不执行任何操作的构造函数和根本不调用之间有区别吗?这不是一个有趣的问题。编译器可以省略它知道不会执行任何操作的调用。
假设我们有一个值类型的变量。我们必须使用new创建的实例初始化变量吗?
不需要。自动初始化的变量(例如字段和数组元素)将被初始化为默认值-即,结构中所有字段本身都是它们的默认值。
形式参数显然将用参数初始化。
值类型的局部变量需要在读取字段之前明确分配某些内容,但它不必是new表达式。
因此,在实际上,值类型的变量会自动初始化为default(S)的等价物,除非它们是局部变量?
是的。
为什么不对局部变量进行相同的处理?
未初始化的局部变量的使用与错误的代码密切相关。 C#语言禁止这样做,因为这样做会发现错误。
假设我们有一个引用类型的变量。我们必须使用new创建的实例初始化S吗?

不会。自动初始化的变量将使用null进行初始化。局部变量可以用任何引用类型进行初始化,包括null,并且在读取之前必须明确定义。

因此,引用类型的变量实际上会自动初始化为null,除非它们是局部变量?

是的。

为什么不对局部变量做同样的事情呢?

同样的原因。这可能会导致错误。

为什么不通过自动调用默认构造函数来自动初始化引用类型的变量?也就是说,为什么不让R r;R r = new R();一样?

首先,许多类型没有默认构造函数,或者干脆没有任何可访问的构造函数。其次,对于未初始化的局部变量或字段有一个规则,对于形式参数有另一个规则,对于数组元素有另一个规则,这似乎很奇怪。第三,现有的规则非常简单:变量必须被初始化为一个值;该值可以是任何你喜欢的值,为什么认为需要新的实例?如果这样做,那就太奇怪了。

R r;
if (x) r = M(); else r = N();

导致构造函数运行以初始化 r

暂且不谈 new 运算符的语义,为什么必须从语法上使用这种运算符呢?

其实不必要。有许多其他的语法形式也可以合法地表示同样的意思。最显然的方法是完全取消 new。如果我们有一个类 C,其中有一个构造函数 C(int),那么我们可以直接使用 C(123) 而不是 new C(123)。或者我们可以使用类似于 C.construct(123) 这样的语法。有许多种方法可以在没有 new 运算符的情况下完成这个操作。

那么为什么还要使用它呢?

首先,C# 的设计初衷是使熟悉使用 new 来表示正在初始化对象的新存储空间的 C++、Java、JavaScript 和其他语言的用户能够立即感到熟悉。

其次,恰当的语法冗余度非常可取。对象的创建是特殊的;我们希望用它自己的运算符来标识它的发生。


3
"对象创建很特别"。我不知道,现在对象创建并不感觉那么特别。无论如何,回答很好。 - Mephy
2
在C# 6中,您可以定义一个默认构造函数,当调用new S()时会被调用,但是当使用default(S)时不会被调用。但是这个特性最终被取消了 - 因为据我记得它引起了太多的麻烦。(当然,您仍然可以在IL中实现它,并且调用它的情况并不总是显而易见...) - Jon Skeet
2
另一个值得注意的事情是,由于C#命名约定目前的状态,如果没有new,很难区分使用构造函数和调用与调用方在同一范围内的方法之间的区别。有了new,可以帮助消除歧义,而不需要完全限定方法调用的需求。 - Dan
3
-1. @Eric 别只是批评原帖的问题并说“找一个导师”,而不解释语法差异,为什么不为听众解释一下呢?你显然知道。对我来说,在两种语法中都可以将类作为对象引用,这就是原帖中两个语句所做的。在我看来,这是一个完全有效的问题,而你冗长的解释没有回答到这一点。 “为什么要有两种不同的语法做一件在这两种语言中都能做的事情?”这是一个合理的问题。 - vapcguy
6
因为这个问题是关于 C# 语言设计的,而不是一个请求教程来纠正原帖作者对C++的错误观念。我正试图回答在这里提出的问题,即关于 new 运算符的设计以及它如何与变量声明相关联。关于C ++存储生命周期如何工作的冗长教程将会偏离主题。 - Eric Lippert
显示剩余4条评论

32

在C#中,你可以做类似的事情:

  // please notice "struct"
  struct MyStruct {
    ....
  }

  MyStruct sample1; // this will create object on stack
  MyStruct sample2 = new MyStruct(); // this does the same thing

请记住,像 intdoublebool 这样的基础类型也是 struct 类型,因此尽管习惯上写成

  int i;

我们也可以写成

  int i = new int(); 

C++和C#不同,C#(在安全模式下)不使用指针来引用实例,但是C#有classstruct声明:

  • class:你有一个引用的实例, 内存分配在上, 必须使用new;类似于C++中的MyClass*

  • struct:你有一个,通常内存分配在上, new可选的;类似于C++中的MyClass

对于您特定的情况,您可以将Car转换为struct

struct Car
{
    public int i;
    public int j;
}

因此,这个片段

Car x; // since Car is struct, new is optional now 
x.i = 2;
x.j = 3;

会是正确的


36
我会反驳那个一直被传播并且制造出一种极为普遍的错误看法:类被放在堆上,结构体被放在栈上。这是完全错误的。决定哪些放在栈上,哪些放在堆上取决于对象的预期寿命,而不是它的本质;一个无法证明寿命短暂的结构体将会被放在堆上: class myClass { int i = 1; //这将会被放在堆上,而不是栈上。 } - InBetween
5
谢谢!这是链接 https://blogs.msdn.microsoft.com/ericlippert/2010/09/30/the-truth-about-value-types/,该文章讲述了有关值类型的真相。 - Dmitry Bychenko
1
在类型内部声明 int i; 和在方法内部声明 int i; 是有区别的。前者将变量初始化为默认值(int 的默认值为0),而后者则不会初始化,因此无法读取。 - svick
10
我不太喜欢这个答案——它有太多的不可靠假设。我认为正确的答案应该是:“在C#中,除非你知道自己在干什么,否则你不应该真的关心内存管理。” - Leonardo Herrera
3
我建议将class foo {...}最接近的C++模拟为typedef class {...} *foo;需要使用new,原因与在C++中需要它相同(通过这种替换)。尽管有垃圾回收器,但最后一个存在的对象指针被销毁时会释放所占用的存储空间,而无需使用delete - supercat
显示剩余7条评论

16

在忽略堆栈方面的区别时:

因为C#在应该只是改变语法时却犯了抄袭C++的错误决定。

Car car = Car()

(或类似的东西)。有"新"是多余的。


5
目前为止,唯一一个正确理解的回答得到了+1。在C#中,没有像在C++中那样存在使用new关键字的基本需求,实际上有其他CLR语言根本不使用它(或任何等效的特定于语言的功能)。 - Mason Wheeler
6
除非你在调用构造函数的作用域中有一个名为Car()的方法,否则现在会产生冲突。 new部分使其清晰明确地表明您正在尝试调用构造函数。我可以理解使用Car.new()的论点,但对我来说仅使用Car()似乎不是个好主意。 - Jon Skeet
是的,添加 new 可以允许消除语法歧义,即在 1) 构造函数/方法/引用类 和 2) 创建对象之间。因此,虽然它不一定是“必需的”,但它可以大大区分你正在使用的内容。 - vapcguy
1
代码 Car car = Car() 在C++中实际上是可行的,尽管它与在C++中使用new得到的结果不同。 - Marian Spanik
1
或者你可以简单地写成Car car = Car.(); - pythonic

15
在C#中,class类型的对象总是在堆上分配的,即这种类型的变量始终是引用(“指针”)。仅声明此类类型的变量不会导致对象的分配。在C#中,像在C ++中普遍做的那样在堆栈上分配class对象通常并不是一个选项。
任何未分配的任何类型的局部变量都被视为未初始化,并且在分配之前无法读取它们。这是一种设计选择,看起来是个好主意,因为它应该可以保护您免受某些编程错误的影响。
这类似于在C++中说SomeClass * object;并且从未分配任何内容是没有意义的。
由于在C#中所有class类型变量都是指针,因此在声明变量时分配空对象将导致代码效率低下,特别是当您实际上只想稍后将值分配给变量时,例如在以下情况下:
// Needs to be declared here to be available outside of `try`
Foo f;

try { f = GetFoo(); }
catch (SomeException) { return null; }

f.Bar();

或者

Foo f;

if (bar)
    f = GetFoo();
else
    f = GetDifferentFoo();

3
说“类类型是引用”有点粗糙。类类型就是类类型。真正的情况是,使用类类型声明的变量表示对对象的可空引用。 - Kerrek SB
@KerrekSB: 更好了吗?我承认谈论堆可能被认为是一个实现细节,但这就是实际情况。 - Matti Virkkunen
这里的堆栈讨论对问题没有任何帮助 - 它与问题无关。对象是在堆栈上还是在堆上是无关紧要的,因为需要使用 new 的原因不同。 - mjwills

8
当您使用引用类型时,在以下语句中:
Car c = new Car();

创建了两个实体:一个名为 c 的引用指向栈中的 Car 类型对象,以及堆中的 Car 类型对象本身。

如果你只写

Car c;

如果 c 是一个局部变量,那么创建一个未初始化的引用,指向任何地方。

实际上,这与 C++ 代码等效,只不过使用指针而不是引用。

例如:

Car *c = new Car();

或只需
Car *c;

C++和C#的区别在于,C++可以在堆栈中创建类的实例,例如:
Car c;

在C#中,这意味着创建一个类型为Car的引用,就像我说的那样指向无处。

2

来自Microsoft编程指南:

在运行时,当您声明引用类型的变量时,该变量包含值为 null,直到您明确使用new运算符创建对象的实例或将其分配给已经使用new在其他位置创建的对象为止

类是引用类型。当创建类的对象时,对象所分配的变量仅保存对该内存的引用。当对象引用被分配给新变量时,新变量引用原始对象。通过一个变量所做的更改在另一个变量中反映,因为它们都引用相同的数据。

结构是值类型。当创建结构时,分配给结构的变量保存结构的实际数据。当结构分配给新变量时,它会被复制。因此,新变量和原始变量包含两个独立的相同数据副本。对一个副本所做的更改不会影响另一个副本。

我认为在您的C#示例中,您实际上正在尝试将值分配给空指针。 在c++翻译中,这将如下所示:

Car* x = null;
x->i = 2;
x->j = 3;

这段代码显然可以编译通过,但会导致程序崩溃。


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