Передача параметров по ссылке и по значению
Рассмотрим задачу: Известны стороны двух прямоугольников. Нужно вычислить площадь и периметр каждого прямоугольника, а затем напечатать периметр того, чья площадь больше.
Внимательно разберитесь в приведенной ниже программе. На ее примере мы пройдем путь от процедур к функциям.
Поскольку прямоугольника два, мы уже предвидим, что будет выгодно создать процедуру, вычисляющую площадь и периметр прямоугольника. Попробуем сделать это по старинке:
Dim A1, B1, S1, P1 As Integer 'Две стороны, площадь и периметр 1 прямоугольника
Dim A2, B2, S2, P2 As Integer 'Две стороны, площадь и периметр 2 прямоугольника
Dim Площадь As Integer 'площадь, вычисленная процедурой
Dim Периметр As Integer 'периметр, вычисленный процедурой
Private Sub Button1_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles Button1.Click
'Работаем с 1 прямоугольником:
A1 = 10 : B1 = 50
Прямоугольник(A1, B1)
P1 = Периметр : S1 = Площадь
'Работаем со 2 прямоугольником:
A2 = 20 : B2 = 30
Прямоугольник(A2, B2)
P2 = Периметр : S2 = Площадь
'Анализируем:
If S1 > S2 Then Debug.WriteLine(P1) Else Debug.WriteLine(P2)
End Sub
Sub Прямоугольник(ByVal Сторона1 As Integer, ByVal Сторона2 As Integer)
Площадь = Сторона1 * Сторона2
Периметр = 2 * Сторона1 + 2 * Сторона2
End Sub
В учебных целях я здесь написал
A1 = 10 : B1 = 50
Прямоугольник(A1, B1)
хотя короче было бы написать
Прямоугольник(10, 50)
В нашей программе нас явно раздражает необходимость писать строки:
Dim Площадь As Integer 'площадь, вычисленная процедурой
Dim Периметр As Integer 'периметр, вычисленный процедурой
P1 = Периметр : S1 = Площадь
P2 = Периметр : S2 = Площадь
К тому же пришлось объявлять слишком много модульных переменных, что снижает безопасность проекта. Нельзя ли как-то укоротить и улучшить программу? Можно. Для этого необходимо добавить в заголовок процедуры один параметр для площади и другой – для периметра. Вот более короткий вариант программы:
Private Sub Button1_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles Button1.Click
Dim A1, B1, S1, P1 As Integer 'Две стороны, площадь и периметр 1 прямоугольника
Dim A2, B2, S2, P2 As Integer 'Две стороны, площадь и периметр 2 прямоугольника
A1 = 10 : B1 = 50
Прямоугольник(A1, B1, S1, P1)
A2 = 20 : B2 = 30
Прямоугольник(A2, B2, S2, P2)
If S1 > S2 Then Debug.WriteLine(P1) Else Debug.WriteLine(P2)
End Sub
Sub Прямоугольник(ByVal Сторона1 As Integer, ByVal Сторона2 As Integer, _
ByRefПлощадь As Integer, ByRefПериметр As Integer)
Площадь = Сторона1 * Сторона2
Периметр = 2 * Сторона1 + 2 * Сторона2
End Sub
Пояснения. До этого момента мы без пояснений писали в заголовке процедуры перед каждым параметром слово ByVal(по значению). Оно рекомендуется для исходных данных, по которым процедура вычисляет результаты. Ну а вот если процедура эти результаты должна «сообщить внешнему миру», чтобы они использовались где-то вне процедуры (что мы и видим в нашем случае), тогда эти результаты нужно включить в число параметров процедуры и предварить словом ByRef(по ссылке).
Вам нужно очень точно понять, что происходит в компьютере при вызове и работе такой процедуры. Разберем момент выполнения вызывающего оператора
Прямоугольник(A1, B1, S1, P1)
Лучше всего это делать в пошаговом режиме. К этому моменту в ячейках A1 и B1 уже находятся числа 10 и 50. А в ячейках S1 и P1 пока пусто, точнее – нули.
Но вот желтая полоса прыгает на заголовок процедуры Прямоугольник, управление передалось этой процедуре. В этот славный момент создаются все ее локальные переменные: параметры Сторона1, Сторона2, Площадь, Периметр. И VB требует, чтобы они тут же получили свои значения от вызывающего оператора Прямоугольник (A1, B1, S1, P1). Причем в том порядке, в каком они приведены в скобках. Поэтому Сторона1 получает свое значение от переменной A1, а значит становится равной 10. Сторона2 получает 50 от B1. А Площадь и Периметр получают по нулю от S1 и P1. Кстати, вы можете сказать, что этим двум и не надо было получать никаких нулей, все равно процедура их правильно бы вычислила. Верно, но такая ненужная здесь механика может пригодиться в других программах.
Смысл. А сейчас вам нужно понять разницу между тем, как процедура работает с параметрами по значению и по ссылке. Представьте себе, что весь проект – это здание школы, процедуры – это различные классы в школе. В классах развешено по нескольку классных досок. Каждая доска – это ячейка под локальную переменную этой процедуры или под ее параметр. Работу каждой процедуры осуществляет учитель с мелом и тряпкой, который сидит в классе.
Теперь будьте внимательны. Что происходит при вызове и работе процедуры Прямоугольник? В классе, отведенном под процедуру Button1_Click, в этот момент на досках A1, B1, S1, P1 написаны числа: соответственно 10, 50, 0, 0. Для определенности назовем комнатой класс, отведенный под процедуру Прямоугольник. В момент вызова мы видим, что в комнатке на доски, помеченные значком ByVal, автоматически переписываются числа с соответствующих досок класса. То есть, на доску Сторона1 переписывается число 10, а на доску Сторона2 – 50. Но вот странная вещь: вместо досок, помеченных значком ByRef, в комнатке дыры. Вместо доски Площадь – дыра с надписью Площадь, которая ведет в класс прямо к доске S1, и учитель может просунуть в дыру свою длинную руку и писать прямо на доске S1 в классе! Абсолютно то же самое с досками Периметр и P1. Поэтому в рассматриваемый момент с досок S1 и P1 в комнатушку ничего не переписывается. В этом нет нужды, так как учитель все равно имеет полный доступ к этим двум доскам. Через дыры он видит, что там – нули.
Итак, учитель может распоряжаться 4 досками, 2 – в своей комнатке и 2 – в классе. Посмотрим, что происходит, когда учитель выполняет оператор
Площадь = Сторона1 * Сторона2
Он смотрит на доски своей комнатки, видит там 10 и 50, перемножает их, затем просовывает руку в дыру с надписью Площадь и записывает результат 500 на доску S1 в классе. Говорят: переменная Площадь является ссылкойна переменную S1 или что переменная Площадь ссылается на переменную S1.
Мы привыкли, что в ячейках памяти хранятся числа или строки. А что же хранится в «дырявой» ячейке Площадь? Говорят, что в ней хранится ссылка на ячейку S1 или, еще говорят, – хранится адресячейки S1.
Если учитель во время работы что-нибудь напишет на доске со значком ByVal, то бесполезно ждать, что в классе об этом когда-нибудь узнают. Обратного переписывания с досок ByVal в комнатушке на соответствующие доски в классе никогда не происходит! Это значит, что если мы нечаянно пометим параметр Периметр не словом ByRef, а словом ByVal, то все усилия процедуры по вычислению периметра окажутся бесполезными: никто никогда снаружи не узнает вычисленного значения периметра. Проверьте и увидите, что программа в этом случае печатает периметр равный нулю.
Теперь скажем то же самое, но только другими словами: при помощи компьютерной терминологии. Говорят, что ByVal обеспечивает передачу параметров по значению, а ByRef – передачу параметров по ссылке. При передаче параметров по значению процедура работает не с теми ячейками, из которых передается информация (A1, B1), а с собственными ячейками (Сторона1, Сторона2), куда информация из A1, B1 копируется при обращении к процедуре. Процедура может как угодно менять информацию в своих ячейках Сторона1 и Сторона2, на чужих ячейках A1 и B1 это никак не скажется. Поэтому обычно никто из программистов и не старается этого делать.
При передаче параметров по ссылке процедура работает не с собственными ячейками (Площадь, Периметр), а непосредственно с теми ячейками, на которые они ссылаются (S1, P1). Это опасно, ведь процедура получает доступ к локальным переменным другой процедуры, а это не приветствуется, ведь эти переменные становятся беззащитными против ошибок в вызываемой процедуре. Поэтому программисты стараются внимательно следить, чтобы ненароком не записать в чужие переменные что-нибудь не то. И опасное слово ByRef употребляют только тогда, когда хотят передать вызывающей процедуре важные сведения, а в остальных случаях используют безопасное ByVal.
Вот пример, как неоправданное использование ByRef довело нас до беды:
Private Sub Button2_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles Button2.Click
Dim A1, B1, S1, P1 As Integer 'Две стороны, площадь и периметр прямоугольника
A1 = 10 : B1 = 50
Прямоугольник_опасный(A1, B1, S1, P1)
Debug.WriteLine(A1 & " " & B1 & " " & S1 & " " & P1)
End Sub
Sub Прямоугольник_опасный(ByRef Сторона1 As Integer, ByVal Сторона2 As Integer, _
ByRef Площадь As Integer, ByRef Периметр As Integer)
Площадь = Сторона1 * Сторона2
Периметр = 2 * Сторона1 + 2 * Сторона2
Сторона1 = 999
End Sub
Пояснения. Автор процедуры Прямоугольник_опасный для каких-то своих нужд написал оператор Сторона1 = 999. И все бы ничего, это его право, но он шапкозакидательски написал в заголовке ByRef Сторона1. В результате ни в чем не виноватая процедура Button2_Click вместо того, чтобы напечатать
10 50 500 120
напечатала
999 50 500 120
Достаточно в приведенной программе вместо ByRef Сторона1 снова написать ByVal Сторона1, и все опять будет нормально. Таким образом, передача параметров по значению – еще один способ повысить надежность программирования.
Важное исключение. Слово ByVal теряет свою способность к защите по отношению к массивам и объектам. Ни массивов, ни объектов мы еще не проходили. О причинах невозможности защиты поговорим в 27.2.4.
Далее. В вызывающем операторе на месте параметров, вызываемых по значению, могут стоять не только переменные, но и литералы, и выражения:
Прямоугольник(10, A1+40, S1, P1)
Разница только в том, что при вызове процедуры выражения сначала вычисляются, после чего вычисленные значения, как и положено, посылаются в ячейки параметров процедуры (Сторона1 и Сторона2).
А вот на месте параметров, вызываемых по ссылке, могут стоять только переменные! Никаких литералов и выражений!
Из механики работы параметров вытекает очень удобный факт: Когда мы пишем процедуру, нам не нужно заботиться о том, какие имена переменных будут использованы при обращении к процедуре, мы просто даем параметру любое пришедшее в голову подходящее имя. И наоборот, когда мы пишем обращение к процедуре, нам не нужно заботиться о том, какие имена имеют параметры в заголовке процедуры. В частности, мы можем использовать в обращении переменные, имеющие такие же имена, что и соответствующие параметры. Эффект затенения не даст им перепутаться.