为什么结构和类在C#中是分开的概念?


44

在用C#编程时,我偶然发现了一个我无法理解的奇怪的语言设计决策。

因此,C#(和CLR)具有两种聚合数据类型:(struct值类型,存储在堆栈上,没有继承)和class(引用类型,存储在堆上,具有继承)。

首先,这种设置听起来不错,但随后您偶然发现了采用聚合类型参数的方法,要弄清楚它实际上是值类型还是引用类型,必须找到其类型的声明。有时可能会造成混乱。

解决该问题的公认方法似乎是将所有structs 声明为“不可变的”(将其字段设置为readonly),以防止可能的错误,从而限制structs的用途。

例如,C ++使用了更多可用的模型:它允许您在堆栈或堆上创建对象实例,并按值或按引用(或按指针)传递它。我一直听到C#的灵感来自C ++,但我不明白为什么它不采用这种技术。结合classstruct进入一个结构有两个不同的分配方案(堆和栈)和周围将它们作为值或(明确)经由引用refout关键字似乎是一个很好的事情。

问题是,为什么在C#和CLR中classstruct成为单独的概念,而不是具有两个分配选项的一种聚合类型?


34
“人们普遍接受的解决方案似乎是将所有结构都声明为“不可变的”,从而限制了结构的有用性。”许多人会争辩说,使任何不可变的东西通常都可以在不成为性能瓶颈的原因时变得更加有用。另外,structs并不总是存储在堆栈中;考虑带有struct字段的对象。除此之外,正如梅森·惠勒(Mason Wheeler)提到的,切片问题可能是最大的原因。
Doval 2015年

7
C#的灵感并非来自C ++,而是真实的。C#的灵感来自C ++和Java设计中的所有(当时的含义和良好的发音)错误。
Pieter Geerkens 2015年

18
注意:堆栈和堆是实现细节。没有什么可以说结构实例必须在堆栈上分配而类实例必须在堆上分配。实际上,这甚至不是事实。例如,编译器很有可能使用Escape Analysis确定变量无法转义局部作用域,因此即使它是类实例,也无法将其分配到堆栈中。它甚至没有说根本没有堆栈。您可以将环境框架分配为堆上的链接列表,甚至根本没有堆栈。
约尔格W¯¯米塔格


3
“但是,您偶然发现了一个采用聚合类型参数的方法,要弄清它实际上是值类型还是引用类型,就必须找到其类型的声明。”嗯,为什么这很重要?该方法要求您传递一个值。您传递一个值。在什么时候您需要关心它是引用类型还是值类型?
a安

Answers:


58

C#(以及Java和C ++之后开发的其他所有OO语言)之所以没有在这方面复制C ++的模型,是因为C ++的处理方式太糟了。

您正确地识别了以上相关点struct::值类型,无继承。 class:引用类型,具有继承性。继承和值类型(或更具体地讲,多态和值传递)不会混合使用;如果将类型的对象传递给type Derived的方法参数Base,然后在其上调用虚拟方法,则获得适当行为的唯一方法是确保所传递的是引用。

在这和您将可继承对象作为值类型(想到复制构造函数和对象切片!)时在C ++中遇到的所有其他混乱之间,最好的解决方案是拒绝。

好的语言设计不仅要实现功能,还要知道不实现哪些功能,而做到这一点的最佳方法之一就是从前辈的错误中吸取教训。


29
这只是C ++上另一种毫无意义的主观因素。无法投票,但如果可以的话。
Bartek Banachewicz 2015年

17
@MasonWheeler:“ 是一团糟 ”听起来很主观。在一个长长的评论话题中已经对您的另一个答案进行了讨论; 线程被刺破了(不幸的是,尽管在火焰战争酱中它包含有用的注释)。我认为在这里不必重复整个过程,但是“ C#正确,C ++错误”(这似乎是您要传达的信息)确实是一个主观的陈述。
安迪·普罗

15
@MasonWheeler:我在被破坏的线程中这样做,其他几个人也是如此-这就是为什么我认为很不幸被删除的原因。我认为在此处复制该线程不是一个好主意,但简短的版本是:在C ++ 中,类型的用户(而不是其设计者)可以决定应使用哪种语义(类型是引用语义还是值)语义)。这有优点也有缺点:您在不考虑优点的情况下就对缺点进行了怒吼。这就是为什么分析是主观的。
安迪·普罗

7
遗憾的是,我相信LSP违规讨论已经开始。我有点相信,大多数人都同意LSP的提及很奇怪且无关紧要,但由于mod破坏了注释线程而无法检查。
Bartek Banachewicz 2015年

9
如果将最后一段移到顶部并删除当前的第一段,我认为您有一个完美的论点。但是当前的第一段只是主观的。
马丁·约克

19

以此类推,C#基本上就像一套机械工具,有人读过,您通常应该避免使用钳子和活动扳手,因此它根本不包含活动扳手,并且钳子被锁定在标记为“不安全”的特殊抽屉中,并且只有在签署免责声明后才可使用,该免责声明使您的雇主对您的健康负有任何责任。

相比之下,C ++不仅包括活动扳手和钳子,还包括一些用途不明显的怪异的专用工具,如果您不知道正确的握持方法,它们可能会轻易切断您的手。拇指(但是一旦您了解了如何使用它们,就可以使用C#工具箱中的基本工具完成本质上不可能的操作)。此外,它还具有车床,铣床,平面磨床,金属切割带锯等,可让您在需要时设计和创建全新的工具(但是,这些机械师的工具会并且会导致如果您不知道自己在做什么,或者即使您只是粗心大意,也可能会受到严重伤害)。

这反映了哲学上的基本差异:C ++试图为您提供您可能想要的任何设计所需的所有工具。它几乎不会尝试控制您使用这些工具的方式,因此也很容易使用它们来生成仅在极少数情况下才能正常工作的设计,以及可能只是一个糟糕想法而没人知道这种情况的设计。他们可能会很好地工作。尤其是,很多工作都是通过解耦设计决策来完成的,即使实际上实际上几乎总是耦合在一起的。结果,仅编写C ++和编写良好的C ++之间存在巨大差异。为了很好地编写C ++,您需要了解很多习惯用法和经验法则(包括关于在打破其他经验法则之前应认真考虑的经验法则)。结果是,C ++的重点是易于使用(专家)而不是易于学习。在很多情况下,使用它们也不是一件容易的事。

C#做出了很多努力来试图(或者至少非常强烈地建议)语言设计师认为好的设计实践。在C ++中有很多解耦的东西(但通常在实践中会结合在一起)在C#中直接耦合。它确实允许“不安全的”代码稍微突破界限,但老实说,并不是很多。

结果是,一方面,有很多设计可以直接用C ++进行表达,而这些设计在C#中表达起来却很笨拙。在另一方面,它是一个整体更容易学习C#,并产生一个很可怕的设计,将您的情况不工作(或者可能是任何其他)的的机会大大减少。可以说,在很多(可能甚至是大多数)情况下,只需“顺其自然”,就可以得到可靠,可行的设计。或者,正如我的一个朋友(至少我喜欢将他视为朋友-不确定他是否真的同意)喜欢说的那样,C#使其很容易陷入成功。

因此,更具体地看一下这两种语言的方式classstruct获得方式的问题:在继承层次结构中创建的对象,在这些继承层次中,您可能以其基类/接口的名义使用派生类的对象,因此,您几乎可以坚持您通常需要通过某种指针或引用来做到这一点的事实-具体而言,发生的事情是派生类的对象包含一些可以视为基类/实例的内存。接口,并且派生对象通过该部分内存的地址进行操作。

在C ++中,取决于程序员是否正确执行此操作-当他使用继承时,取决于他(例如)确保与层次结构中的多态类一起工作的函数通过指针或对基的引用来做到这一点类。

在C#中,类型之间基本相同的分隔要明确得多,并且由语言本身来实施。程序员无需采取任何步骤即可通过引用传递类的实例,因为默认情况下会发生这种情况。


2
作为C ++爱好者,我认为这是C#与瑞士军用电锯之间差异的出色总结。
David Thornley '18

1
@DavidThornley:我至少试图写出我认为比较平衡的内容。不是指责,而是我写这篇文章时所看到的一些东西使我感到……有些不准确(说得很好)。
杰里·科芬

7

摘自“ C#:为什么我们需要另一种语言?” -Gunnerson,Eric:

简洁是C#的重要设计目标。

可能会在简单性和语言纯净度上过分提倡,但是出于纯净度的考虑,纯净度对于专业程序员而言几乎没有用处。因此,我们试图在希望拥有一种简单而简洁的语言与解决程序员所面临的现实问题之间取得平衡。

[...]

值类型,运算符重载和用户定义的转换都增加了语言的复杂性,但可以极大地简化重要的用户场景。

对象的引用语义是一种避免很多麻烦的方法(当然,不仅是对象切片),但现实世界中的问题有时可能需要具有值语义的对象(例如,看看听起来像我不应该使用引用语义的吧?换一个角度来看)。

因此,有什么比将那些带有价值语义的肮脏,丑陋和不良的有价值的对象隔离开来更好的方法struct呢?


1
我不知道,也许不使用带有引用语义的那些肮脏,丑陋和不良的对象?
Bartek Banachewicz 2015年

也许...我迷失了方向。
manlio

2
恕我直言,Java设计中的最大缺陷之一是缺少任何声明变量用于封装身份所有权的方法,而C#的最大缺陷之一是缺少区分操作的手段。来自对变量持有引用的对象的操作中的变量。即使Runtime不在乎这种区别,使用一种语言指定类型的变量是否int[]应该是可共享的还是可变的(数组可以是一个变量,但通常不能同时存在)将有助于使错误的代码看起来不正确。
supercat

4

与其考虑从中派生的值类型Object,不如考虑存在于与类实例类型完全独立的Universe中的存储位置类型,但是对于每个值类型都有一个对应的堆对象类型。结构类型的存储位置仅包含该类型的公共字段和私有字段的串联,堆类型根据以下模式自动生成:

// Defined structure
struct Point : IEquatable<Point>
{
  public int X,Y;
  public Point(int x, int y) { X=x; Y=y; }
  public bool Equals(Point other) { return X==other.X && y==other.Y; }
  public bool Equals(Object other)
  { return other != null && other.GetType()==typeof(this) && Equals(Point(other)); }
  public bool ToString() { return String.Format("[{0},{1}", x, y); }
  public bool GetHashCode() { return unchecked(x+y*65531); }
}        
// Auto-generated class
class boxed_Point: IEquatable<Point>
{
  public Point value; // Fake name; C++/CLI, though not C#, allow full access
  public boxed_Point(Point v) { value=v; }
  // Members chain to each member of the original
  public bool Equals(Point other) { return value.Equals(other); }
  public bool Equals(Object other) { return value.Equals(other); }
  public String ToString() { return value.ToString(); }
  public Int32 GetHashCode() { return value.GetHashCode(); }
}

对于以下语句:Console.WriteLine(“该值为{0}”,somePoint);

被翻译为:boxed_Point box1 = new boxed_Point(somePoint); Console.WriteLine(“值是{0}”,box1);

在实践中,由于存储位置类型和堆实例类型存在于单独的Universe中,因此不必将堆实例类型称为boxed_Int32;因为系统会知道哪些上下文需要堆对象实例,哪些上下文需要存储位置。

有人认为,任何行为不像对象的值类型都应视为“邪恶”。我持相反的观点:由于值类型的存储位置既不是对象也不是对对象的引用,因此认为它们的行为应类似于对象的期望应该被认为是无益的。在一个结构可以像一个对象一样有用地工作的情况下,这样做没有错,但每个struct结构的核心无非就是将公共和私有领域用胶带绑在一起。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.