为什么结构体不能有析构函数?

31

有关面试中这样的问题,你认为最好的回答是什么?

我认为如果已经有类似的回答,请提供链接。


7
在面试中,我不会问这个问题,因为我喜欢问与实际工作经验有关的问题,包括过去和未来。如果您想要看到某人如何推理出晦涩的边角案例或者看到他们在面试中如何应对这样的问题,那么它可能适用于更大的一系列问题。否则,这只是琐事。我会给他们思考问题的分数,并指出这是在现实中可能不会遇到的事情。如果他们提到这是一个无意义的问题,取决于他们的表现,也可能会得到额外的加分。 - Merlyn Morgan-Graham
我知道一个地方,在面试中会问这个问题。我认为有各种不同的方法可以回答它。我喜欢当前得到赞同的答案。另外,据我所知,这位面试官不会问与lambda/linq相关的问题,但他确实会问这个几乎没有用的问题(在我看来)。特别是因为我知道该领域与结构级别相距甚远,而且我相信该项目中少于0.5%的结构体,实际数字可能更小。所以这是一个奇怪的问题,而且我真的不确定他们想要测试什么。 - Valentin Kuzub
PS:鉴于上述描述,我怀疑提到这是一个无意义的问题不会在面试中为我们赢得分数 :) 当然,下一个问题是 - 我们真的想在那里工作吗? - Valentin Kuzub
@ValentinKuzub:你可以让他/她谈论一下他们最近参与的项目。虽然他们最近的项目可能涉及到特定的东西(如LINQ、模式等),但如果候选人对该项目有真正的兴趣,那么它们必然会涉及到值得进一步提问的某些内容 - Joel B Fant
我并不总是这样认为。有些职位需要热情的人,有些需要优秀的团队合作者,有些需要在您的团队推动边界的人。因此,这取决于候选人和职位。如果某人的表现与其水平不成比例,您可能会考虑将他们分配到一个不那么关键的职位,或者基于软技能而完全淘汰他们。不过,部分责任也在于您让他们放松 - 这是双方面的 :) 除此之外,就像Joel所说,可以询问候选人的课外编程兴趣,特别是如果这些兴趣在他们的简历上提到了。 - Merlyn Morgan-Graham
显示剩余2条评论
4个回答

68

另一种看待这个问题的方式是,不只是引用规范中指出结构体不能/没有析构函数。而是考虑如果规范被改变以允许结构体拥有析构函数会发生什么,或者更确切地说,让我们问一个问题:为什么语言设计者决定一开始就不允许结构体拥有“析构函数”?

(不要被这里的“析构函数”一词所困扰;我们基本上正在谈论一种在结构体上自动调用的魔术方法。换句话说,这是一种类似于C++析构函数的语言特性。)

首先要意识到的是,我们并不关心释放内存。无论对象是在堆栈上还是堆上(例如,在类中的结构体),内存最终总会被照顾到;通过弹出或收集等方式。拥有类似析构函数的真正原因是为了管理外部资源-像文件句柄、窗口句柄或其他需要特殊处理才能清理它们的东西,CLR本身并不知道。

现在假设允许结构体拥有可以进行此清理的析构函数。很好,直到你意识到当结构体作为参数传递时,它们会按值传递:它们被复制了。现在你有两个具有相同内部字段的结构体,它们将尝试清理同一个对象。其中一个会先发生,因此之后使用另一个代码将开始出现神秘故障……然后它自己的清理也会失败(希望如此!-最坏情况是它可能成功地清理了一些其他随机资源-例如,在句柄值被重用的情况下可能会发生这种情况。)

你可以为作为参数的结构体做一个特殊情况,以便它们的“析构函数”不运行(但要小心-现在需要记住调用函数时始终是外部函数“拥有”实际资源-因此现在有些结构体与其他结构体略有不同……),但是对于普通结构体变量仍存在这个问题,其中一个可以分配给另一个从而创建副本。

你可以通过为赋值操作添加特殊机制来解决这个问题,该机制以某种方式允许新结构体与其新副本协商底层资源的所有权——也许它们共享它,或者从旧的转移所有权到新的——但现在你基本上已经进入了C++领域,需要复制构造函数、赋值运算符,并增加了一堆等待陷阱不知情的初学者程序员的微妙之处。请记住,C#的整个重点是尽可能避免那种C++风格的复杂性。

而且,为了让事情变得更加混乱,正如其他答案中指出的那样,结构体不仅存在于局部对象中。对于本地环境,作用域是良好定义的;但是结构体也可以成为类对象的成员。在这种情况下,“析构函数”何时被调用?当然,您可以在容器类最终完成时执行它;但现在您拥有了一个行为非常不同的机制,具体取决于结构体所在的位置:如果结构体是本地的,则在范围结束时立即触发它;如果结构体位于类内,则会懒惰地触发它...因此,如果您真的关心确保某些资源在您的一些结构体中在特定时间内清除,并且如果您的结构体最终可能成为类的成员,则可能需要像IDisposable/using()这样的显式内容来确保您已做好准备。

因此,虽然我不能代表语言设计者发言,但我可以猜测他们决定不包括此功能的一个原因是因为它会引起问题,而他们希望保持C#相对简单。


5
非常棒的回答!对于这种类型的问题,我总是寻求这样的答案,“设计决策”而不是“最终决策”。点赞。 - OmarOthman
9
只有当结构体具有覆盖默认复制构造函数的方式时,析构函数才对结构体有意义。 - supercat
2
@supercat 我完全同意。现在的问题是为什么不允许这样做?在我的情况下,我想要实现一个无需麻烦的引用计数系统,用于对象池中的使用。(为什么?因为在实时应用程序中,例如游戏,非确定性垃圾回收可能会导致死机。对象池可以确保除非存在严重的内存压力或某些显式的“加载”阶段,否则内存将被重复使用,且永远不会被释放。) - Domi
@Domi:如果结构体不能有构造函数,那么数组可以通过填充新分配的内存空间而构建仅为零的数组。支持结构体构造函数将极大地复杂化数组构造,尤其是考虑到以下可能性:(1)结构体可能嵌套在一起,(2)构造函数在初始化数组的过程中可能失败。 - supercat
1
现在你有两个具有相同内部字段的结构体,它们都将尝试清理同一个对象。其中一个会先发生,因此在之后使用另一个的代码将开始出现神秘的故障。如果析构函数只是调用Dispose(false),而且Dispose()允许被多次调用而不会引起问题。那么这是否意味着终结器可以被多次调用而不会引起问题? - David Klempfner
@DavidKlempfner 看看 ObjectDisposedException。 - mBardos

34

来自Jon Jagger的说法:

"结构体无法拥有析构函数。析构函数实际上只是一个伪装成object.Finalize覆盖的方法,而结构体作为值类型,不受垃圾回收的控制。"


简短、直接。你的答案也是唯一一个没有错误地提到堆栈的答案。 - user203570
4
如果它是一个局部变量,或者当它是对象的一部分且对象被垃圾回收时,为什么不在结构体超出作用域时运行finalizer呢?如果一个结构体可以实现IDisposable,那为什么不能有一个finalizer? - David Klempfner
1
如果结构体包含在堆上的对象中,它们不会被垃圾回收吗? - David Klempfner
@Backwards_Dave 不是的。堆上的对象会被垃圾回收。如果你有一个类C的实例{ int x; },你不会说“整数x被垃圾回收”。 - Warty
@Warty 当包含它的对象被垃圾回收时,位于堆上的 int 会发生什么? - David Klempfner
有点迂腐哈哈,垃圾回收适用于整个GC分配。 GC分配中的单个字节不是单独释放的,而是GC分配被释放。 如果您在此处阅读https://github.com/dotnet/docs/blob/main/docs/standard/garbage-collection/fundamentals.md ,垃圾回收适用于整个对象。 有点类似于如果您执行X = malloc(sizeof(someStruct))然后free(X),则不能说您正在释放X中的单个字段F; 您正在释放内存块X或另一种情况是包含F的内存块X; 这样说您正在释放F很奇怪:P - Warty

0
除了数组和字符串之外的所有对象都以相同的方式存储在堆上:一个标头给出关于“与对象相关”的属性信息(其类型、是否由任何活动监视器锁定使用、是否具有非抑制Finalize方法等),以及其数据(意味着类型的所有实例字段的内容(公共,私有和受保护的混合,在派生类型字段之前出现基类字段)。因为每个堆对象都有一个标头,所以系统可以引用任何对象并知道它是什么,以及垃圾回收器应该对其执行什么操作。如果系统拥有所有已创建并具有Finalize方法的对象列表,则可以检查列表中的每个对象,查看其Finalize方法是否未被抑制,并适当地采取行动。

结构体存储时不包含任何头文件;像Point这样具有两个整数字段的结构体只是简单地存储为两个整数。 尽管可以有一个指向结构体的ref(当结构体作为ref参数传递时会创建这样的东西),但使用ref的代码必须知道ref指向哪种类型的结构体,因为ref和结构体本身都没有保存该信息。 此外,堆对象只能由垃圾回收器创建,垃圾回收器将保证任何创建的对象始终存在直到下一个GC周期。 相比之下,用户代码可以自行创建和销毁结构体(通常在堆栈上);如果代码创建了一个结构体以及一个对其的ref,并将该ref传递给被调用的例程,则在被调用的例程返回之前,该结构体无法被销毁(或执行任何操作),因此保证结构体至少存在直到被调用的例程退出。 另一方面,一旦被调用的例程退出,它所给出的ref应被假定为无效,因为调用者随时可以自由地销毁结构体。


-5

因为按照定义,析构函数用于销毁类的实例,而结构体是值类型。

参考:http://msdn.microsoft.com/en-us/library/66x5fx1b.aspx

根据微软自己的说法:"析构函数用于销毁类的实例。"所以问"为什么不能在(不是类的东西)上使用析构函数?"有点傻 ^^


这种“按定义的道路”有点学究气,但我不确定在面试中采取这种方式是否最好。然而,在某些情况下,我可以看到这种方法的优点。当我们被问及“为什么平行线不相交?”时,回答“按定义”似乎比其他回答更合适。 - Valentin Kuzub
问题在于:很多问题可以用这种方式回答,但更好的答案通常也存在。虽然它是完全正确的答案,但在大多数情况下它不会为我们赢得分数。 - Valentin Kuzub
析构函数不会销毁类的实例。尽管它们的名字很不幸,但它们的作用是允许类执行需要执行的操作,而这些操作只有它们自己才能完成。例如,如果“某人”要求系统打开一个文件,系统将创建一个文件句柄并将其交给请求者,并承诺拥有该句柄的人可以独占地访问该文件,直到拥有该句柄的人表示不再需要此访问为止。持有这种句柄的“文件”类有责任确保在不再需要时告知系统。 - supercat
supercat,感谢您提供的额外信息。那么您是说微软在他们自己的定义上是错误的吗?(说实话,这并不让我感到惊讶)。 - Gaijinhunter
微软网站上有很多建议,可能曾经有价值,但现在已远非最佳实践。Finalize比Cleanup更具描述性,但NotifyOfAbandonment会更好。 - supercat
显示剩余2条评论

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