Поиск

Простые операторы присваивания

Значение в левой части оператора присваивания называется lvalue, а в правой части — rvalue. В качестве rvalue может быть любая константа, переменная, число или выражение, результат которого совместим с lvalue. Между тем lvalue должно быть переменной определенного типа. Дело в том, что значение копируется из правой части в левую. Таким образом, для нового значения должно быть выделено физическое адресное пространство. Например, можно написать /' = 4, поскольку для / есть место в памяти — в стеке или в куче — в зависимости от типа переменной /. А вот оператор 4 = 1 выполнить нельзя, так как 4 — это значение, а не переменная, содержимое которой в памяти можно изменить. Замечу кстати, что в С# в качестве lvalue может быть переменная, свойство или индексатор.

Подробнее о свойствах и индексаторах см. главу 7. В этой главе я для простоты использую переменные. Если с присваиванием числовых значений все достаточно понятно, с объектами дело сложнее. Напомню, что когда вы имеете дело с объектами, вы манипулируете не элементами стека, которые легко копировать и перемещать. В случае объектов у вас на самом деле есть лишь ссылки на некоторые сущности, для которых динамически выделена память. Следовательно, когда вы пытаетесь присвоить переменной объект (или любой ссылочный тип) копируются не данные, как это происходит в случае размерных типов, а ссылки.

Скажем, у вас два объекта: testl и test2. Если вы укажете testl = test2, testl не будет копией test2. Они будут совпадать! Объект testl указывает на ту же память, что и test2, и любые изменения объекта testl приведут к изменениям test2. Вот программа, которая это иллюстрирует:

using System;
class Foo {
public int i; }
class RefTestlApp {
public static void MainO {
Foo testl = new Foo(); testl.i = 1;
Foo test2 = new Foo(); test2.i = 2;
Console.WriteLine("До назначения объектов");
Console.WriteLine("test1.i={0>", testl.i);
Console.WriteLine("test2.i={0}", test2.i);
Console.WriteLine("\n");
testl = test2;
Console.Writel_ine("После назначения объектов");
Console.WriteLine("test1.i={0}", testl.i);
Console.WriteLine("test2.i={0}", test2.i);
Console.WriteLine("\n");
testl.i = 42; ;'
Console.WriteLine("Пocлe изменения только члена TEST1");
Console.WriteLine("test1.i={0}", testl.i);
Console.WriteLine("test2.i={0}", test2.i); Console.WriteLine("\n"); } }
Выполнив этот код, вы увидите:
До назначения объекта
test1.i=1
test2.i=2
После назначения объекта
testt.i=2
test2.i=2
После изменения только члена TEST1
test1.i=42
test2.i=42

Посмотрим, что происходит на каждом этапе выполнения этого примера. Foo — это простой к класс с единственным членом, /. В методе Main создаются два экземпляра этого класса: testl и test2 — и их члены i устанавливаются в 1 и 2 соответственно. Затем эти значения выводятся, и, как и ожидалось, testl.i равен 1, a test2.i — 2. И тут начинается самое интересное! В следующей строке объекту testl присваивается test2. Читатели, программирующие на Java, знают, что будет дальше. Однако большинство программистов на C++ будут ожидать, что член / объекта testl теперь равен члену объекта test2 (если исходить из предположения, что при компиляции такого приложения будет выполнена некая разновидность оператора копирования членов объектов). Выводимый результат это вроде подтверждает. Однако на самом деле связь между объектами теперь гораздо глубже. Присвоим значение 42 члену testl.i и снова выведем результат. И?! При изменении объекта testl изменился и testZ Это произошло из-за того, что объекта testl больше нет. После присваивания ему test2 объект testl утерян, так как приложение на него больше не ссылается и в результате он «вычищается» сборщиком мусора (garbage collector, GC). Теперь testl и test2 указывают на одну и ту же память в куче. Следовательно, при изменении одной переменной пользователь увидит изменение и другой.

Обратите внимание на две последние выводимые строки: хотя в коде изменялось только значение testl.i, значение test2.i также изменилось. Еще раз: обе переменные теперь указывают на одно место в памяти — такое поведение и ожидали программисты на Java. Однако это совершенно не соответствует ожиданиям разработчиков на C++, поскольку в этом языке производится именно копирование объектов: каждая переменная имеет свою уникальную копию членов и изменения одного объекта не влияют на другой. Поскольку это ключ к пониманию работы объектов в С#, сделаем небольшое отступление и посмотрим, что будет происходить при передаче объекта методу:

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("До вызова метода");
Console.WriteLine("test.i={0}", test.i); Console.WriteLine("\n");
app.ChangeValue(test);
Console.WriteLine("После вызова метода");
Console.WriteLine("test.i={0}", test.i);
Console.WriteLine("\n"); > }

В большинстве языков, кроме Java, этот код будет копировать созданный объект test в локальный стек метода RefTest2App.ChangeValue. В таком случае объект test, созданный в методе Main, никогда не увидит изменений объекта/, производимых в методе ChangeValue. Однако еще раз повторю, что метод Main передает ссылку на выделенный в куче объект test. Когда метод ChangeValue манипулирует своей локальной переменной //, он так же напрямую манипулирует объектом test метода Main.

Подведем итоги

Главное в любом языке программирования — способ выполнения присваивания, математических, логических операций и операций отношения — всего, что требуется для работы реальных приложений. В коде эти операции представлены операторами. К факторам, влияющим на выполнение операторов, относятся старшинство и ассоциативность (правая и левая) операторов. Мощный набор предопределенных операторов в С# можно расширять реализациями, определенными пользователем, о чем мы поговорим в главе 13.