C Sharp

Simple Assignment Operators

The value on the left side of an assignment operator is called the lvalue; the value on the right side is called the rvalue. The rvalue can be any constant, variable, number, or expression that can be resolved to a value compatible with the lvalue. However, the lvalue must be a variable of a defined type. The reason for this is that a value is being copied from the right to the left. Therefore, there must be physical space allocated in memory, which is the ultimate destination of the new value. As an example, you can state i = 4 because i represents a physical location in memory, either on the stack or on the heap, depending on the actual type of the i variable. However, you can't execute the statement 4 = i because 4 is a value, not a variable in memory the contents of which can be changed. As an aside, technically the rule in C# is that the lvalue can be a variable, property, or indexer. For more on properties and indexers,refer back to Chapter 7. To keep things simple, I'll stick to examples using variables found in this chapter.

Although numeric assignment is fairly straightforward, assignment operations involving objects is a much trickier proposition. Remember that when you're dealing with objects, you're not dealing with simple stack allocated elements that are easily copied and moved around. When manipulating objects, you really have only a reference to a heap-allocated entity. Therefore, when you attempt to assign an object (or any reference type) to a variable, you're not copying data as you are with value types. You're simply copying a reference from one place to another.-

Let's say you have two objects: test1 and test2. If you state test1 = test2, test1 is not a copy of test2. It is the same thing! The test1 object points to the same memory as test2. Therefore, any changes on the test1 object are also changes on the test2 object. Here's a program that illustrates this: -

using System;
class Foo
{
    public int i;
}
class RefTest1App
{
    public static void Main()
    {
        Foo test1 = new Foo();
        test1.i = 1;
        Foo test2 = new Foo();
        test2.i = 2;
        Console.WriteLine("BEFORE OBJECT ASSIGNMENT");
        Console.WriteLine("test1.i={0}", test1.i);
        Console.WriteLine("test2.i={0}", test2.i);
        Console.WriteLine("\n");
        test1 = test2;
        Console.WriteLine("AFTER OBJECT ASSIGNMENT");
        Console.WriteLine("test1.i={0}", test1.i);
        Console.WriteLine("test2.i={0}", test2.i);
        Console.WriteLine("\n");
        test1.i = 42;
        Console.WriteLine("AFTER CHANGE TO ONLY TEST1 MEMBER");
        Console.WriteLine("test1.i={0}", test1.i);
        Console.WriteLine("test2.i={0}", test2.i);
        Console.WriteLine("\n");
    }
}

Run this code, and you'll see the following output: -

BEFORE OBJECT ASSIGNMENT
test1.i=1
test2.i=2
AFTER OBJECT ASSIGNMENT
test1.i=2
test2.i=2
AFTER CHANGE TO ONLY TEST1 MEMBER
test1.i=42
test2.i=42

Let's walk through this example to see what happened each step of the way. Foo is a simple class that defines a single member named i. Two instances of this class-test1 and test2-are created in the Main method and, in each case, the new object's i member is set (to a value of 1 and 2, respectively). At this point, we print the values, and they look like you'd expect with test1.i being 1 and test2.i having a value of 2. Here's where the fun begins. The next line assigns the test2 object to test1. The Java programmers in attendance know what's coming next. However, most C++ developers would expect that the test1 object's i member is now equal to the test2 object's members (assuming that because the application compiled there must be some kind of implicit member-wise copy operator being performed). In fact, that's the appearance given by printing the value of both object members. However, the new relationship between these objects now goes much deeper than that. The code assigns 42 to test1.i and once again prints the values of both object's i members. What?! Changing the test1 object changed the test2 object as well! This is because the object formerly known as test1 is no more. With the assignment of test1 to test2, the test1 object is basically lost because it is no longer referenced in the application and is eventually collected by the garbage collector (GC). The test1 and test2 objects now point to the same memory on the heap. Therefore, a change made to either variable will be seen by the user of the other variable.

Notice in the last two lines of the output that even though the code sets the test1.i value only, the test2.i value also has been affected. Once again, this is because both variables now point to the same place in memory, the behavior you'd expect if you're a Java programmer. However, it's in stark contrast to what a C++ developer would expect because in C++ the act of copying objects means just that-each variable has its own unique copy of the members such that modification of one object has no impact on the other. Because this is key to understanding how to work with objects in C#, let's take a quick detour and see what happens in the event that you pass an object to a method: -

using System;
class Foo
{
    public int i;
}
class RefTest2App
{
    public void ChangeValue(Foo f)
    {
        f.i = 42;
    }
    public static void Main()
    {
        RefTest2App app = new RefTest2App();
        Foo test = new Foo();
        test.i = 6;
        Console.WriteLine("BEFORE METHOD CALL");
        Console.WriteLine("test.i={0}", test.i);
        Console.WriteLine("\n");
        app.ChangeValue(test);
        Console.WriteLine("AFTER METHOD CALL");
        Console.WriteLine("test.i={0}", test.i);
        Console.WriteLine("\n");
    }
}

In most languages-Java excluded-this code would result in a copy of the test object being created on the local stack of the RefTest2App.ChangeValue method. If that were the case, the test object created in the Main method would never see any changes made to the f object within the ChangeValue method. However, once again, what's happening here is that the Main method has passed a reference to the heap-allocated test object. When the ChangeValue method manipulates its local f.i variable, it's also directly manipulating the Main method's test object.

Summary

A key part of any programming language is the way that it handles assignment, mathematical, relational, and logical operations to perform the basic work required by any real-world application. These operations are controlled in code through operators. Factors that determine the effects that operators have in code include precedence, and left and right associativity. In addition to providing a powerful set of predefined operators, C# extends these operators through user-defined implementations, which I'll discuss in Chapter 13.