【问题标题】:Cannibal Classes食人族课程
【发布时间】:2009-03-16 18:14:58
【问题描述】:

一段时间以来,我一直在试图弄清楚为什么允许编译某些“食人”类的原因。

在我继续之前,也许我应该解释一下我所说的“食人族”课程。不确定我是否刚刚发明了这个术语,或者它是否已经存在了一段时间,或者我是否正确使用它,但现在这并不重要。

我基本上将食人者类称为消耗自身的类。换句话说,一个类的接口声明了它自己类型的成员。例如:

class Foo
{
    public Foo SomeFoo; 
}

正如您在上面看到的,类 Foo 有一个 Foo 类型的成员(本身)。

现在,我第一次看到这个(很久以前)我没想到它会编译,但令我惊讶的是它确实编译了。我之所以不认为它会编译,是因为对我来说,这是一种递归的噩梦。

为了让事情更复杂一点,我决定尝试同样的方法,但将类设为结构体,例如:

struct Foo
{
    public Foo SomeFoo; 
}

不幸的是,这不会编译,而是会出现错误:'Foo' 类型的结构成员 'Foo.SomeFoo' 导致结构布局中出现循环

对我来说,编译错误比没有错误更有意义,但我确信对这种行为有一个合乎逻辑的解释,所以我想知道你们中是否有人可以解释这种行为。

谢谢。

【问题讨论】:

  • LOL 食人族类。这让我笑了:)
  • 从来没有听说过它通常被称为食人族,但我更喜欢食人族。

标签: c# .net


【解决方案1】:

你不能设计这样的结构的原因是因为结构必须在分配一些默认值时进行初始化。所以,当你有一个像你描述的结构 Foo 并创建一个 Foo...

Foo x; // x = default(Foo)

它调用Foo 的默认构造函数。但是,该默认构造函数必须为 Foo.SomeFoo 字段提供默认值。

嗯,它是怎么找到的?它必须调用default(Foo)。其中,为了构造一个Foo,必须为Foo.SomeFoo 字段提供一个默认值......正如你所猜测的,这是一个递归的噩梦。

由于您可以创建对类的空引用,并避免立即实际创建该类的实例,所以没有问题;你可以打电话给new Foo()Foo.SomeFoo 将只是空。不需要递归。

附录:当您考虑这个问题时,如果您仍然感到困惑,我认为其他答案有另一种考虑它的好方法(相同的基本问题,不同的方法) - 当程序为Foo分配内存,它应该怎么做?当Foo 包含一些东西,然后是另一个完整的Foo 时,sizeof(Foo) 是什么?你不能那样做。

相反,如果它是一个类,它只需要分配几个字节来引用Foo,而不是实际的Foo,这没有问题。

【讨论】:

  • 总结一下:class= 引用类型,struct = 值类型。您不能布局包含自身的类型的内存映射。但是,引用类型将只包含内存引用,而不是实际的数据结构。因此,没有必要在编译时知道该类型的结构。
【解决方案2】:

不同之处在于 Foo 是一个引用类型。 类内存布局将有一个指向 Foo 实例的指针,这样就可以了。

在结构的情况下,你基本上有一个无限递归的内存布局,这是行不通的。

现在,如果您的类尝试在其自己的构造函数中实例化 Foo,那么您将遇到堆栈溢出问题。

【讨论】:

  • 这是正确答案。值类型和引用类型的区别,也就是结构体和类的区别。默认值问题是由此产生的结果。
【解决方案3】:

它们被称为递归类型,它们很常见。例如,一棵树通常根据引用其他节点的“节点”来实现。这没有什么自相矛盾的地方,只要它们引用彼此但不包含彼此。

作为理解这一点的一种方法,整数的平方始终是另一个整数。这没什么奇怪的,它只是两个整数之间的引用或关系。但是你不能有一个 包含 自身完整副本作为子字符串的字符串。这就是区别。

【讨论】:

  • 我刚刚发明了一个包含自身副本的东西......我想我会称之为分形! :)
  • 啊,但是分形包含自己的完整副本。相反,分形的部分可以在某种映射下与整体相对应,直到某个 epsilon。
【解决方案4】:
class Foo
{
    public Foo SomeFoo; 
}

本示例中的 SomeFoo 只是一个链接 - 不会造成 递归创建问题
正因为如此,犬类才能存在。

【讨论】:

    【解决方案5】:

    好的,我明白了。

    所以为了强调这一点,如果我理解正确,编译器并不关心成员是 Foo、Bar 还是其他类型。编译器只需要知道它需要为该成员变量分配的字节数。

    因此,如果将其编译为 32 位操作系统,那么我假设编译器在技术上将 Foo 类型的声明更改为:

    class Foo
    {
        public Int32 SomeFoo; 
    }
    

    编译器实际上看到的不是“Foo”,而是 32 位(有点)。

    谢谢。

    【讨论】:

    • 最终,在记忆方面,是的。
    【解决方案6】:

    要了解为什么允许这样做以及它如何有用,经典的链表实现就是一个很好的例子,请查看本教程

    http://cyberkruz.vox.com/library/post/c-tutorial-linked-list.html

    你看,他们定义了一个节点类如下:

    public class LinkedList
    {
    
        Node firstNode;
    
        public class Node
        {
    
            Node previous;
            Node next;
            int value;
        }
    }
    

    每个节点都指向上一个和下一个节点,因此将其视为指向另一个对象的链接是一个好的开始。

    如果你以前没有做过链表,我强烈推荐它,因为它是一个很好的练习。

    【讨论】:

      【解决方案7】:

      那么你把单例实现称为犬类吗?

      public class ShopSettings
      {
        public static ShopSettings Instance
        {
          get
          {
            if (_Instance == null)
            {
              _Instance = new ShopSettings();
            }
      
            return _Instance;
          }
        }
      }
      

      【讨论】:

      • -1,不,你的实例是静态的,可以完全避免这个问题。
      【解决方案8】:

      正如他们所说,它是一个值类型,因此它不能包含自己,因为它不能工作(想想看)。

      但是,您可以执行以下操作:

      unsafe struct Foo
      {
        public Foo* SomeFoo;
      }
      

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 2021-04-25
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2022-01-26
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多