2. Expressions and Assignment Statements

Recall that an assignment statement is used to store a value in a variable, and looks like this:

variable-name = expression;

When I first introduced assignment statements, I told you that C# requires that the data type of the expression be compatible with the data type of the variable (on the left side). Thus, if x is an int variable, x = 5 is legal, but x = "Jon" is not.

We need to dig into this rule a little bit, because until you understand it well, you will have difficulty when you are working with expressions that include variables of different data types, which happens all the time in C#. There are two parts to consider: "the data type of the expression" and "compatible with the data type of the variable".

2.1. Determining the Data Type of an Expression

An expression, as you know, computes a value, and that value has a data type. By "data type of an expression," I am referring to the data type of the value produced by the expression. For example, the expression 5 + 5 yields the int value 10, and the expression 2.0 * 3.0 yields the double value 6.0.

It's pretty easy to determine the data type of a simple expressions, such as a literal or a single variable. But what happens when you start mixing types? More complicated expressions can present a challenge, but if you learn to tackle them in a systematic way, you can easily analyze them to determine their type. First, here are the two rules governing numeric expressions:

  • A mathematical expression that consists only of integer types will produce an int result. When a long value is involved, the result is long.

  • A mathematical expression that contains at least one double or float value will always produce a floating point result. The resulting data type depends on the "largest" data type in the expression. For example, if a double value is involved, then the result is a double. If only float values (and possibly integer values) are used, then the result is a float.

  • An expression that contains at least one string value will always produce a string result.

Let's take a look at some examples. We'll use the following variables:

int int1, int2;
long long1, long2;
float float1, float2;
double double1, double2;
string str1, str2;

Here is the first one:

int1 + int2 - 5

This one is easy. Only int values are involved; the result is an int.

int1 + long1

Still only integer types, but since a long is involved, the result is a long.

long1 + float1 - int2

Since a float is involved, the result must be floating point. No double value, so the result is a float.

int1 + int2 - (long1 * float1) / double2 * float2

Again, since floats and doubles are involved, the result must be a floating point type. Since a double is involved, the result is double.

"Fred weighs " + double1 + " pounds."

Since a string is involved, the result is a string.

[Tip]Tip

A good rule of thumb to help you predict the type of a mathematical expression is to look for the most "comprehensive" data type in the expression, and expect that the result will be of that type. If you see a string anywhere, expect that the result will be a string.

2.2. Data Type Compatibility

In an assignment statement, the data type of the expression must be compatible with the data type of the variable being changed. I use the word "compatible" because the two types don't have to be identical. Let me give you an example to explain what I mean.

int i;
double d;

d = i; // ok
i = d; // not ok

In this fragment, the compiler would permit the first assignment statement, but not the second. Here's why. An int expression is "assignment compatible" with a double variable, but a double expression is not "assignment compatible" with an int variable. The reason that one is allowed and the other is not has to do with information loss. Any int value can be safely stored in a double variable with no loss of information. However, a double value cannot be safely stored in an int variable, because an int variable cannot hold digits after the decimal point. In other words, C# allows automatic data type conversions any time that information loss is avoided.

Perhaps you have encountered an error message like this:

Test.cs(4,18): Cannot implicitly convert type 'double' to 'int'

This is the error that my compiler issued for the second assignment statement above. To avoid errors like this, you must observe the rules on assignment compatibility:

  • A smaller integer-type expression can be stored in a larger integer-type variable. For example, an int expression can be stored in a long variable.

  • Any integer-type expression (byte, short, int, long) can be stored in a float or double variable.

  • A float expression can be stored in a double variable.

  • Other combinations are not legal.

[Tip]Tip

If the numeric variable being changed in an assignment statement has the same data type or a more "comprehensive" data type than the type of the expression, the assignment is usually ok.

2.3. Forcing Square Pegs into Round Holes

There are times when you have an expression that produces a result whose type is not compatible with the variable you want to hold the result. In other words, there are legitimate reasons to want to convert data from one type to another. In these cases, you need a way to tell C# to force the conversion, losing information if necessary. You do this with something called a cast. In our example, here's how it would work:

i = (int) d;

When you write a data type in parenthesis in front of an expression, C# computes the result of the expression as it normally would, and then after the result is computed, converts it to the indicated type. For example, if d were 3.2532, the (int) cast would convert the expression to 3, so it can be stored in the int variable. Note that the cast does not affect any variables in the expression; it only affects the result of the expression. So d's value is not changed.

The cast is actually an operator. It has a very high precedence -- higher than all the arithmetic operators. When your expression is more complicated than a single variable, you should enclose the expression in parenthesis. Take the following as an example:

i = i + d;

This assignment statement is invalid, because the type of the expression is double. You might try to correct the problem by adding a cast:

i = (int) i + d;

But the compiler will still complain that the expression produces a double. What's going on here? To understand the problem, take a look at the assignment statement rewritten with the expression fully parenthesized:

i = ((int) i) + d;

This statement is equivalent to the previous one, and highlights the problem. The cast (int) has higher precedence than the + operator, so it affects only the value of i, not the value of (i + d). In effect, the cast does nothing. There are two ways to fix the problem.

  1. Parenthesize the expression like this:

    i = (int) (i + d);

    Now the cast applies to the double value produced by (i + d).

  2. Change the order of the operands:

    i = (int) d + i;

    Here, the double value of d is converted to an int before the addition occurs, thus ensuring an int result.

I prefer the first technique, because it is more obvious. In fact, to increase your chances of using the cast correctly, I suggest that you always parenthesize the expression being cast, whether it's needed or not.

2.4. More on Casting

The cast can occur in places other than the beginning of an expression. For example, consider this expression:

(total / num - 2) * 2

If total and num are both int variables, the result is an int. The integer division will likely yield undesirable results. You might think adding a cast at the beginning would solve the problem:

(double) (total / num - 2) * 2

But it doesn't. To understand why it doesn't help, fully parenthesize the expression according to operator precedence:

((double) ((total / num) - 2)) * 2

Now, follow the steps the computer would follow when evaluating the expression:

  1. Compute total / num, yielding an int result (oops).

  2. Subtract 2, yielding an int

  3. Convert the result to a double

  4. Multiply by 2, yielding a double

The problem occurred in the very first step. We want that division to yield a double, not an int. To fix the problem, we must move the cast inside the parentheses:

((double) total / num - 2) * 2

Now, fully parenthesized, the expression looks like this:

((((double) total) / num) - 2) * 2

And the computer evaluates it like this:

  1. Take the value of total and convert to a double.

  2. Divide the result by num, yielding a double.

  3. Subtract 2, yielding a double

  4. Multiply by 2, yielding a double

2.5. String Conversions

There's an important limitation on casts: you cannot use a cast to convert between a string type and a numeric type. For example, the following won't work:

int i = 5;
string s = "5";

s = (string) i; // no good
i = (int) s;    // no good

To convert between string and numeric values, you must use methods in the Convert class. Here's a fragment that shows how to do it:

int i = 5;
string s = "5";

s = Convert.ToString(i); // convert int to string
i = Convert.ToInt32(s);  // convert string to int

When you have a numeric value that you need to convert to a string, use the Convert.ToString( ) method. It accepts a numeric expression and returns the string equivalent.

Converting from Strings to numeric types requires the use of the ToDouble and ToInt32 methods in the Convert class. Note that conversion will fail with a runtime error if the string contains any spaces or other non-numeric characters. I will discuss how to handle this problem gracefully in a later chapter.

2.6. Increment, Decrement, and Modulus Operations

You'll find that adding 1 to a variable is an extremely common operation in programming. Subtracting 1 from a variable is also pretty common. You might perform the operation of adding 1 to a variable with assignment statements such as:

x = x + 1;

The effect of the assignment statement x = x + 1 is to take the old value of the variable x, compute the result of adding 1 to that value, and store the answer as the new value of x. The same operation can be accomplished by writing x++ (or, if you prefer, ++x). This actually changes the value of x, so that it has the same effect as writing "x = x + 1". The statement above could be written

x++;

Similarly, you could write x-- (or --x) to subtract 1 from x. That is, x-- performs the same computation as x = x - 1. Adding 1 to a variable is called incrementing that variable, and subtracting 1 is called decrementing. The operators ++ and -- are called the increment operator and the decrement operator, respectively. These operators can be used on variables belonging to any of the numerical types and also on variables of type char.

Sometimes you want to increment or decrement a variable by more than 1. The += and -= operations come in handy for this purpose. Here's an example of what you can do:

counter += 2;

This is basically an abbreviation for

counter = counter + 2;

You can use any expression on the right-hand side of the += operator. For example,

counter += (num * 5) + 2;

would be equivalent to writing

counter = counter + ((num * 5) + 2);

All of the binary arithmetic operators, including +, -, *, /, and the modulo operator (introduced next) can be used in this way. For example, to multiply a number by 3, you could write:

goals *= 3;

In addition to the standard +, -, *, and / operators, C# also has an operator for computing the remainder when one integer is divided by another. This operator is the modulo operator, indicated by %. If A and B are integers, then A % B represents the remainder when A is divided by B. For example, 7 % 2 is 1, while 34577 % 100 is 77, and 50 % 8 is 2. A common use of % is to test whether a given integer is even or odd. N is even if N % 2 is zero, and it is odd if N % 2 is 1. More generally, you can check whether an integer N is evenly divisible by an integer M by checking whether N % M is zero.

2.7. Precedence Rules

If you use several operators in one expression, and if you don't use parentheses to explicitly indicate the order of evaluation, then you have to worry about the precedence rules that determine the order of evaluation. (Advice: don't confuse yourself or the reader of your program; use parentheses liberally.) Here is a listing of the operators discussed in this section (along with some we haven't discussed yet), listed in order from highest precedence (evaluated first) to lowest precedence (evaluated last):

Table 4.4. Operator Precedence in C#

Unary operators:++, --, !, unary - and +, (cast)
Multiplication and division:*, /, %
Addition and subtraction:+, -
Relational operators:<, >, <=, >=
Equality and inequality:==, !=
Boolean and:&&
Boolean or:||
Assignment operators:=, +=, -=, *=, /=, %=

Operators on the same line (like * and /) have the same precedence. When they occur together, unary operators and assignment operators are evaluated right-to-left, and the remaining operators are evaluated left-to-right. For example, A*B/C means (A*B)/C, while A=B=C means A=(B=C). (Tip: You can write things like X = Y = 0 to assign several values the same value with one statement.)