Harlan said:
I see. It's come back to me now from back when I originally learned
about C#. I guess I just haven't found reason to think about it since
then. But in that case, what makes it a value type?
You mean other than the fact that is _is_ a value type?
The short answer is: because it's a "struct" and not a "class".
That's the mechanical explanation for why it's a value type. I'm not
sure what other answer you might be expecting. I mean, I could use the
behavior of System.Int32 to argue that's why it's a value type. But in
reality, it's the other way around. That is, it has that behavior
_because_ it's a value type. The only thing that "makes it a value
type" is the fact that that's how it was implemented in the language and
framework.
It seems that it
doesn't hold a value, but a reference to a changeable place in memory
where a value has been placed.
Unfortunately, I think you don't really have a full understanding of the
concepts of a variable, versus a value type or reference type. It's
hard in this context to provide a good explanation – the forum isn't
really interactive enough for a proper discussion – but I'll try my best.
As I mentioned before, a variable is always mutable (in C#…in other
languages this is not necessarily true). And yes, the _variable_ can be
thought of as "a changeable place in memory where a value has been placed".
It's unfortunate that the words "value" and "reference" have slightly
different meanings depending on the context. But, they do. So
hopefully by pointing that out, I can use those words in their different
ways without making the explanation too confusing. With that said, here
goes nothin'…
The question of "value type" versus "reference type" has nothing to do
with the variable per se, but rather what that "value" that's in the
variable is. Note that all variables have a "value", even variables
that hold a "reference type". This is what I mean by the use of the
words in multiple ways.
Now, a variable holding a value type has as its value the value type
itself. The storage allocated for the variable _is_ the value type
instance itself. You can modify the contents of that storage by
assigning a new instance of a value type to the variable. Or, if the
value type is mutable, you can do something that will ultimately modify
a field in the value type (assign to the field, or call a method or
property that assigns to the field).
But in this case, the storage the variable represents is modified _only_
if you are using that variable representing the value type itself. Any
other way of getting the value type instance (e.g. returned from a
property) will create a copy of the original value, and the operation
will modify the copy, not the value in the original storage location.
A variable holding a reference type has as its value the _reference_ to
the instance of that type, which is actually stored elsewhere. The
storage allocated for the variable is only that reference, while the
storage for the reference type instance is elsewhere (i.e. the heap).
For a reference type variable, if you modify the contents of the
variable, you are simply copying a different reference into the
variable. Nothing happens to the object that variable was previously
referencing. Conversely, if you modify the object the variable was
referencing, you will see that modification immediately when accessing
the instance via that variable _and_ via any other variable that was
referencing that same instance, while nothing at all happens to the
variable's value itself.
So, when we look at the type System.Int32 (i.e. "int"), because it's
declared as a "struct" and not a "class", it is by declaration and by
definition a value type. This gives it the specific value type behavior
described above.
Furthermore, there's nothing in the System.Int32 struct that allows you
to modify an instance of System.Int32. The only thing you can ever do
is assign a new value of System.Int32 to a variable of that type. This
does mutate the _variable_, but not the original instance of
System.Int32 the variable used to hold. A whole new instance of
System.Int32 is copied into the variable.
Thus, System.Int32 is a value type (because it's declared as "struct")
and it's immutable (because there's nothing in the type that allows you
to modify an instance of the type…you can only copy a new instance over
an old one).
I will now digress a bit…
I think part of the problem is that because many programmers are so used
to using primitive types without ever thinking about mutability or
immutability, their internal conceptualization of the behavior of the
types is too imprecise. I know that happened to me when I starting
using C# after years of C++.
The same concepts of mutability can apply in other languages, such as
C++. But C++ doesn't provide the same kind of clear division between
value types and reference types. Instead, any given structure or class
can act as either, depending on usage.
That is, technically in C++ _everything_ is basically a value type (i.e.
you can always store the instance in a variable and copy the whole
instance from one place to another), but because the language includes
an idiom for accessing class members via a pointer, you can effectively
treat a data structure as a reference type by passing that pointer
around instead of copies of the structure itself.
Note that in C++, because everything's a value type, you still have the
same issues with regards to mutability and value types. Consider this code:
class Mutable
{
private:
int _i;
public:
Mutable() : _i(0) { }
int getI() { return _i; }
void setI(int i) { _i = i; }
}
class Holder
{
private:
Mutable _m;
public:
Mutable getM() { return _m; }
void setM(Mutable m) { _m = m; }
}
void Method()
{
Holder holder;
holder.getM().setI(5);
printf("%d", holder.getM().getI());
}
What do you expect the above to print? "5"? Or "0"? Note that nowhere
in the code am I using pointers, and thus everything is being treated as
a value type (accessed via copy or variable).
We could instead make the Mutable class immutable (and of course change
the name to match). Then you would be forced to make changes in a way
that have more obvious semantics:
class Immutable
{
private:
int _i;
public:
Immutable(int i) { _i = i; }
int getI() { return _i; }
}
class Holder
{
private:
Immutable _m;
public:
Holder() : _m(0) { }
Immutable getM() { return _m; }
void setM(Immutable m) { _m = m; }
}
void Method()
{
Holder holder;
// This no longer is possible:
// holder.getM().setI(5);
// This has no effect, for obvious reasons:
holder.getM() = Immutable(5);
// Instead, you have to do this:
holder.setM(Immutable(5));
// And so the result of this statement is
// much easier to predict (because it's more
// consistent with the likely expectation
// of the programmer when doing operations
// that look like they are changing things):
printf("%d", holder.getM().getI());
}
And finally, note in the above that in C++, the type "int" is also
immutable. It's probably immutable in an even less interesting way than
in C#, because the type doesn't even have any members (at least in C#,
there's the _possibility_ of a method or property in System.Int32 that
could modify an instance of the type, even though of course none
actually exists). But just like in C#, you cannot modify a given
instance of "int"; you can only modify a _variable_ holding an instance
of "int", by copying a whole new instance of "int" into the variable.
Pete