int myNumber; | ||
showDouble(ref myNumber); | ||
Console.WriteLine("myNumber = {0}", myNumber); |
4.3.4.Выходные параметры
В дополнение к возможности передавать параметры по ссылке мы можем указать, что данный параметр является выходным: в описание такого параметра включается ключевое слово out, используемое так же, как и ключевое слово ref (в качестве модификатора параметра в описании функции и при вызове функции). Фактически этот способ предоставляет почти такие же возможности, что и передача параметров по ссылке, в том смысле, что значение параметра после выполнения функции попадает в переменную, использовавшуюся при вызове этой функции. Имеются, однако, и существенные отличия. Хотя использовать переменную, которой не присвоено начальное значение, в качестве параметра типа ref недопустимо, она может применяться в качестве параметра типа out. Более того, параметр типа out будет рассматриваться как не имеющий начального значения самой функцией, в которой он используется. Это означает, что хотя передача переменной, которой присвоено некоторое значение, в качестве параметра типа out является допустимой, однако в процессе выполнения функции хранящееся в этой переменной значение будет утрачено.
В качестве примера рассмотрим расширение функции mахVа1ue(), которая возвращает элемент массива с максимальным значением. Модифицируем эту функцию так, чтобы получать индекс элемента массива, содержащего наибольшее значение; в случаях, когда максимальное значение содержится в нескольких элементах, мы будем получать индекс первого максимального элемента. Для этого добавим выходной параметр:
static int MaxValue(int[] intArray, out int maxIndex)
{
int maxVal = intArray[0]; maxIndex = 0;
for (int i = 1; i < intArray.Length; i++)
{
if (mtArray[i] >maxVal) { maxVal = intArray[i]; maxIndex = i;
}
}
return maxVal; }
Эта функция может быть использована таким образом:
int[] myArray = {1, 8, 3, 6, 2, 5, 9, 3, 0, 2}; | ||||
int maxIndex; | ||||
Console.WriteLine("The maximum value in myArray is {0}", | ||||
maxValue(myArray, out maxIndex)); | ||||
Console.WriteLine("The first occurrence of this value is at element {0}", maxIndex + 1) ; |
Результатом ее выполнения будет следующее:
The maximum value in myArray is 9.
(Максимальное значение, содержащееся в массиве myArray,– 9).
The first occurrence of this value is at element 7.
(Первое вхождение с таким значением найдено в элементе 7).
Важным моментом, на который следует обратить внимание, является необходимость использовать ключевое слово
out при вызове функции (так же, как и ключевое слово ref).
Следует обратить внимание, что в тексте программы к значению переменной maxIndex при выводе ее на экран прибавляется единица. Это сделано для того, чтобы, придать индексу более удобную для восприятия форму, как если бы первый элемент массива считался элементом 1, а не элементом 0.
4.4.Область действия переменных
Возможно, читая предыдущую лекцию, вы задавались вопросом, зачем вообще нужно обмениваться данными с функциями. Ответ заключается в том, что в C# доступ к переменным может осуществляться только из определенных участков кода. Принято говорить, что переменная имеет определенную область действия, в рамках которой она является доступной.
Область действия переменной представляет собой очень важное понятие.
Перед тем как пойти дальше, следует обратить внимание на то, что важность заявлений, сделанных в предшествующем разделе, далеко выходит за рамки определения области действия переменных в разных функциях. Утверждалось, что область действия переменных распространяется на блок кода, в котором они описаны, и на все блоки, непосредственно в него вложенные. Это также применимо и к другим типам блоков, например, к программным конструкциям, использующим ветвление и циклы. Рассмотрим следующую программу:
int i;
for (i = 0; i < 10; i++)
{
string text = "Line" + Convert.ToString(i);
Console.WriteLine("{0}", text); };
Console.WriteLine("Last text output in loop: {0}", text);
В данной программе строковая переменная text является локальной для цикла for. Такой код не пройдет компиляцию, поскольку в обращении к Console.WriteLine(), которое происходит вне этого цикла, делается попытка использовать переменную text, область действия которой не распространяется за пределы цикла. Изменим код следующим образом:
int i; string text;
for (i = 0; i < 10; i++)
{
text = "Line " + Convert.ToString(i); Console.WriteLine("{0}", text); };
Console.WriteLine("Last text output in loop: {0}", text);
Этот код также недопустим. Причина кроется в том, что переменные должны и описываться, и инициализироваться до того, как они будут использоваться, а переменная text инициализируется только в цикле for. Значение, присвоенное переменной text, при выходе из цикла будет утрачено. Однако мы можем модифицировать код еще раз:
int i; string text = " ";
for (i = 0; i < 10; i++)
{
text = "Line" + Convert.ToString(i); Console.WriteLine("{0}", text); }
Console.WriteLine("Last text output in loop: {0}", text);
На этот раз переменная text инициализирована вне цикла, и мы имеем доступ к ее значению. Результат выполнения этой простой программы показан на рисунке слева.
В данном случае значение, присвоенное переменной text внутри цикла, оказывается доступным и вне его.
Как вы, вероятно, заметили, эта тема требует определенных усилий для понимания. В первый момент представляется не вполне очевидным, почему – в свете предыдущего примера – переменная text не сохраняет в качестве своего значения пустую строку, присвоенную ей перед началом цикла, для кода, расположенного после его окончания.
Причина кроется в способе выделения памяти для переменной text, да и для всех остальных переменных тоже. Простое объявление переменной некоторого простого типа не влечет за собой выполнения каких-либо существенных действий. Только тогда, когда переменным присваиваются значения, для этих значений выделяется память, в которой они будут храниться. Когда такое выделение памяти происходит внутри цикла, это значение определяется как локальное и область его действия не выходит за пределы цикла. И хотя переменная не является локализованной внутри данного цикла, к ее значению это не относится. Напротив, присваивание переменной значения вне цикла дает гарантию того, что это значение локальное для всего основного кода и что область действия переменной распространяется в том числе и на цикл. Другими словами, мы будем оставаться внутри области действия переменной до тех пор, пока не покинем блок основного кода, поэтому возможность доступа к переменной имеется и за пределами цикла.
Компилятор C# самостоятельно обнаруживает проблемы, связанные с областью действия переменных, и генерирует сообщения об ошибках. В качестве заключительного замечания необходимо упомянуть о "наилучшей практике". Вообще говоря, лучше всего объявлять и инициализировать переменные перед теми блоками кода, в которых они используются. Исключением из этого правила могут быть переменные циклов, объявление которых является составной частью самого цикла. Например:
for (int i = 0; i < 10; i++)
{
}
В данном случае, переменная i является локализованной в блоке кода, представляющем собой цикл, но это и хорошо, поскольку редко когда требуется доступ к значению счетчика цикла из внешнего кода.
Параметры и возвращаемые значения по сравнению с глобальными данными.
Этот раздел посвящен более детальному рассмотрению обмена данными с функциями через механизм глобальных переменных и через механизм параметров и возвращаемых значений. Напомним, что речь идет о различиях между двумя программами. Первая из них имеет вид:
class Classl { static void showDouble(ref int val) {
val *= 2;
Console.WriteLine("val doubled = {0}", val); } static void Main(string[] args) (
int val = 5; | |||
Console.WriteLine("val = {0}", val); | |||
showDouble(ref val) ; | |||
Console.WriteLine("val = {0}", val); } } |
Этот код несколько отличается от того кода, где в функции Main() использовалась переменная с именем myNumber. Это иллюстрация того факта, что локальные переменные могут обладать идентичными именами и не мешать друг другу. Это также означает, что две программы, которые приводятся здесь, оказываются очень похожими друг на друга, позволяя нам сосредоточить внимание на принципиальных отличиях и не беспокоиться об именах переменных. Вторая программа имеет следующий вид: class Classl { static int val;
static void showDouble() {
val *= 2;
Console.WriteLine("val doubled = {0}", val); } staticvoidMain(string[] args) { val = 5;
Console.WriteLine("val = {0}", val); showDouble ();
Console.WriteLine("val = {0}", val); } }
Результаты выполнения обеих программ совершенно идентичны.
Следует заметить, что не существует жестких правил, определяющих предпочтительность использования одного способа вместо другого: оба эти способа являются вполне допустимыми. Тем не менее существуют некоторые соображения, которые желательно учитывать при выборе одного из них.
Для начала обратите внимание на то, что версия функции showDouble(), которая использует глобальное значение, сможет использовать только глобальное значение переменной val. Применение такой версии функции принуждает нас использовать именно эту глобальную переменную, что несколько ограничивает гибкость в применении данной функции и требует постоянно копировать значение глобальной переменной в другие переменные в том случае, если необходимо сохранить результат. Кроме того, глобальные данные могут быть изменены где-нибудь в другом месте приложения, что приведет к получению непредсказуемых результатов (мы можем вспомнить о том, что значения изменены, слишком поздно).
Однако такая потеря гибкости зачастую является преимуществом. Существуют ситуации, когда применение некоторой функции планируется исключительно для одной конкретной цели и использование глобально описанных данных уменьшает вероятность того, что мы неправильно обратимся к функции, например, передав неверные параметры.