There are two broad categories of variables we'll define:
value types and
reference types.
Value type variables have a strong connection to a single piece of memory stored somewhere. This includes
enumerations, primtive data types (int, bool, char), and programmer-defined structs.
Reference types do not contain values themselves but instead an address to where values can be found.
The simplest example of this is an array: basically a pointer to memory where a contiguous block of memory
is found, containing a collection of homogeneous data. Objects from a class, delegates, strings, and
variables referring to dynamically allocated memory, are all reference types.
"enum" is the keyword for defining an enumeration, a programmer-defined, public type that consists of a set of
named constants called the "enumerator list". For example:
enum Doctor { Tennant, Smith, Capaldi, Whittaker };
Whereby
Doctor.Tennant would have an integer value of 0, Smith is 1, Whittaker is 3.
However, you can alter this indexing scheme by doing the following:
enum Doctor { Tennant = 1, Smith, Capaldi, Whittaker };
Whereby all members of the enumeration would begin counting from 1 instead of zero, for you godless heathens
out there.
In this context, the fact that
Doctor.Tennant has an integer value of 0, does
next to nothing when it comes time to output this information. Instead, you will likely want to do something
like...
System.Console.WriteLine("The current Doctor is {0}", ((Doctor) current).ToString());
Where
current is a
enum type of variable, presumably
with a well-defined value.
Interestingly, the .NET Framework doesn't have any intrinsic check for whether an enumeration value has
been defined or not. For the above example, if I were to assign the value of (Doctor)4, the print statement
would simply print 4. If you're anything like me, then this bothers you, as how might you protect your code
from bad data or (more likely the case) bad user input? This person from
StackOverflow.com devised a series of methods used to check whether an enumeration value is valid and if
not, throws a custom "InvalidCastException". I'm a fan of this work!
For those of you coming from a C++ background, the
object data type should be
new.
object is the most general way to describe
any type of data that is
stored in memory. That is, all memory you use, from integers to strings to instantiations of the Slacker
class, can be referred to quite generally as
objects. A value type variable can be
implicitly converted to the
object type by "boxing" it.
int taxicab = 1729;
object slacker = taxicab; // This "boxes" slacker into an integer type
int slackest = (int)slacker; // This "unboxes" the slacker,
// possible only because it was previously "boxed"
readonly is a subtle qualifer, that functions very similiarly to
const. They both permit a variable to be given an initial value and then to never have that value
altered by your program. But
const variables are statically stored in memory, as
compared to
readonly which acts more like a reference to where that value will
be found. The difference comes whenever you want to update those values, and which assemblies need to be
re-compiled in order to correctly update.
As a rule of thumb: if you're defining a value you expect will truly never change in the future (e.g. there
will only ever be four seasons in any given year, 3.14159 will always approximate pi), define it as
const. For other values that may be subject to change (e.g. background colors, margins,
or the upper limit to the amount of baloney I will tolerate), define them as
readonly
.
nullable types bring an interesting twist to the table. These are extensions to
the normal data types available to us, with the addition of allowing them to take on
null values. To define a variable as
nullable, you can do either of the
following:
float? myVar;
Nullable<float> myOtherVar;
Now, I'm a big advocate for being a lazy programmer, such that you write as little code as possible in order
to achieve your objectives. I will abuse aliases, define an excessive number of functions, and try to
generalize my code as best as possible to minimize how much I type, and certainly to reduce how much I
have to re-type. While my knee-jerk reaction to learning that both declarations serve the same goal, you
should be able to see a value with explicitly typing out the
Nullable<T>
business, because it does a better job of self-documenting your code. It stands out more than just a ?,
which you might overlook as you're browsing through your program for the 4,823
rd time, trying
to figure out why you're getting a
NullPointerException.
Since both declarations work exactly the same way, I expect no one will ever type out
Nullable<T>. Why burden yourself with typing out all those extra characters when a
? suffices? Love yourself enough to use the ?.
You can use the
HasValue property of the
nullable type
to determine if the variable contains a value (true) or is equal to
null (false).
There's also a
Value property that returns the — you guessed it! —
value. It makes sense to only use this if you know it isn't already
null. For
example:
float? alpha = null;
float? beta = 2.718;
if (beta.HasValue) // Would evaluate to true
System.Console.WriteLine(beta.Value);
There's also the ?? notation you can use to define a default value when assigning a
nullable type to a non-
nullable type. For example:
int? slacker = null;
int test = slacker ?? -1;
// This means: if slacker is not null, set test equal to its value. If it is null, set test to -1
Generally speaking, you'll want to use
nullable types as an alternative to
defining sentinel values (i.e. "this integer is said to be undefined if equal to -99999) or whenever
you are managing a database system, as you will often have attributes of tables defined as possibly being
null.