ПОНЯТНО О Visual Basic NET (том 2)

Передача параметров по ссылке и по значению


Рассмотрим задачу: Известны стороны двух прямоугольников. Нужно вычислить площадь и периметр каждого прямоугольника, а затем напечатать периметр того, чья площадь больше.

Внимательно разберитесь в приведенной ниже программе. На ее примере мы пройдем путь от процедур к функциям.

Поскольку прямоугольника два, мы уже предвидим, что будет выгодно создать процедуру, вычисляющую площадь и периметр прямоугольника. Попробуем сделать это по старинке:

    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).
А вот на месте параметров, вызываемых по ссылке, могут стоять только переменные! Никаких литералов и выражений!
Из механики работы параметров вытекает очень удобный факт: Когда мы пишем процедуру, нам не нужно заботиться о том, какие имена переменных будут использованы при обращении к процедуре, мы просто даем параметру любое пришедшее в голову подходящее имя. И наоборот, когда мы пишем обращение к процедуре, нам не нужно заботиться о том, какие имена имеют параметры в заголовке процедуры. В частности, мы можем использовать в обращении переменные, имеющие такие же имена, что и соответствующие параметры. Эффект затенения не даст им перепутаться.

Содержание раздела