Операторы, позволяющие управлять ходом выполнения программы, в приложениях С# разбиты на три категории: операторы выбора, итерационные операторы и операторы перехода. Во всех выполняется проверка вычисленного булевского значения, и на основе этой проверки изменяется выполнение приложения. В этой главе вы узнаете, как с помощью операторов каждого из этих типов управлять ходом программы.
Позволяют определить, когда и какой код выполнять. В С# два оператора выбора: switch, управляющий ветвлением программы на основе некоторого значения, и //, который выполняет код в зависимости от логического условия. Чаще используется //.
Выполняет один или несколько операторов, если вычисленное им выражение имеет результат true. Вот синтаксис оператора // (квадратные скобки указывают, что конструкция else является необязательной, о чем мы скоро поговорим):
if (выражение)
оператор! [else
оператор2\
Указанное здесь выражение должно давать булевский результат. Если он
равен true, управление передается на оператор]. Если результат
равен false и присутствует конструкция else, управление передается
на oneратор2. Нужно заметить, что оператор! и оператор2
могут состоять из одного оператора, заканчивающегося точкой с запятой (называемого
простым оператором) или из нескольких операторов, заключенных в фигурные скобки
(составной оператор). Пример составного оператора, который вычисляется, если
значение выражение! равно true'.
if (выражение!)
{
оператор! оператор2 }
В приведенном далее примере приложение запрашивает у пользователя ввод числа между 1 и 10. Затем генерируется случайное значение, и пользователю сообщается, совпадет ли его число со случайным. Этот простой пример иллюстрирует применение оператора J/B С#:
using System;
class IfTestlApp <
const int MAX = 10;
public static void MainQ {
Console.Write("Угадайте число от 1 до {0}...", MAX);
string inputString = Console.ReadLineO;
int userGuess = inputString.Tolnt32();
Random rnd = new RandomQ;
double correctNumber = rnd.NextDoubleQ * MAX;
correctNumber = Math.Round(correctNumber);
Console.Write("Правильное число {0}, а вы задали {1}...", correctNumber, userGuess);
if (userGuess == correctNumber) // Число угадано!
{
Console. Кг^еи.пе("Поздравляем!");
}
else // Неверный ответ!
{
Console.WriteLine("Может, в другой раз повезет!"); } } }
Несколько конструкций else
Конструкция else оператора //позволяет определить действия, которые нужно выполнить, если результатом вычисления выражения в if будет false. В предыдущем примере приложение производит простое сравнение введенного пользователем числа со случайным. В этом случае существуют всего две возможности: пользователь мог ввести правильное или неправильное число. Но г/и else можно также применять в случаях, когда нужно проверить более двух условий. В приведенном ниже примере я спрашиваю пользователя, каким языком он сейчас пользуется (кроме С#). При этом я включил возможность выбора трех языков, так что if должен иметь дело с четырьмя возможными ответами: тремя конкретными языками и ситуацией, когда пользователь выбрал неизвестный язык. Вот один из способов запрограммировать это с помощью if/else: using System;
class IfTest2App <
const string CPlusPlus = "C++";
const string VisualBasic = "Visual Basic";
const string Java = "Java";
public static void Main() {
Console.Write("Ha каком языке вы сейчас программируете " + "(кроме С#)?");
string inputString = Console.ReadLineO;
if (0 == String.Compare(inputString, CPlusPlus, true)) {
Console.WriteLine("\nEwin выберите С#, у вас не будет " +
"проблем !"); }
else if (0 == String.Compare(inputString, VisualBasic, true)) {
Console.WriteLine("\nB C# вы обнаружите много " +
"прекрасных возможностей VB !");
}
else if (0 == String.Compare(inputString, Java, true))
{
Console.WriteLine("\n8bi6paB C#, вы облегчите себе " + "жизнь <G> !!");
} else
{
Console.WriteLine("\nM3BHHMTe, это вычислить не можем.");
} } }
Для'сравнения возвращенного методом String.Compare значения с О применяется оператор ==. Это делается потому что String.Compare возвращает —1, если первая строка меньше второй, 1 — если больше, и О, если они совпадают. Между тем здесь есть интересные детали, связанные с тем, как С# выполняет оператор if.
Как if выполняется в С#
Новички в С# могут "пойматься" на том, что результатом вычисления выражения в операторе //должно быть булевское значение. Этим С# отличается от таких языков как C++, которые позволяют в операторе if сравнивать любые переменные на их совпадение с нулем. Этот пример показывает, какие распространенные ошибки могут допустить разработчики на C++, впервые применяя операторы if в С#:
using System;
interface ITest
{ }
class TestClass : ITest
{
>
class InvalidlfApp {
protected static TestClass GetTestClassO
{
return new TestClassQ;
}
public static void Main() {
int foo = 1;
if (foo) // Ошибка: попытка перевести int в bool.
{ }
TestClass t = GetTestClassO;
if (t) // Ошибка; попытка перевести TestClass в bool.
{
Console.WriteLine("{0}", t);
ITest i = t as ITest;
if (i) // Ошибка; попытка перевести ITest в bool.
{
// Методы ITest } } } }
Попытавшись скомпилировать этот код, вы получите такие сообщения об ошибках компилятора С#:
invalidlf.cs(22,7): error CS0029: Cannot implicitly
convert type 'int' to 'bool' invalidlf.cs(27,7): error CS0029: Cannot implicitly
convert type 'TestClass' to 'bool' invalidlf.cs(31,14): warning CS0183:
The given expression is always of the provided ('ITest') type invalidlf.cs(32,8): error CS0029: Cannot implicitly
convert type 'ITest' to 'bool'
Как видите, компилятор трижды "ругнулся" в ответ на попытки использовать в операторе //выражения с небулевскими значениями. Причина в том, что проектировщики С# хотели помочь вам избавиться от неоднозначного кода и предполагали, что компилятор должен заставлять оператор if выполнять свою "естественную" функцию — управлять ходом программы на основе результата логической проверки. Ниже пример переписан так, чтобы компилятор не выдавал сообщений об ошибках. Каждая строка из предыдущей программы, вызывавшая ошибку, переписана так, чтобы выражение возвращало булевский результат, и компилятор успокоился.
using System;
interface ITest
{
>
class TestClass : ITest
{
}
class ValidlfApp
protected static TestClass GetTestClassO
{
return new TestClassO;
}
public static void Hain()
{
int foo = 1; if (foo > 0) { }
TestClass t = GetTestClassO;
if (t != null)
{
Console.WriteLine("{0>", t);
ITest i = t as ITest; if (i != null)
<
// Методы ITest.
} } > }
В операторе switch вы указываете выражение, возвращающее некоторое
значение и один или несколько фрагментов кода, которые будут выполняться в зависимости
от результата выражения. Он аналогичен применению нескольких операторов if/else,
но если в последних вы можете указать несколько условий (возможно, не связанных
между собой), то в операторе switch содержится лишь один условный оператор,
за которым следуют блоки, которые нужно выполнять. Вот его синтаксис:
switch (выражение-переключатель) {
case выражение-константа:
оператор
оператор_перехода
case выражение-константа_N:
onepamop_N [default]
}
Здесь нужно усвоить два правила. Во-первых, выражение-переключатель должно иметь тип sbyte, byte, short, ushort, int, uint, long, ulong, char или string (или епит на основе одного из этих типов) или же должно быть явно преобразовано в один из этих типов. Во-вторых, в каждом операторе case (кроме последнего блока) должен быть тот или иной операто-р_перехода, включая оператор break. Поскольку работает switch не так, как в некоторых других языках, я подробней остановлюсь на отличиях в разделе "Оператор switch без передачи управления вниз".
По сути оператор switch работает так же, как //. Сначала вычисляется выражение-переключатель, а затем результат сравнивается со всеми выражениями-константами или case-метками, определенными в операторах case. При обнаружении совпадения управление передается первой строке кода в соответствующем операторе case.
Кроме нескольких операторов case, в switch можно указать оператор default. Это аналогично конструкции else в операторе //. Каждый оператор switch может иметь только одну метку default. При ее отсутствии, если значение выражения-переключателя не соответствует ни одной case-метке, управление передается на первую строку после закрывающей скобки оператора switch. Рассмотрим пример, в котором класс Payment использует оператор switch для определения выбранного платежного средства:
enura Tenders : int {
Cash = 1,
Visa,
MasterCard,
AmericanExpress •.
};
class Payment
{
public Payment(Tenders tender)
<
this.Tender = tender; }
protected Tenders tender; public Tenders Tender {
get
{
return this/tender;
}
set
{
this.tender = value;
} }
public void ProcessPaymentO {
switch ((int)(this.tender)) {
case (int)Tenders.Cash:
Console.Кг11е1_1пе("\пНаличные - Принимаются"); break;
case (int)Tenders.Visa:
Console.WriteLine("\nVisa - Принимается"); break;
case (int)Tenders.MasterCard:
Console.WriteLine("\nMastercard - Принимается"); break;
case (int)Tenders.AmericanExpress:
Console.WriteLine("\nAmerican Express - Принимается"); break;
default:
Console.WriteLine("\nll3BHHHTe, недопустимое "+
"платежное средство"); break; } } }
class SwitchApp {
public static void Main() {
Payment payment = new Payment(Tenders.Visa); payment.P rocessPayment(); } >
Поскольку созданному экземпляру класса Payment мы передали значение Tenders. Visa, в результате выполнения этого приложения мы увидим:
Visa - Принимается.
Объединение case-меток
В примере Payment мы использовали несколько case-меток для каждого поля Payment.tenders отдельности. А если объединить case-метки? Скажем, для всех трех типов кредитных карточек, использование которых вы допускаете и которые приведены в перечислителе Tenders, вы хотите вывести диалоговое окно для авторизации карточки. В этом случае нужно расположить case-метки одну за другой:
using System;
enum Tenders : int {
Cash = 1,
Visa,
MasterCard,
AmericanExpress };
class Payment {
public Payment(Tenders tender)
{
this.Tender = tender;
}
protected Tenders tender; public Tenders Tender {
get
{
return this.tender;
}
set
{
this.tender = value;
} }
public void ProcessPayment() {
switch ((int)(this.tender)) {
case (int)Tenders.Cash: Console.WriteLine
("ХпНаличные - любимое всеми платежное "средство."); break;
case (int)Tenders.Visa: case (int)Tenders.MasterCard: case (int)Tenders.AmericanExpress:
Console. 1й/г11е1_1пе("\пОтображение диалогового окна "авторизации "+ " карточки.");
break;
default:
Console.Иг11е1-1пе("\пИзвините, недопустимое платежное
"средство."); break;
} > }
class CombiningCaseLabelsApp {
public static void Main() {
Payment payment = new Payment(Tenders.MasterCard);
payment. ProcessPaymentO; } }
Оператор switch без передачи управления вниз
На фазе проектирования разработчики СП взвешивали все "за" и "против", решая, какую функцию языка реализовать. Передача управления вниз (fall-through) — пример возможности, от которой разработчики отказались. Обычно в C++ оператор case выполняется, когда выражение-константа совпадает с выражением-переключателем. Затем оператор break передает управление за пределы оператора switch. Передача управления вниз означает, что при отсутствии break будет выполняться следующий оператор case, содержащийся в switch.
Передача управления вниз, хотя она и не поддерживается в С#, полезна, когда
у вас две case-метки и вторая метка представляет операцию, которая должна выполняться
в любом случае. Например, я когда-то писал на C++ редактор БД, позволяющий пользователям
применять графический интерфейс при создании таблиц и полей. Все таблицы отображались
в окне с деревом вроде Windows Explorer. Когда пользователь щелкал правой кнопкой
дерево, мне нужно было отображать меню с элементами наподобие "Печатать
все таблицы " и "Создать новую таблицу". Если же пользователь
щелкал правой кнопкой конкретную таблицу, нужно было отображать контекстное
меню для этой таблицы. Но при этом хотелось, чтобы в меню входили и все те элементы,
что отображаются для дерева. Концептуально мой код похож на этот:
// Динамическое создание меню в C++.
switch(itemSelected)
{
case TABLE:
// Добавляем параметры меню для текущей таблицы;
// break опущен преднамеренно.
case TREE_VIEW:
// Добавляем элементы меню для дерева.
break;
> -// Отображаем меню.
После первой case-метки выполнялась вторая, и мне не нужно было дублировать код или дважды обращаться к одному методу. Однако проектировщики языка С# решили, что, хотя такая возможность и удобна, ее достоинства не превышают связанного с ней риска, так как в большинстве случаев оператор break нечаянно упускается, что приводит к ошибкам, которые трудно отловить. В С# в рассмотренной ситуации, вероятно, лучше использовать оператор if.
II Динамическое создание меню, if (itentSelected == TABLE) {
// Добавляем параметры меню для текущей таблицы. }
// Добавляем элементы меню для дерева. // Отображаем меню.
Управляемые итерации, или циклы, в С# выполняют операторы while, do/while, for nforeach. В каждом случае исполняется простой или составной оператор, пока значение булевского выражения остается равным true. Исключение составляет foreach, производящий итерацию списка объектов.
Форма оператора while такова:
while (булевское^выражение) встроенный ^оператор
Наш пример с угадыванием чисел можно переписать с использованием while, чтобы игра продолжалась до тех пор, пока вы не угадаете число или не решите выйти:
using System;
class WhileApp
<
const int MIN = 1;
const int MAX = 10;
const string QUIT_CHAR = "Q";
public static void Main() {
Random rnd = new RandomQ; double correctNumber;
string inputString; int userGuess;
bool correctGuess = false; bool userQuit = false;
while (!correctGuess && !userQuit) {
correctNumber = rnd.NextDoubleQ * MAX;
correctNumber = Math.Round(correctNumber);
Console.Write
("Угадайте число от {0} до {1}...({2} - выход)",
MIN, MAX, QUIT_CHAR); inputString = Console. ReadLineO;
if (0 == string.Compare(inputString, QUIT_CHAR, true))
userQuit = true; else {
userGuess = inputString.Tolnt32();
correctGuess = (userGuess == correctNumber);
Console.WriteLine
("Правильное число {0}\п", correctNumber); } }
if (correctGuess && !userQuit) {
Console. КгШи.пе("Поздравляем!"); >
else {
Console.WriteLine("Может, в следующий раз повезет!"); } } }
При работе с этим приложением вы будете получать подобные результаты:
C:\>WhileApp
Угадайте число от 1 до 10...(Q - выход)3 Правильное число 5
Угадайте число от 1 до 10...(Q - выход)5 Правильное число 5
Поздравляем!
C:\>WhileApp
Угадайте число от 1 до 10...(О - выход)q
Может, в следующий раз повезет!
Приглядевшись к синтаксису оператора while, вы можете обнаружить потенциальную проблему. Булевское_выражение вычисляется до исполнения встроенного ^оператора. Поэтому в предыдущем примере приходится инициализировать переменные correctGuess и userQuit значением false, чтобы гарантировать вход в цикл. Затем эти переменные применяются, чтобы отслеживать, угадал ли пользователь число или решил выйти. А если мы хотим, чтобы встроенный jonepamop всегда выполнялся хоть раз, не устанавливая переменные искусственно? Для этого и служит оператор do/while, форма которого: do
встроенный оператор while (булевское_выражение)
Поскольку булевское_выражение вычисляется после встроенного^оператора, у нас есть гарантия, что встроенный_оператор будет выполнен хотя бы раз. Теперь наша "угадайка" будет выглядеть так: using System;
class DoWhileApp {
const int MIN = 1;
const Int MAX = 10;
const string QUIT_CHAR = "Q";
public static void MainQ {
Random rnd = new RandomQ;
double correctNumber;
string inputStrlng; int userGuess = -1;
bool userHasNotduit = true;
do
{
correctNumber = rnd.NextDoubleO * MAX; correctNumber = Math.Round(correctNumber);
Console.Write
("Угадайте число от {0} до {1}...({2} - выход)",
MIN, MAX, QUIT.CHAR); inputString = Console. ReadLineO;
if (0 == string.Compare(inputString, QUIT_CHAR, true))
userHasNotQuit = false; else {
userGuess = inputString.Tolnt32();
Console.WriteLine
("Правильное число {0}\n", correctNumber); }
} while (userGuess l= correctNumber && userHasNotQuit);
if (userHasNotQuit
&& userGuess == correctNumber) {
Console.WriteLine("Поздравляем!");
}
else // Неверный ответ!
{
Console.WriteLine("Может, в следующий раз повезет!");
} }
}
Функционально это приложение аналогично примеру с while. Отличие лишь в управлении циклом. На практике оператор while применяется чаще do/while. Однако поскольку можно легко управлять входом в цикл, инициализируя булевскую переменную, выбор того или иного оператора — дело вкуса.
Этот самый распространенный итерационный оператор может содержать до трех частей. Первая, которая может встречаться только раз, служит для инициализации в начале цикла. Вторая — проверка условия, в результате которой определяется, выполнять ли цикл снова. И третья — "приращение" — обычно (но не обязательно) используется для инкремента счетчика, управляющего продолжением цикла — именно этот счетчик обычно анализируется во второй части оператора. Форма оператора/or:
for (инициализация; булевское ^выражение; приращение) встроенный_оператор
Любая из трех частей (инициализация, булевское^выражение, приращение) может отсутствовать. Когда булевское_выражение равно false управление передается от начала цикла к строке, следующей за встроенным_-оператором. То есть оператор for работает так же, как и while, но при этом у вас две дополнительные части: инициализация и приращение. Вот пример оператора/or, который выводит отображаемые ASCII-символы: using System;
class ForTestApp {
const int StartChar = 33;
const int EndChar = 125;
static public void MainQ {
for (int i = StartChar; i <= EndChar; i++) {
Console.WriteLine("{0}={1}", i, (char)i); } } }
Последовательность событий в этом цикле for такова.
1. В стеке выделяется место для размерной переменной /, и она инициализируется значением 33. Эта переменная выйдет из области видимости по завершении цикла for.
2. Встроенный оператор выполняется, пока значение / меньше 126. Здесь
я применил составной оператор, но, поскольку в цикле содержится одна строка,
будет тот же результат, если убрать фигурные скобки.
3. После каждого прохода цикла переменная i увеличивается на 1.
Вложенные циклы
Встроенный jonepamop цикла for может содержать такие же циклы — их называют вложенными. В предыдущий пример я добавил вложенный цикл, чтобы выводить по три символа в строке, а не по одному:
using System;
class NestedForApp {
const int StartChar = 33;
const int EndChar = 125;
const int CharactersPerLine = 3;
static public void Main()
{
for (int i = StartChar; i <= EndChar; i+=CharactersPerl_ine)
{
for (int j = 0; ] < CharactersPerLine; j++)
{
Console.Write("{0}={1} ", i+j, (char)(i+j));
}
Console. Writel_ine("");
}
}
>
Переменная / внешнего цикла остается в поле видимости внутреннего цикла. А вот переменная у во внешнем цикле недоступна.
Использование запятой
Запятая может служить не только разделителем в списке аргументов методов,
но и оператором в конструкции for. В инициализации и приращении
оператора for оператор "запятая" может быть применен для
разделения нескольких последовательно обрабатываемых операторов. Я взял за основу
предыдущий пример и заменил в нем вложенный цикл одним циклом for с оператором
"запятая":
using System;
class CommaOpApp {
const int StartChar = 33;
const int EndChar = 125;
const int CharactersPerLine = 3;
static public void Main() {
for (int i = StartChar, J = 1; i <= EndChar; i++, j++) {
Console.Write("{0}={1} ", i, (char)i); if (0 == (J X CharactersPerLine)) {
// Переход на новую строку, если J кратно 3. Console.WriteLine(""); } } } }
Использование запятой в операторе for — мощная возможность, но код получается трудным для чтения и сопровождения. Такой формально корректный код, хоть в нем и определены литеральные константы, понятным не назовешь:
using System;
class CommaOp2App {
const int StartChar = 33;
const int EndChar = 125;
const int CharsPerLine = 3;
const int NewLine = 13;
const int Space = 32;
static public void MainQ
{
for (int i = StartChar, extra = Space; i <= EndChar;
H-+, extra = ((0 == (i - (StartChar-1)) % CharsPerLine) ? NewLine : Space))
{
Console.Write("{0}={1> {2}", 1, (char)i, (char)extra);
> } }
Некоторые языки, такие как Visual Basic, давно имеют специальные операторы для итерации массивов и наборов. В С# тоже есть такая конструкция — оператор foreach:
foreach (тип in выражение) встроенный _оператор
Рассмотрим следующий класс-массив:
class MyArray
<
public ArrayList words;
public MyArrayO {
words = new ArrayListO;
words.Add("foo");
words.Add("bar");
words.Add("baz"); } }
Познакомившись с итерационными операторами, вы понимаете, что пройти по массиву можно по-разному. Но для большинства Java- и С++-программистов наиболее логичным будет такой способ:
using System;
using System.Collections;
class MyArray
{
public ArrayList words;
public MyArrayO {
words = new ArrayListQ;
words.Add("foo");
words.Add("bar");
words.Add("baz"); } }
class ForeacMApp {
public static void Nain()
{
HyArray myArray = new MyArrayO;
for (int 1=0; i < myArray.words.Count; i++) {
Console.WriteLine("{0}", myArray.wordsfi]); } > }
Но такой подход обременен потенциальными проблемами:
Источников потенциальных проблем масса. Используя оператор foreach, этих проблем можно избежать и единообразно производить итерацию наборов и массивов. С оператором foreach предыдущий пример можно переписать так:
using System;
using System.Collections;
class MyArray
{
public ArrayList words;
public MyArrayO {
words = new ArrayListO;
words.Add("foo");
words.Add("bar");
words.Add("baz"); } }
class Foreach2App
{
public static void Main()
{
MyArray myArray = new MyArrayO;
foreach (string word in myArray.words)
{
Console.WriteLine("{0>", word);
} } }
Насколько понятней оператор foreaM Вы гарантированно получите каждый элемент, поскольку вам не потребуется вручную устанавливать цикл и запрашивать счетчик, а оператор, содержащийся в цикле, автоматически поместит элемент в указанную вами переменную — достаточно лишь сослаться на нее в этом операторе.
Ветвления с помощью операторов перехода
Во встроенных операторах любого из рассмотренных нами итерационных операторов вы можете управлять ходом исполнения программы с помощью одного из операторов перехода: break, continue, goto и return.
Прерывает текущий вложенный цикл или условный оператор, в котором он присутствует. После этого управление передается на строку кода, следующую за встроенным оператором этого цикла или условного оператора. Оператор break указывается в том месте, откуда вы хотите передать управление и имеет простейшую форму, без скобок или аргументов:
break
В следующем примере приложение будет выводить все числа от 1 до 100, кратные 6. Но когда значение счетчика достигнет 66, break прервет цикл for.
using System;
class BreakTestApp {
public static void MainQ {
for (Int 1=1; 1 <= 100; 1 ++) <
if (0 == i X 6) {
Console.WriteLine(i); }
if (i == 66) {
break; } } } }
}
Выход из бесконечного цикла
Другое применение оператора break — создание бесконечного цикла, выход из которого осуществляется, только когда встречается оператор break. Следующий пример — еще один способ написания "угадайки" с применением оператора break для выхода из цикла, когда пользователь введет Q. Заметьте: я заменил оператор while на оператор while(true), так что он не закончится, пока не встретится break.
using System;
class InfiniteLoopApp
{
const int MIN = 1;
const int MAX = 10;
const string QUIT_CHAR = "Q";
public static void MainQ
{
Random rnd = new RandomQ; double correctNumber;
string inputString; int userGuess;
bool correctGuess = false; bool userQuit = false;
while(true)
{
correctNumber = rnd.NextDoubleO * MAX; correctNumber = Math.Round(correctNumber);
Console.Write
("Угадайте число от {0} до {1}...({2} - выход)",
MIN, MAX. QUIT_CHAR); inputString = Console. ReadLineO;
if (0 == string.Conpare(inputString, QUIT_CHAR, true))
{
userQuit = true;
break; } else
{
userGuess = inputString.Tolnt32(); correctGuess = (userGuess == correctNumber);
if ((correctGuess = (userGuess == correctNumber)))
{
break; }
else
{
Console.WriteLine
("Правильное число {0}\п", correctNumber); } } }
if (correctGuess && luserQuit) {
Console. Writel_ine( "Поздравляем!"); }
else {
Console.WriteLine("Может, в следующий раз повезет!"); } } }
И последнее: вместо оператора while(true) сгодился бы пустой оператор for в виде for (;;). Работают они одинаково, так что их применение — дело вкуса.
Как и break, оператор continue позволяет изменять выполнение цикла. Но continue не завершает встроенный оператор текущего цикла, а останавливает текущую итерацию и передает управление на начало цикла для следующей итерации. В следующем примере я хочу проверить, не повторяются ли строки в массиве. Один из вариантов решения — просматривать массив с помощью вложенных циклов, сравнивая один элемент с другим. Но мне, конечно же, не хочется сравнивать элемент с самим собой, иначе я вычислю неверное число совпадающих элементов. Следовательно, если один индекс массива (/) совпадает с другим индексом этого массива (/'), это значит, что у меня один и тот же элемент и сравнивать их не нужно. В этом случае я использую оператор continue чтобы прекратить текущую итерацию и передать управление на начало цикла.
using System;
using System.Collections;
class MyArray {
public ArrayList words;
public MyArrayO {
words = new ArrayListO; words.Add("foo"); words.Add("bar"); words.AddC'baz"); words.Add("bar"); words.AddC'ba"); words.Add("foo"); } >
class ContinueApp {
public static void MainQ
{
MyArray myArray = new MyArrayO; ArrayList dupes = new ArrayListO;
Console.WriteLine("Обработка массива..."); for (int i = 0; i < myArray.words.Count; i++)
{
for (int J = 0; j < myArray.words.Count; ]++)
{
if (i == j) continue;
if (myArray.words[i] == myArray.words[j]
&& !dupes.Contains(j)) {
dupes.Add(i);
Console.WriteLine("'{0}' встречается "+ "в строках {1} и {2}". myArray.words[i], i + 1, ] + 1); } } } Console.WriteLine("Было обнаружено {0} совпадений",
((dupes.Count > 0) ? dupes.Count.ToStringO : "no")); } >
Для просмотра массива сгодился бы и цикл foreach. Однако здесь я хотел отслеживать текущий элемент, так что лучше использовать for.
Печально известный оператор goto
За всю историю программирования, пожалуй, ни один оператор не вызывал столько нареканий, как goto. Так что прежде чем рассматривать синтаксис и варианты применения goto, ознакомимся с мнениями людей, которые категорически против применения этого оператора, и с проблемами, которые он помогает решить.
Оператор goto: (очень) краткая история
Оператор goto попал в опалу после публикации в 1968 г. работы Дейкст-ры (Edsger W. Dijkstra) "Go To Statement Considered Harmful" ("Обоснование пагубности оператора Go To"). В то время шли неистовые дебаты вокруг структурного программирования. К сожалению, общим проблемам структурного программирования уделялось меньше внимания, чем мелочам: должны ли быть представлены в современных языках программирования конкретные операторы, такие как go to (сейчас, как правило с ключевым словом goto). Как это часто случается, многие, по совету Дейкстры, ударились в крайность и пришли к убеждению, что любое применение goto — это зло и нужно избегать goto любой ценой.
Проблема с goto — это не ключевое слово как таковое, а применение goto в неподходящих местах. Оператор goto может быть полезным для структурирования алгоритма программы и позволяет писать более выразительный код, чем тот, в котором применяются другие механизмы ветвлений и итераций. Один из таких примеров — "полуторный цикл" (неологизм Дейкстры). Вот псевдокод, иллюстрирующий классическую проблему полуторного цикла:
loop
read in a value
if value == sentinel then exit
process the value end loop
Выход из этого цикла производится только при выполнении оператора exit в середине цикла. Однако такой цикл loop/exit/end loop вызовет беспокойство у сторонников программирования без goto, и они перепишут этот код так:
read in a value
while value != sentinel
process the value read in a value end while
Роберте (Eric S. Roberts) из Стэнфордского университета указал у второго подхода два основных недостатка. Во-первых, необходимо дублировать оператор(ы), требующиеся для чтения значения. При любом дублировании кода возникает очевидная проблема с его сопровождением: изменив один оператор, нужно обязательно изменить второй. Вторая проблема не столь явная и, вероятно, не столь значима. Главное, что требуется при написании надежного кода, который легко понимать и, следовательно, сопровождать, — писать код, осуществляющий чтение естественным способом. Пытаясь объяснить словами, что делает этот код, кто-то может сказать: "Сначала мне нужно прочитать некоторое значение. Если это метка конца блока информации (sentinel), я заканчиваю. Если нет, я обрабатываю это значение и продолжаю со следующим значением". Следовательно, код без оператора exit — противоестествен, так как переворачивает с ног на голову естественный способ представления проблемы. А теперь рассмотрим ситуации, в которых применение оператора goto — лучший способ структурирования алгоритма.
Применение оператора goto
Оператор goto может иметь одну из следующих форм:
goto идентификатор',
goto case выражение-константа',
goto default.
В первом случае идентификатор указывает на оператор метки вида: идентификатор:
Если в текущем методе такой метки нет, при компиляции возникнет ошибка. Еще одно важное правило: goto может применяться для выхода из вложенного цикла. Однако если он находится вне области видимости метки, при компиляции возникнет ошибка. Так что перейти внутрь вложенного цикла невозможно.
Ниже приложение просматривает простой массив, читая каждое значение, пока не встретит признак завершения, после чего производится выход из цикла. Оператор goto на самом деле действует, как break, в том смысле, что передает управление из цикла foreach.
using System;
using System.Collections;
Glass MyArray
{
public ArrayList words;
public const string TerminatingWord = "stop";
public MyArrayO {
words = new ArrayListQ;
for (int 1 = 1; i <= 5; i++) words.Add(i.ToStringO); words.Add(TerminatingWord);
for (int 1 = 6; i <= 10; i++) words.Add(l.ToStringO); } }
class GototApp {
public static void Main()
{
MyArray myArray = new MyArrayO;
Console.WriteLine("Обработка массива ...");
foreach (string word in myArray.words) {
if (word == MyArray.TerminatingWord) goto finished;
Console.WriteLine(word); }
finished:
Console.WriteLine("Обработка массива закончена"); } >
Что касается применения здесь goto, кто-то может сказать, что с не меньшей эффективностью можно применить оператор break и в метке не будет нужды. Мы рассмотрим другую форму goto, к вы увидите, что схожие проблемы могут быть решены только с его помощью.
Рассказывая об операторе switch, я говорил, что в С# не поддерживается
передача управления вниз. Да если б и поддерживалась, нельзя было бы решить
следующую проблему. Скажем, у нас есть класс Payment, принимающий разные
формы платежей или платежных средств: Visa, American Express, MasterCard, наличные
и списание со счета (по сути кредит). Поскольку Visa, American Express и MasterCard
— все являются кредитными картами, мы хотим объединить их под одной case-меткой
и обрабатывать единообразно. При списании со счета нам потребуется вызвать специфический
для этого случая метод, а при покупке за наличные — только распечатать квитанцию.
Кроме того, квитанция должна распечатываться и во всех других случаях. Как мы
можем иметь case-метки для трех разных ситуаций, но при этом, чтобы в первых
двух случаях (кредитные карты и списание со счета) мы переходили на третью case-метку?
Решение проблемы — хороший пример использования goto:
using System;
enum Tenders ; int {
ChargeOff,
Cash,
Visa,
MasterCard,
AmericanExpress };
class Payment <
public Payment(Tenders tender)
<
this.Tender = tender;
}
protected Tenders tender; public Tenders Tender {
get
{
return this.tender;
}
set
{
this.tender = value;
} }
protected void ChargeOffQ {
Console.WriteLineC'CnncaHMe со счета,");
}
protected bool ValidateCreditCardQ
{
Console.WriteLine("Карта принимается.");
return true; }
protected void ChargeCreditCardO {
Console.WriteLine("Списание с кредитной карты");
}
protected void PrintReceiptQ
{
Console.WriteLine("Cnacn6o, всегда вам рады.");
}
public void ProcessPaymentO <
switch ((int)(this.tender))
{
case (int)Tenders.ChargeOff: ChargeOffQ; goto case Tenders.Cash;
case (int)Tenders.Visa:
case (int)Tenders.MasterCard:
case (int)Tenders.AmericanExpress:
if (ValidateCreditCardO) ChargeCreditCardO;
goto case Tenders.Cash;
case (int)Tenders.Cash: PrintReceiptO; break;
default:
Console.WriteLine("\nH3BKHHTe - недопустимое "+
"платежное средство."); break;
}
} }
class GotoCaseApp {
public static void Main() {
Payment payment = new Payment(Tenders.Visa); payment. ProcessPaymentO; } }
Вместо того, чтобы решать проблему противоестественным способом, мы просто указали компилятору, что по завершении обработки кредитной карты или списании со счета нужно перейти к ветке, обрабатывающей наличные. Последнее замечание: если в С# вы выходите за пределы case-метки, использовать оператор break нельзя — компилятор укажет, что код недоступен.
Последняя форма оператора goto позволяет переходить на метку default в операторе switch, что дает возможность написать один блок кода, который будет выполнен в результате нескольких вычислений в switch.
У оператора return две функции. Он определяет значение, возвращаемое исполняемым в данный момент кодом вызывающему оператору (если в текущем коде не определено, что он возвращает void) и приводит к немедленному возврату к вызывающему оператору. Синтаксис return:
return [возвращаемое^выражение]
Встретив оператор return метода, определяющий возвращаемое^выражение, компилятор анализирует, можно ли возвращаемое^выражение неявно преобразовать в форму, совместимую со значением, которое возвращает данный метод. Вызывающему оператору возвращается результат этого преобразования.
При использовании return в обработчиках исключений нужно четко понимать некоторые правила. Если return содержится в блоке try, у которого есть соответствующий блок finally, управление на самом деле передается первой строке блока, finally, и когда он завершится, управление будет передано вызывающему оператору. Если блок try вложен в другой блок try, управление будет по цепочке передаваться наверх, пока не выполнится последний блок finally.
Подведем итоги
Условные операторы С# позволяют управлять ходом программы. Три категории операторов управления ходом программы включают операторы выбора (if и switch), итерационные операторы (while, for и foreach) и операторы перехода (break, continue, goto и return). Основываясь на материале этой главы вы сможете выбрать операторы для создания хорошо структурированных и удобных в сопровождении приложений.