OK... forget about structs for a second. Let's talk only about native
types (like ints, longs, doubles, and floats) and classes.
There are only two places that things are stored in a running .NET
program. (Actually, as Willy pointed out, there are three: static
things are stored elsewhere, but we'll ignore those for now.) Something
can be stored either on the stack, or on the heap.
First, think about the stack. What is it? It is a place to store the
local variables that you declare in methods, and arguments to method
parameters. So, if you have code like this:
public decimal PowerOf(decimal value, int power)
{
decimal result = 1;
for (int i = 0; i < power; i++)
{
result *= power;
}
return result;
}
....
decimal x = PowerOf(2, 8);
Yes, I realize that this is a cheesy example (and the method doesn't
even work for negative powers), but take a look at what's going on here
with respect to the stack.
In the main program, down below, x is allocated on the stack because
it's a local (non-static) variable. 2 and 8 are copied onto the stack,
because they're arguments to PowerOf. Within PowerOf, result and i are
also allocated space on the stack because they're local variables. The
return value from PowerOf is also copied onto space allocated on the
stack, and then copied from that space into the variable x, which as
you recall was allocated space on the stack.
So, in this example, everything is happening on the stack. The heap
isn't involved at all.
A struct would act exactly the same as any of these decimals and ints.
A struct variable declared as a local (non-static) variable would be
allocated space on the stack. A struct passed to a method as an
argument would be _copied_ onto the stack (just as 2 and 8 were copied
onto the stack).
Now let's look at what happens with classes and the heap:
public class MyClass
{
private int classInt = 0;
private decimal classDecimal = 15.0;
public SomeOtherClass DoSomeStuff(YetAnotherClass
yetAnotherInstance)
{
SomeOtherClass someOtherInstance = new SomeOtherClass();
...
return someOtherInstance;
}
}
....
MyClass mine = new MyClass();
SomeOtherClass other = mine.DoSomeStuff(new YetAnotherClass());
Again, a silly method and a silly call, but let's look at what's going
on.
The first thing that happens is that a new instance of MyClass is
created _on the heap_, and a _reference_ to that instance (the space
allocated for the class information on the heap) is saved in "mine",
which is allocated _on the stack_ because it's a local variable. So
what was saved on the heap? Well. every instance of MyClass contains
two class members, "classInt" and "classDecimal". So, space for an int
and a decimal was reserved on the heap, those variables were
intialized, and a reference to that heap location (a pointer, if you
will) was saved in the variable "mine", which is located on the stack.
Every time we create a new instance of MyClass, space for yet another
int and decimal will be created on the heap.
Notice, however, that everything on the heap eventually comes back to
the stack. What is stored on the stack in the case of class instances
are _references_ or _pointers_ to the heap location where the class
information is stored.
The next thing that happens is that we create an instance of
YetAnotherClass in order to pass it to mine.DoSomeStuff. The instance
of YetAnotherClass is allocated space on the heap... space for whatever
class members it declares (we can't see the declaration, so we don't
know how much space it needs). Then, a reference to that instance of
YetAnotherClass is placed on the stack as an argument to DoSomeStuff.
DoSomeStuff creates a new instance of SomeOtherClass. Again, space for
whatever members SomeOtherClass defines is reserved on the heap, and
the members of SomeOtherClass are initialized into that heap space. A
reference (or pointer) to this instance is stored in someOtherInstance
on the stack (because someOtherInstance is a local variable).
When DoSomeStuff returns, it copies the reference (pointer) to the
instance of SomeOtherClass into space allocated on the stack for its
return value. Back in the main program, this reference is copied into
the variable "other", which has space reserved on the stack (because
it's a local variable to the main program). So, we're left with "other"
containing a reference (pointer) to an instance of SomeOtherClass,
which stores its members in space on the heap.
So, now, what about structs? Well, if we were to change classInt and
classDecimal to user-defined struct types, _nothing would change_. When
space was allocated for MyClass on the heap, .NET would allocate enough
space to hold all of the information for the two structs, just as
though they were ints or decimals. It would initialize the space for
those structs with some intial values, just as you would initialize an
int or a decimal, and then it would put a reference to the MyClass
instance's heap space (which contains the information for the two
structs) on the stack. Again, all that goes on the stack in this case
is a simple reference (pointer) to the information on the heap.
As I said: structs act exactly like ints, doubles, floats, or decimals.
When passed as arguments to methods they are copied onto the stack.
When returned from methods, they are returned on the stack. When you
have local variables of a "struct" type, space for the entire struct's
information is allocated on the stack. When you assign them from one
variable to another, they are copied.
When a struct forms part of the information (the "state") for a class
instance, space for that struct is allocated on the heap along with
(and in the same memory as) the ints, doubles, and decimals that make
up the rest of the class's state information.
If you can understand how basic types like ints and decimals are
treated by the compiler and the CLR, then you understand how structs
are treated: exactly the same.