Ограничения типа значения
Ограничения в использовании типа значения связаны с фактом копирования дан¬ных. Это влияет на то, что происходит при передаче ссылочных типов и типов значе¬ния методам в качестве параметров.
Эффект копирования данных
Когда один специальный тип1 значения присваивается другому специальному типу значения, их содержимое копируется. Чтобы увидеть это в действии, рассмотрим объ¬явления на рис. 4.4.
При объявлении пользовательских типов все их переменные-члены и методы объяв¬ляют внутри фигурных скобок ({ }). Можете считать объявление надписью на крышке коробки, а фигурные скобки — стенками коробки; все, что внутри фигурных скобок, на¬ходится в коробке, это и есть содержимое типа. Идентификатор перед первой фигурной скобкой — это имя типа.
Типы, объявленные на рис. 4.4, не имеют идентификатора области видимости. Считайте идентификатор области видимости указанием того, кто имеет право досту¬па к вашим карманам и бумажнику. В данном примере область видимости (scope) по¬добна высказыванию, что вашей супруге разрешено просматривать ваш бумажник, а посторонним — нет.
Если бы перед идентификатором типа было ключевое слово public (открытый), то пользовательский тип был бы предоставлен для доступа извне, а это похоже на то, что вы оставили бумажник на скамейке в парке, чтобы он стал достоянием широкой публи-ки. В случае бумажника это плохая идея — держать его открытым, но иногда область видимости public нужна, если вы способны контролировать доступ. Каждый раз, когда зы оплачиваете что-нибудь кредитной карточкой, вы вручаете ее официанту или клер-ку. В этом случае вы предоставляете части своего бумажника публичному доступу — под своим контролем, естественно.
Теперь рассмотрим код на рис. 4.5. Он создает экземпляр структуры MyValueType присваивает его другой переменной того же типа.
Пример на рис. 4.5 иллюстрирует то, что происходит с двумя переменными, ког¬да создаются их экземпляры, одна присваивается другой, а затем вторая изменяется. Вы хотите понять, как изменяется каждый тип, когда он взаимодействует с другим типом.
Для сравнения, те же самые операции можно выполнить с помощью ссылочного типа, этот код будет выглядеть так.
MyReferenceType val = new MyReferenceType () ; MyReferenceType copiedVal = val;
Console.WriteLine ("val value=" + val.value +
" copiedVal value=" + copiedVal.value); val.value = 10; Console.WriteLine ("val value=" + val.value +
" copiedVal value=" + copiedVal.value);
Итак, если две части кода функционально идентичны, с одним различием, заклю¬чающимся в используемом типе (значения или ссылочном), то дадут ли они одинаковые результаты?
Запуск обеих частей кода приводит к следующему выводу.
var value=0 copiedVar value=0 var value=10 copiedVar value=0 val value=0 copiedVal value=0 val value=10 copiedVal value=10
Внимательно рассматривая результат, можно заметить, что две функционально идентичные части кода создают совершенно различные результаты, даже при том, что единственное различие — это тип используемой переменной.
• Когда вы присваиваете и изменяете тип значения, изменяется только содержимое измененной переменной.
• Когда вы присваиваете и изменяете ссылочный тип, изменится содержимое обеих переменных.
Этот пример демонстрирует, что при определении пользовательских типов необходи¬мо быть внимательным, выбирая тип значения или ссылочный тип.
Как уже было сказано в главе 2, "Типы и значения чисел .NET", содержимое пере¬менной типа значения хранится в стеке. Таким образом, объявление пользовательского типа значения означает, что все его содержимое будет храниться в стеке. Когда вы при¬сваиваете одну переменную типа значения другой переменной типа значения, вы ко¬пируете все содержимое типа значения. Это копирование очевидно в нашем примере, когда мы использовали простые числовые типы (например, double), но когда вы копи¬руете большие структуры с содержимым, возможны весьма неожиданные побочные эффекты.
Типы значения, содержащие ссылочные типы
Типы значения копируют содержимое при присвоении переменных, но вот проблема: это правило не применяется, если тип значения содержит ссылочный тип. Рассмотрим гледующее объявление:
struct MyValueTypeWithReferenceType {
public int value;
public MyReferenceType reference;
Первая строка — это объявление типа значения, содержащего один тип значения (int) и один ссылочный тип (MyReferenceType). Третья строка — объявление переменной-члена ссылочного типа. Объявление подразумевает, что тип значения сохраняется в стеке, но ссылочный тип находится в распределяемой памяти.
Тип значения, который содержит ссылочный тип, контролируется следующим про-верочным кодом:
MyValueTypeWithRefегепсеТуре var = new MyValueTypeWithRefегепсеТуре(); var.reference = new MyReferenceType(); MyValueTypeWithReferenceType copiedVar = var;
Console.WriteLine("var value=" + var.reference.value +
" copiedVar value=" + copiedVar.reference.value); var.reference.value = 10; Console.WriteLine ("var value=" +
var.reference.value + " copiedVar value=" +
copiedVar.reference.value
Важно понять, что размещение в памяти MyValueTypeWithReferenceType не под¬разумевает размещения встроенного специального типа. В проверочном коде разме¬щение в памяти MyValueTypeWithReferenceType осуществляется как и в предыдущих примерах кода, но второе размещение MyReferenceType также необходимо, поскольку MyReferenceType — это ссылочный тип. Если бы MyReferenceType был типом значения, размещение в памяти не было бы необходимо. Но если вы размещаете тип значения, то тем же способом вы размещаете ссылочный тип, а компилятор игнорирует директиву.
Запуск проверочного кода приводит к следующему:
value value=0 reference value=10
Когда вы присваиваете и модифицируете встроенный ссылочный тип, экземпляр ссы-лочного типа изменяется для обеих переменных. В данном случае, когда мы присвоили тип значения, содержимое было скопировано, включая указатель на ссылочный тип.
В табл. 4.1 подведен итог поведению типов, когда размещенная в памяти переменная присваивается другой переменной и исходная переменная-член изменяется. Например, есть код custom2 = customl; customl .member = [new~value], что будет значением custom2.member?
Таблица 4.1. Поведение при размещении переменной, присвоенной другой переменной, а также исходной измененной переменной-члена
Тип
Тип значения
Ссылочный тип
Тип значения, встроенный в тип значения
Тип значения, встроенный в ссылочный тип
Ссылочный тип, встроенный в тип значения
Ссылочный тип, встроенный в ссылочный тип
Поведение
Присвоенная переменная-член не изменяется
Присвоенная переменная-член изменяется
Присвоенная встроенная переменная-член не изменяется
Присвоенная встроенная переменная-член изменяется
Присвоенная встроенная переменная-член изменяется
Присвоенная встроенная переменная-член изменяется
Типы значения и параметры
Другое ограничение типов значений относят к сохранению переменных и способам манипулирования ими при передаче методу в качеству параметра. Предположим, что вы создаете метод, который имеет параметры типа значения и ссылочного типа. Если параметры в методе будут изменены, то какие изменения будут у вызывающей стороны метода? Рассмотрим следующий код:
static void Method(MyValueType value, MyReferenceType reference) { value.value = 10; reference.value = 10; >
Вызывающая сторона может передавать экземпляры типа значения и ссылочно¬го типа, которые обрабатываются в контексте метода. Теперь давайте вызовем метод Method () со следующим кодом:
MyValueType value = new MyValueType () ; MyReferenceType reference = new MyReferenceType (); Method(value, reference);
Console.WriteLine("value value=" + value.value + " reference value=" + reference.value) ;
Вызывающая сторона создает экземпляры типов MyValueType и MyReferenceType, зызывает метод Method (), а затем проверяет значение переменной-члена value типа значения и ссылочного типа.
В результате запуска кода будет получен следующий вывод:
value value=0 reference value=10
Как демонстрирует выполнение этого кода, переменная-член типа значения (MyValueType) не изменялась, в то время как переменная-член ссылочного типа (MyReferenceType) изменилась. Это правильно и демонстрирует, что при вызове ме¬тода вы присваиваете параметры метода переменным внутри вызываемого метода. Вернитесь к табл. 4.1; как можно заметить, при присвоении значения типу значения манипуляции присваивают экземпляр, не изменяя первоначальный экземпляр.
Эта ситуация подразумевала бы, что всякий раз, когда вы используете тип значения, вызов метод и последующее изменение типа значения, вы никогда не увидите измене-ний. Это ограничение означает, главным образом, то, что вы должны использовать ссы-лочные типы. Но среда CLR предоставляет решение для этой проблемы, ключевое слово out, которое связано с методом, как иллюстрирует рис. 4.6. Ключевое слово out означает, что переменной будет присвоено значение при выходе из метода, а не при его вызове.
Преимуществом использования ключевого слова out является то, что можно присво-ить тип значения в методе и обеспечить вызывающей стороне изменения. Недостатком является то, что ключевое слово out игнорирует присвоение параметров метода вызы-вающей стороны. Чтобы обеспечить способность передать информацию методу, а затем получить ее из метода, как у ссылочного типа, вы используете ключевое слово ref, как в следующем примере.
static void Method (ref MyValueType value, MyReferenceType reference) {
value.value = 10; reference.value = 10;
MyValueType value = new MyValueType(); MyReferenceType reference = new MyReferenceType(); Method(ref value, reference);
Console.WriteLine("value value=" + value.value + " reference value=" + reference.value);
При использовании ключевого слова ref вы преобразуете тип значения в ссылоч¬ный тип и, таким образом, получаете возможность вызова метода Method (), требующе¬го размещения в памяти типа значения.
На заметку. Наблюдая за использованием ключевых слов out и ref, можно заметить, что язык С#
считается явным. Вы указываете ключевые слова out и ref как при объявлении метода, так и
при вызове метода. Программируя на языке С#, вы всегда знаете, что делает параметр, метод,
переменная или класс и как делает. Это позволяет другим людям читать ваш код и понимать
то, что вы пытаетесь сделать. •
Теперь, когда вы имеете понятие об алгоритме поиска в глубь и определении струк¬туры данных как пользовательского типа значения, давайте начнем построение алго¬ритма поиска.