Chapter 2
IN THIS CHAPTER
Using C# variables, such as integers, as storage lockers
Declaring other types of variables — dates, characters, strings
Handling numeric constants
Changing types and letting the compiler figure out the type
The most fundamental of all concepts in programming is that of the variable. A C# variable is like a small box in which you can store things, particularly numbers, for later use. (The term variable is borrowed from the world of mathematics.)
Unfortunately for programmers, C# places several limitations on variables — limitations that mathematicians don’t have to consider. However, these limits are in place for a reason. They make it easier for C# to understand what you mean by a particular kind of variable and for you to find mistakes in your code. This chapter takes you through the steps for declaring, initializing, and using variables. It also introduces several of the most basic data types in C#.
Mathematicians work with numbers in a precise manner, but in a way that C# could never understand. The mathematician is free to introduce the variables as needed to present an idea in a particular way. Mathematicians use algorithms, a set of procedural steps used to solve a problem, in a way that makes sense to other mathematicians to model real-world needs. Algorithms can appear quite complex, even to other humans, much less C# (it doesn’t understand algorithms except what you tell it in code). For example, the mathematician may say this:
x = y2 + 2y + 1
if k = y + 1 then
x = k2
Programmers must define variables in a particular way that’s more demanding than the mathematician’s looser style. A programmer must tell C# the kind of value that a variable contains and then tell C# specifically what to place in that variable in a manner that C# understands. For example, a C# programmer may write the following bit of code:
int n;
n = 1;
The first line means, “Carve off a small amount of storage in the computer’s memory and assign it the name n.” This step is analogous to reserving one of those storage lockers at the train station and slapping the label n on the side. The second line says, “Store the value 1 in the variable n
, thereby replacing whatever that storage location already contains.” The train-locker equivalent is, “Open the train locker, rip out whatever happens to be in there, and shove a 1 in its place.”
In C#, each variable has a fixed type. When you allocate one of those train lockers, you have to pick the size you need. If you pick an integer locker, for instance, you can’t turn around and hope to stuff the entire state of Texas in it — maybe Rhode Island, but not Texas.
For the example in the preceding section of this chapter, you select a locker that’s designed to handle an integer — C# calls it an int
. Integers are the counting numbers 1, 2, 3, and so on, plus 0 and the negative whole numbers –1, –2, –3, and so on.
// Declare a variable named n - an empty train locker.
int n;
// Declare an int variable m and initialize it with the value 2.
int m = 2;
// Assign the value stored in m to the variable n.
n = m;
The first line after the comment is a declaration that creates a little storage area, n
, designed to hold an integer value. The initial value of n
is not specified until it is assigned a value, so this locker is essentially empty. The second declaration not only declares an int
variable m
but also initializes it with a value of 2, all in one shot.
The final statement in the program assigns the value stored in m
, which is 2, to the variable n
. The variable n
continues to contain the value 2 until it is assigned a new value. (The variable m
doesn't lose its value when you assign its value to n
. It’s like cloning m
.)
You can initialize a variable as part of the declaration, like this:
// Declare another int variable and give it the initial value of 1.
int p = 1;
This is equivalent to sticking a 1 into that int
storage locker when you first rent it, rather than opening the locker and stuffing in the value later.
// The following is illegal because m is not assigned
// a value before it is used.
int n = 1;
int m;
n = m;
// The following is illegal because p has not been
// declared before it is used.
p = 2;
int p;
Finally, you cannot declare the same variable twice in the same scope (a function, for example).
Most simple numeric variables are of type int
. However, C# provides a number of twists to the int
variable type for special occasions.
All integer variable types are limited to whole numbers. The int
type suffers from other limitations as well. For example, an int
variable can store values only in the range from roughly –2 billion to 2 billion.
A distance of 2 billion inches is greater than the circumference of the Earth. In case 2 billion isn't quite large enough for you, C# provides an integer type called long
(short for long int
) that can represent numbers almost as large as you can imagine. The only problem with a long
is that it takes a larger train locker: A long
consumes 8 bytes (64 bits) — twice as much as a garden-variety 4-byte (32-bit) int
. C# provides several other integer variable types, as shown in Table 2-1.
TABLE 2-1 Size and Range of C# Integer Types
Type | Bytes | Range of Values | In Use |
---|---|---|---|
sbyte | 1 | –128 to 127 | sbyte sb = 12; |
byte | 1 | 0 to 255 | byte b = 12; |
short | 2 | –32,768 to 32,767 | short sh = 12345; |
ushort | 2 | 0 to 65,535 | ushort ush = 62345; |
int | 4 | –2,147,483,648 to 2,147,483,647 | int n = 1234567890; |
uint | 4 | 0 to 4,294,967,295 | uint un = 3234567890U; |
long | 8 | –9,223,372,036,854,775,808 to 9,223,372,036,854,775,807 | long l = 123456789012L; |
ulong | 8 | 0 to 18,446,744,073,709,551,615 | ulong ul = 123456789012UL; |
As explained in the section entitled “Declaring Numeric Constants,” later in this chapter, fixed values such as 1 also have a type. By default, a simple constant such as 1 is assumed to be an int, unless the value won't fit in an int
, in which case the compiler automatically selects the next largest type. Constants other than an int must be marked with their variable type. For example, 123U
is an unsigned integer, uint
.
Most integer variables are called signed, which means they can represent negative values. Unsigned integers can represent only positive values, but you get twice the range in return. As you can see from Table 2-1, the names of most unsigned integer types start with a u
, while the signed types generally don't have a prefix.
Integers are useful for most calculations. However, many calculations involve fractions, which simple integers can't accurately represent. The common equation for converting from Fahrenheit to Celsius temperatures demonstrates the problem, like this:
// Convert the temperature 41 degrees Fahrenheit.
int fahr = 41;
int celsius = (fahr - 32) * (5 / 9);
This equation works just fine for some values. For example, 41 degrees Fahrenheit is 5 degrees Celsius.
Okay, try a different value: 100 degrees Fahrenheit. Working through the equation, 100–32 is 68; 68 times 5 is 340; 340 / 9 is 37 when using integers. However, a closer answer is 37.78. Even that’s wrong because it’s really 37.777 … with the 7s repeating forever.
For temperatures, 37 may be good enough. It’s not like you wear short-sleeved shirts at 37.7 degrees but pull on a sweater at 37 degrees. But integer truncation is unacceptable for many, if not most, applications.
Actually, the problem is much worse than that. An int
can't handle the ratio 5/9 either; it always yields the value 0. Consequently, the equation as written in this example calculates celsius
as 0 for all values of fahr
.
The limitations of an int
variable are unacceptable for some applications. The range generally isn't a problem — the double-zillion range of a 64-bit-long integer should be enough for almost anyone. However, the fact that an int
is limited to whole numbers is a bit harder to swallow.
In some cases, you need numbers that can have a nonzero fractional part. Mathematicians call these real numbers. (Somehow that always seemed like a ridiculous name for a number. Are integer numbers somehow unreal?)
Fortunately, C# understands real numbers. Real numbers come in two flavors: floating-point and decimal. Floating-point is the most common type. You can find a description of the decimal type in the section “Using the Decimal Type: Is It an Integer or a Float?” later in this chapter.
A floating-point variable carries the designation float
, and you declare one as shown in this example:
float f = 1.0;
After you declare it as float
, the variable f
is a float
for the rest of its natural lifetime.
Table 2-2 describes the two kinds of floating-point types. All floating-point variables are signed. (There's no such thing as a floating-point variable that can’t represent a negative value.)
TABLE 2-2 Size and Range of Floating-Point Variable Types
Type | Bytes | Range of Values | Accuracy to Number of Digits | In Use |
---|---|---|---|---|
float | 8 | 1.5 * 10–45 to 3.4 * 1038 | 6 to 7 | float f = 1.2F; |
double | 16 | 5.0 * 10–324 to 1.7 * 10308 | 15 to 16 | double d = 1.2; |
The Accuracy column in Table 2-2 refers to the number of significant digits that such a variable type can represent. For example, 5/9 is actually 0.555 … with an unending sequence of 5s. However, a float
variable is said to have six significant digits of accuracy — which means that numbers after the sixth digit are ignored. Thus 5/9 may appear this way when expressed as a float
:
0.5555551457382
Here you know that all the digits after the sixth 5 are untrustworthy.
The same number — 5/9 — may appear this way when expressed as a double
:
0.55555555555555557823
The double
packs a whopping 15 to 16 significant digits.
double celsius = (fahr - 32.0) * (5.0 / 9.0);
You may be tempted to use floating-point variables all the time because they solve the truncation problem so nicely. Sure, they use up a bit more memory. But memory is cheap these days, so why not? But floating-point variables also have limitations, which you discover in the following sections.
You can’t use floating-point variables as counting numbers. Some C# structures need to count (as in 1, 2, 3, and so on). You know that 1.0, 2.0, and 3.0 are counting numbers just as well as 1, 2, and 3, but C# doesn’t know that. For example, given the accuracy limitations of floating-points, how does C# know that you aren’t actually saying 1.000001?
You have to be careful when comparing floating-point numbers. For example, 12.5 may be represented as 12.500001. Most people don’t care about that little extra bit on the end. However, the computer takes things extremely literally. To C#, 12.500000 and 12.500001 are not the same numbers.
So, if you add 1.1 to 1.1, you can’t tell whether the result is 2.2 or 2.200001. And if you ask, “Is doubleVariable
equal to 2.2?” you may not get the results you expect. Generally, you have to resort to some bogus comparison like this: “Is the absolute value of the difference between doubleVariable
and 2.2 less than .000001?” In other words, “within an acceptable margin of error.”
Integers are always faster than floats to use because integers are less complex. Just as you can calculate the value of something using whole numbers a lot faster than using those pesky decimals, so can processors work faster with integers.
Unfortunately, modern processors are so complex that you can’t know precisely how much time you save by using integers. Just know that using integers is generally faster, but that you won’t actually see a difference unless you’re performing a long list of calculations.
In the past, a floating-point variable could represent a considerably larger range of numbers than an integer type. It still can, but the range of the long
is large enough to render the point moot much of the time.
As explained in previous sections of this chapter, both the integer and floating-point types have their problems. Floating-point variables have rounding problems associated with limits to their accuracy, while int
variables just lop off the fractional part of a variable. In some cases, you need a variable type that offers the best of both worlds:
Fortunately, C# provides such a variable type, called decimal
. A decimal
variable can represent a number between 10–28 and 1028 — which represents a lot of zeros! And it does so without rounding problems unless you're dealing with extremely large numbers.
Decimal variables are declared and used like any variable type, like this:
decimal m1 = 100; // Good
decimal m2 = 100M; // Better
The first declaration shown here creates a variable m1
and initializes it to a value of 100
. What isn't obvious is that 100 is actually of type int
. Thus, C# must convert the int
into a decimal
type before performing the initialization. Fortunately, C# understands what you mean — and performs the conversion for you.
The declaration of m2
is the best. This clever declaration initializes m2
with the decimal
constant 100M
. The letter M at the end of the number specifies that the constant is of type decimal
. No conversion is required. (See the section “Declaring Numeric Constants,” later in this chapter.)
The decimal
variable type seems to have all the advantages and none of the disadvantages of int
or double
types. Variables of this type have a very large range, they don't suffer from rounding problems, and 25.0 is 25.0 and not 25.00001.
The decimal
variable type has two significant limitations, however. First, a decimal
is not considered a counting number because it may contain a fractional value. Consequently, you can't use them in flow-control loops, as explained in Chapter 5 of this minibook.
The second problem with decimal
variables is equally serious or even more so. Computations involving decimal
values are significantly slower than those involving either simple integer or floating-point values. On a crude benchmark test of 300,000,000 adds and subtracts, the operations involving decimal
variables were approximately 50 times slower than those involving simple int
variables. The relative computational speed gets even worse for more complex operations. Besides that, most computational functions, such as calculating sines or exponents, are not available for the decimal
number type.
Clearly, the decimal
variable type is most appropriate for applications such as banking, in which accuracy is extremely important but the number of calculations is relatively small.
Finally, here's a logical variable type, one that can help you get to the truth of the matter. The Boolean type bool
can have two values: true
or false
.
You declare a bool
variable this way:
bool thisIsABool = true;
No direct conversion path exists between bool
variables and any other types. In other words, you can't convert a bool
directly into something else. (Even if you could, you shouldn’t because it doesn’t make any sense.) In particular, you can’t convert a bool
into an int
(such as false
becoming 0) or a string
(such as false
becoming the word “false”).
A program that can do nothing more than spit out numbers may be fine for mathematicians, accountants, insurance agents with their mortality figures, and folks calculating cannon-shell trajectories. (Don't laugh. The original computers were built to generate tables of cannon-shell trajectories to help artillery gunners.) However, for most applications, programs must deal with letters as well as numbers.
C# treats letters in two distinctly different ways: individual characters of type char
(usually pronounced char, as in singe or burn) and strings of characters — a type called, cleverly enough, string
.
The char
variable is a box capable of holding a single character. A character constant appears as a character surrounded by a pair of single quotation marks, as in this example:
char c = 'a';
You can store any single character from the Roman, Hebrew, Arabic, Cyrillic, and most other alphabets. You can also store Japanese katakana and hiragana characters, as well as many Japanese and Chinese kanjis.
In addition, char
is considered a counting type. That means you can use a char
type to control the looping structures described in Chapter 5 of this minibook. Character variables do not suffer from rounding problems.
Some characters within a given font are not printable, in the sense that you don’t see anything when you look at them on the computer screen or printer. The most obvious example of this is the space, which is represented by the character ' ' (single quotation mark, space, single quotation mark). Other characters have no letter equivalent — for example, the tab character. C# uses the backslash to flag these characters, as shown in Table 2-3.
TABLE 2-3 Special Characters
Character Constant | Value |
---|---|
' ' | Newline |
' ' | Tab |
' ' | Null character |
' ' | Carriage return |
'' | Backslash |
Another extremely common variable type is the string
. The following examples show how you declare and initialize string
variables:
// Declare now, initialize later.
string someString1;
someString1 = "this is a string";
// Or initialize when declared - preferable.
string someString2 = "this is a string";
A string
constant, often called a string literal, is a set of characters surrounded by double quotation marks. The characters in a string
can include the special characters shown in Table 2-3. A string cannot be written across a line in the C# source file, but it can contain the newline character, as the following examples show (see boldface):
// The following is not legal.
string someString = "This is a line
and so is this";
// However, the following is legal.
string someString = "This is a line
and so is this";
When written out with Console.WriteLine()
, the last line in this example places the two phrases on separate lines, like this:
This is a line
and so is this
A string
is not a counting type. A string
is also not a value type — no “string” exists that's intrinsic (built in) to the processor. A computer processor understands only numbers, not letters. The letter A is actually the number 65 to the processor. Only one of the common operators works on string
objects: The +
operator concatenates two strings into one. For example:
string s = "this is a phrase"
+ " and so is this";
These lines of code set the string
variable s
equal to this character string:
"this is a phrase and so is this"
string mySecretName = String.Empty; // A property of the String type
By the way, all the other data types in this chapter are value types. The string
type, however, is not a value type, as explained in the following section. Chapter 3 of this minibook goes into much more detail about the string
type.
The programmer-defined types explained in Chapter 8 of this minibook, known as reference types, are neither value types nor intrinsic. The string
type is a reference type, although the C# compiler does accord it some special treatment because string
types are so widely used.
Although strings deal with characters, the string
type is amazingly different from the char
. Of course, certain trivial differences exist. You enclose a character with single quotation marks, as in this example:
'a'
On the other hand, you put double quotation marks around a string:
"this is a string"
"a" // So is this -- see the double quotes?
The rules concerning strings are not the same as those concerning characters. For one thing, you know right up front that a char
is a single character, and that's it. For example, the following code makes no sense, either as addition or as concatenation:
char c1 = 'a';
char c2 = 'b';
char c3 = c1 + c2;
A string, on the other hand, can be any length. So concatenating two strings, as shown here, does make sense:
string s1 = "a";
string s2 = "b";
string s3 = s1 + s2; // Result is "ab"
As part of its library, C# defines an entire suite of string operations. You find these operations described in Chapter 3 of this minibook.
What if you had to write a program that calculates whether this year is a leap year?
The algorithm looks like this:
It's a leap year if
year is evenly divisible by 4
and, if it happens to be evenly divisible by 100,
it's also evenly divisible by 400
You don’t have enough tools yet to tackle that in C#. But you could just ask the DateTime
type (which is a value type, like int
):
DateTime thisYear = new DateTime(2020, 1, 1);
bool isLeapYear = DateTime.IsLeapYear(thisYear.Year);
The result for 2020 is true
, but for 2021, it's false
. (For now, don’t worry about that first line of code, which uses some things you haven’t gotten to yet.)
With the DateTime
data type, you can do something like 80 different operations, such as pull out just the month; get the day of the week; add days, hours, minutes, seconds, milliseconds, months, or years to a given date; get the number of days in a given month; and subtract two dates.
The following sample lines use a convenient property of DateTime
called Now
to capture the present date and time, and one of the numerous DateTime
methods that let you convert one time into another:
DateTime thisMoment = DateTime.Now;
DateTime anHourFromNow = thisMoment.AddHours(1);
You can also extract specific parts of a DateTime
:
int year = DateTime.Now.Year; // For example, 2021
DayOfWeek dayOfWeek = DateTime.Now.DayOfWeek; // For example, Sunday
If you print that DayOfWeek
object, it prints something like “Sunday.” And you can do other handy manipulations of DateTimes
:
DateTime date = DateTime.Today; // Get just the date part.
TimeSpan time = thisMoment.TimeOfDay; // Get just the time part.
TimeSpan duration = new TimeSpan(3, 0, 0, 0); // Specify length in days.
DateTime threeDaysFromNow = thisMoment.Add(duration);
The first two lines just extract portions of the information in a DateTime
. The next two lines add a duration (length of time) to a DateTime
. A duration differs from a moment in time; you specify durations with the TimeSpan
class, and moments with DateTime
. So the third line sets up a TimeSpan
of three days, zero hours, zero minutes, and zero seconds. The fourth line adds the three-day duration to the DateTime
representing right now, resulting in a new DateTime
whose day component is three greater than the day component for thisMoment
.
Subtracting a DateTime
from another DateTime
(or a TimeSpan
from a DateTime
) returns a DateTime
:
TimeSpan duration1 = new TimeSpan(1, 0, 0); // One hour later.
// Since Today gives 12:00:00 AM, the following gives 1:00:00 AM:
DateTime anHourAfterMidnight = DateTime.Today.Add(duration1);
Console.WriteLine("An hour after midnight will be {0}", anHourAfterMidnight);
DateTime midnight = anHourAfterMidnight.Subtract(duration1);
Console.WriteLine("An hour before 1 AM is {0}", midnight);
The first line of the preceding code creates a TimeSpan
of one hour. The next line gets the date (actually, midnight this morning) and adds the one-hour span to it, resulting in a DateTime
representing 1:00 a.m. today. The next-to-last line subtracts a one-hour duration from 1:00 a.m. to get 12:00 a.m. (midnight).
There are very few absolutes in life; however, C# does have an absolute: Every expression has a value and a type. In a declaration such as int n
, you can easily see that the variable n
is an int
. Further, you can reasonably assume that the type of a calculation n + 1
is an int
. However, what type is the constant 1
?
The type of a constant depends on two things: its value and the presence of an optional descriptor letter at the end of the constant. Any integer type between the values of –2,147,483,648 to 2,147,483,647 is assumed to be an int
. Numbers larger than 2,147,483,647 are assumed to be long
. Any floating-point number is assumed to be a double
.
Table 2-4 demonstrates constants that have been declared to be of a particular type. The case of these descriptors is not important; 1U
and 1u
are equivalent.
TABLE 2-4 Common Constants Declared along with Their Types
Constant | Type |
---|---|
1 | int |
1U | unsigned int |
1L | long int (avoid lowercase l; it's too much like the digit 1) |
1.0 | double |
1.0F | float |
1M | decimal |
true | bool |
false | bool |
'a' | char |
' ' | char (the character newline) |
'x123' | char (the character whose numeric value is hex 123)1 |
"a string" | string |
"" | string (an empty string); same as String.Empty |
1 “hex” is short for hexadecimal (numbers in base 16 rather than in base 10).
Humans don’t treat different types of counting numbers differently. For example, a normal person (as distinguished from a C# programmer) doesn’t think about the number 1 as being signed, unsigned, short, or long. Although C# considers these types to be different, even C# realizes that a relationship exists between them. For example, this bit of code converts an int
into a long
:
int intValue = 10;
long longValue;
longValue = intValue; // This is OK.
An int
variable can be converted into a long
because any possible value of an int
can be stored in a long
— and because they are both counting numbers. C# makes the conversion for you automatically without comment. This is called an implicit type conversion.
A conversion in the opposite direction can cause problems, however. For example, this line is illegal:
long longValue = 10;
int intValue;
intValue = longValue; // This is illegal.
But what if you know that the conversion is okay? For example, even though longValue
is a long
, maybe you know that its value can't exceed 100 in this particular program. In that case, converting the long
variable longValue
into the int
variable intValue
would be okay.
You can tell C# that you know what you're doing by means of a cast:
long longValue = 10;
int intValue;
intValue = (int)longValue; // This is now OK.
In a cast, you place the name of the type you want in parentheses and put it immediately in front of the value you want to convert. This cast forces C# to convert the long
named longValue
into an int
and assumes that you know what you're doing. In retrospect, the assertion that you know what you’re doing may seem overly confident, but it’s often valid.
A counting number can be converted into a floating-point number automatically, but converting a floating-point into a counting number requires a cast:
double doubleValue = 10.0;
long longValue = (long)doubleValue;
All conversions to and from a decimal
require a cast. In fact, all numeric types can be converted into all other numeric types through the application of a cast. Neither bool
nor string
can be converted directly into any other type.
So far in this book — well, so far in this chapter — when you declared a variable, you always specified its exact data type, like this:
int i = 5;
string s = "Hello C#";
double d = 1.0;
You're allowed to offload some of that work onto the C# compiler, using the var
keyword:
var i = 5;
var s = "Hello C# 4.0";
var d = 1.0;
Now the compiler infers the data type for you — it looks at the stuff on the right side of the assignment to see what type the left side is.
var x = 3.0 + 2 - 1.5;
The compiler can figure out that x
is a double
value. It looks at 3.0
and 1.5
and sees that they're of type double
. Then it notices that 2
is an int
, which the compiler can convert implicitly to a double
for the calculation. All the additional terms in x
's initialization expression end up as double
types. So the inferred type of x
is double
.
But now, you can simply utter the magic word var
and supply an initialization expression, and the compiler does the rest:
var aVariable = <initialization expression here>;
What’s really lurking in the variables declared in this example with var
? Take a look at this:
var aString = "Hello C# 3.0";
Console.WriteLine(aString.GetType().ToString());
The mumbo jumbo in that WriteLine()
statement calls the String.GetType()
method on aString
to get its C# type. Then it calls the resulting object's ToString()
method to display the object’s type. Here’s what you see in the console window:
System.String
The output from this code proves that the compiler correctly inferred the type of aString
.
You see examples later in which var
is definitely called for, and you use it part of the time throughout this book, even sometimes where it's not strictly necessary. You need to see it used, and use it yourself, to internalize it.
What’s more, the dynamic
type takes var
a step further. The var
type causes the compiler to infer the type of the variable based on the initialization value. The dynamic
keyword does this at runtime, using a set of tools called the Dynamic Language Runtime. You can find more about the dynamic
type in Chapter 6 of Book 3.
3.16.130.201