4

I understand why structs can't contain circular references which lead to logical memory problems, but why doesn't a nullable reference circumvent this limitation? For example:

struct Foo
{
    Foo? bar;
}

Obviously this could very easily lead to stack overflows and circular references, if one wasn't careful, but shouldn't bar be a pointer to another Foo instance, and default to null? Or (more likely) do I not understand how nullable value types are laid out in memory?

(My background knowledge consists mainly of information from this question and answers.)

Community
  • 1
  • 1
dlras2
  • 8,416
  • 7
  • 51
  • 90

6 Answers6

9

No, not quite. A nullable value type is really an instance of Nullable<> with a value type as the generic parameter. The question mark is just a shorthand.

Nullable is a struct, and therefore is a value type. Since it retains a reference to the Foo struct, you still have a circular reference consisting of value types.

Andrew
  • 14,325
  • 4
  • 43
  • 64
7

Nullable<T> is a struct which looks like this (excluding constructors etc):

public struct Nullable<T> where T : struct
{
    private readonly T value;
    private readonly bool hasValue;
}

As that's a value type, your Foo would end up looking a bit like this:

struct Foo
{
    Foo barValue;
    bool hasBarValue;
}

Now hopefully it's more obvious that that's a problem :)

Jon Skeet
  • 1,421,763
  • 867
  • 9,128
  • 9,194
  • As I suspected - my problem was not knowing how `Nullable` was laid out in memory. This makes a lot more sense now! – dlras2 Nov 15 '11 at 19:41
  • @RoyiNamir: Yup - think about what it would look like as `Foo` is a value type... – Jon Skeet Nov 15 '11 at 20:01
5

Foo? bar is a shortcut for

Nullable<Foo> bar;

Nullable<T> is a struct that roughly looks like this:

public struct Nullable<T> where T : struct
{
    private readonly T value;
    private readonly bool hasValue;
    //..
}

In the case of Foo, Nullable<Foo> would hold a Foo, which in turn holds a Nullable<Foo> which in turn...

BrokenGlass
  • 158,293
  • 28
  • 286
  • 335
2

As you probably realize, a struct can't have a circular reference because when you lay the struct out in memory, you have to include storage inside the struct for each of its members. A cyclical definition requires an infinite amount of storage:

  • A struct with two Int32 members requires 8 bytes (2 * sizeof(Int32)); similarly, a struct with four Int32 members requires 16 bytes.
  • If a struct S has two Int32 members plus one S member, it would need 2 * sizeof(Int32) + sizeof(S).
  • But if sizeof(S) = 2 * sizeof(Int32) + sizeof(S), we have infinite recursion, and we can't allocate memory for the struct; recursive definitions are therefore illegal.

Now, assume sizeof(Nullable<T>) = sizeof(bool) + sizeof(T) (see Jon Skeet's answer). Consider a struct S with this definition:

struct S
{
    int _someField;
    S? _someOtherField;
}

In this case, sizeof(S) = sizeof(Int32) + sizeof(Nullable<S>).

Replacing sizeof(Nullable<S>) with sizeof(bool) + sizeof(S), we get

sizeof(S) = sizeof(Int32) + sizeof(bool) + sizeof(S)

Again, infinite recursion.

phoog
  • 42,068
  • 6
  • 79
  • 117
1

structs are value types. So, the nested struct creates a memory structure that takes an infinite amount of ram. Classes are reference types. So, the nested class creates a memory structure that could be infinite, but at initialization, it is still small.

Royi Namir
  • 144,742
  • 138
  • 468
  • 792
  • I understand this - I was missing that `Nullable` is a generic struct, not a pointer to `Foo`. – dlras2 Nov 15 '11 at 19:53
1

The compiler is walking the struct to determine if there is a cycle.

While reference types contained in structs will be put in the heap and the compiler will treat them differently when looking for cycles, the real type of a nullable type is Nullable is a struct. So the compiler sees a struct Nullable and considers it a circular reference.

There is a way to get around this - inherit from an interface:

public interface IFoo
{
}

public struct Foo : IFoo
{
    IFoo Foo;
}

Because interfaces are a level of indirection, the compiler will treat your struct like a reference type.

Geoff Cox
  • 6,102
  • 2
  • 27
  • 30