为什么C#和Java要使用“new”运算符?

18

现代编程语言如C#和Java中为什么会存在new operator?它是纯粹的自我记录代码特性,还是有实际用途?

例如以下示例:

Class1 obj = new Class1();

Class1 foo()
{
    return new Class1();
}

这样的写法与更符合Python语言风格的写法一样易于阅读:

Class1 obj = Class1();

Class1 foo()
{
    return Class1();
}

编辑:Cowan 的澄清问题的观点非常到位:为什么他们选择了这种语法?

9个回答

24
  1. 这是一个自我记录的功能。
  2. 这是一种让在其他类中命名方法为“Class1”成为可能的方法。

1
至于第二点:你仍然可以这样做,但必须使用this.Class1()来限定。当类型和成员名称冲突时,现在也会发生同样的事情。 - Øyvind Skaar
4
另一方面,如果没有 new 操作符,就可以在其他地方创建一个 new() 函数。 ;) 我认为答案很简单:“这样做看起来更熟悉于 Java/C# 面向的程序员。” - jalf
2
这确实是可能的,但你为什么要违反良好的命名规范呢?你知道,类应该是名词,方法应该是动词... - Anttu

9

文档的用处在于 - 它比Python更容易区分对象创建和方法调用。

原因是历史遗留问题,直接来自C ++语法。 在C ++中,“Class1()”是一个表达式,在堆栈上创建Class1实例。例如: vector a = vector(); 在这种情况下,创建了一个向量并将其复制到向量a中(优化器可以在某些情况下删除冗余副本)。

相反,“new Class1()”在堆上创建一个Class1实例,就像Java和C#一样,并返回对它的指针,具有不同的访问语法,不像Java和C ++。实际上,new的含义可以重新定义为使用任何特殊用途的分配器,但仍必须引用某种堆,以便所获得的对象可以通过引用返回。

此外,在Java / C#/ C ++中,Class1()本身可能会引用任何方法/函数,这会令人困惑。 Java编码约定实际上会避免这种情况,因为它们要求类名以大写字母开头,方法名以小写字母开头,也许这就是Python在这种情况下避免混淆的方式。读者期望“Class1()”创建一个对象,“class1()”是一个函数调用,“x.class1()”是一个方法调用(其中“x”可以是“self”)。

最后,由于在Python中他们选择使类成为对象,特别是可调用对象,因此允许没有“new”的语法,并且允许使用另一种语法是不一致的。


2
不仅如此,在Python中,函数和构造函数之间没有语义上的区别,至少在类的外部是这样。就像你会做 def inc(a):return a + 1; map(inc,somelist) 来增加 somelist 中所有项的值一样,你也可以做 map(int,somelist)somelist 中所有项转换为 int 类型。 - Imagist

9
Class1 obj = Class1();

在C#和Java中,需要使用"new"关键字,否则它会将"Class1()"视为调用一个名为"Class1"的方法。

真的吗?但如果不是这样呢?为什么new运算符首先存在呢? - dalle
1
但事实确实如此。你在问:“如果引擎不是驱动汽车的原因,那会怎样?为什么‘引擎’一开始就存在?” - Michael
如果一匹马拉着汽车行驶,发动机仅控制收音机和空调怎么办?或者还有其他类型的汽车可以在没有发动机的情况下行驶呢? - Paco
我想回答你的问题,但我有一节由驾驭哨声和内部照明的引擎控制的牛拉动的火车要赶。 - Michael
12
完全不同的问题。这个问题是“为什么选择这种句法?”更像是“为什么选择内燃机来驱动汽车?”,这是一个非常合理的问题,也有非常合理的答案。请注意,不要改变原意,使语言更通俗易懂。 - Cowan

5
C#中的新操作符直接映射到称为newobj的IL指令,该指令实际上为新对象的变量分配空间,然后执行构造函数(在IL中称为.ctor)。在执行构造函数时,类似于C++,将初始化对象的引用作为不可见的第一个参数传递进去(类似于thiscall)。
类似于thiscall的约定允许运行时仅一次加载和JIT特定类的所有代码并重用它们以用于类的每个实例。
Java可能有类似的中间语言操作码,但我不熟悉。

2
Java选择它的原因是因为语法对于C++开发者来说很熟悉。而C#选择它的原因是因为它对于Java开发者来说很熟悉。
在C++中使用new运算符的原因可能是因为在手动内存管理中,明确内存何时被分配非常重要。虽然Python式的语法也可以工作,但这会使内存分配不太明显。

2
C++为程序员提供了在堆上或栈上分配对象的选择。
基于栈的分配更高效:分配成本更低,解除分配成本真正为零,并且语言提供帮助来划分对象的生命周期,减少遗忘释放对象的风险。另一方面,在C++中,当发布或共享指向基于栈的对象的引用时,需要非常小心,因为基于栈的对象在堆栈帧被展开时会自动释放,导致悬空指针。
使用new运算符,在Java或C#中所有对象都在堆上分配。

Class1 obj = Class1();

实际上,编译器会尝试查找名为Class1()的方法。
例如,以下是常见的Java错误:
public class MyClass
{
  //Oops, this has a return type, so its a method not a constructor!
  //Because no constructor is defined, Java will add a default one.
  //init() will not get called if you do new MyClass();
  public void MyClass()
  {
     init();
  }
  public void init()
  {
     ...
  }
} 

注意:“所有对象都分配在堆上”并不意味着在底层不会偶尔使用堆栈分配。
例如,在Java中,Hotspot优化,如逃逸分析就使用堆栈分配。
运行时编译器执行这个分析,可以得出结论,例如,在方法中仅局部引用堆上的对象,并且没有引用可以从此作用域逃逸。如果是这样,Hotspot可以应用运行时优化,可以将对象分配到堆栈或寄存器中,而不是分配到堆上。
虽然这种优化并不总是被认为是决定性的……

1
新操作符分配对象的内存,这是它的目的;正如您所说,它还自我记录了您正在使用哪个实例(即新实例)。

在C#中为结构体调用new关键字并不会分配内存。 - dalle
它确实存在,只是在堆栈上而不是堆上。 - Rowland Shaw
不是的,当函数被调用时,它已经被分配了。 - dalle

0

正如其他人所指出的,Java和C#提供了new语法,因为C++也这样做了。而且C++需要一种方法来区分在堆栈上创建对象、在堆上创建对象或调用返回指向对象的指针的函数或方法。

C++使用了这种特定的语法,因为早期的面向对象语言Simula使用了它。Bjarne Stroustrup受到Simula的启发,试图向C中添加类似Simula的功能。C有一个用于分配内存的函数,但不能保证也调用构造函数。

来自Bjarne Stroustrup的1994年的《C++设计与演化》,第57页:

因此,我引入了一个运算符来确保分配和初始化都完成了:
``` monitor* p = new monitor; ```
这个运算符被称为 `new`,因为它是对应 Simula 运算符的名称。`new` 运算符调用一些分配函数来获取内存,然后调用构造函数来初始化该内存。这个组合操作通常被称为实例化或者简单地叫做对象创建:它将原始内存创建成一个对象。
运算符 `new` 提供的符号方便性非常重要...

-1
除了上面的评论外,据我所知,他们在Java 7的早期草案中计划删除new关键字。但后来他们取消了这个计划。

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