Операторы, позволяющие управлять ходом выполнения программы, в приложениях С# разбиты на три категории: операторы выбора, итерационные операторы и операторы перехода. Во всех выполняется проверка вычисленного булевского значения, и на основе этой проверки изменяется выполнение приложения. В этой главе вы узнаете, как с помощью операторов каждого из этих типов управлять ходом программы.
Запятая может служить не только разделителем в списке аргументов методов, но и оператором в конструкции
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);
> } }
Управляемые итерации, или циклы, в С# выполняют операторы
while, do/while, for nforeach.
В каждом случае исполняется простой или составной оператор, пока значение булевского выражения остается равным
true.
Исключение составляет
foreach,
производящий итерацию списка объектов.
Новички в С# могут "пойматься" на том, что результатом вычисления выражения в операторе //должно быть булевское значение. Этим С# отличается от таких языков как 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.
} } > }
Конструкция
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.
В примере
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; } }
Прерывает текущий вложенный цикл или условный оператор, в котором он присутствует. После этого управление передается на строку кода, следующую за встроенным оператором этого цикла или условного оператора. Оператор
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,
оператор
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.
Приглядевшись к синтаксису оператора
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.
Некоторые языки, такие как 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]); } > }
Но такой подход обременен потенциальными проблемами:
если переменная цикла for проинициализирована некорректно, итерация всего списка будет невозможной; если неверно булевское выражение оператора for, итерация всего списка будет невозможной; если неверно приращение цикла, итерация всего списка будет невозможной; у совокупностей и массивов разные методы и свойства для доступа к их счетчикам; семантика выделения конкретного элемента из совокупности и массива различна; во встроенном операторе цикла for при выделении элемента для него нужна переменная соответствующего типа.
Источников потенциальных проблем масса. Используя оператор
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
Вы гарантированно получите каждый элемент, поскольку вам не потребуется вручную устанавливать цикл и запрашивать счетчик, а оператор, содержащийся в цикле, автоматически поместит элемент в указанную вами переменную — достаточно лишь сослаться на нее в этом операторе.
Оператор
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 —
лучший способ структурирования алгоритма.
Выполняет один или несколько операторов, если вычисленное им выражение имеет результат
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("Может, в другой раз повезет!"); } } }
У оператора
return
две функции. Он определяет значение, возвращаемое исполняемым в данный момент кодом вызывающему оператору (если в текущем коде не определено, что он возвращает
void)
и приводит к немедленному возврату к вызывающему оператору. Синтаксис
return:
return
[возвращаемое^выражение]
Встретив оператор
return
метода, определяющий
возвращаемое^выражение,
компилятор анализирует, можно ли
возвращаемое^выражение
неявно преобразовать в форму, совместимую со значением, которое возвращает данный метод. Вызывающему оператору возвращается результат этого преобразования.
При использовании
return
в обработчиках исключений нужно четко понимать некоторые правила. Если
return
содержится в блоке
try,
у которого есть соответствующий
блок finally,
управление на самом деле передается первой строке
блока, finally,
и когда он завершится, управление будет передано вызывающему оператору. Если блок
try
вложен в другой блок
try,
управление будет по цепочке передаваться наверх, пока не выполнится последний блок
finally.
В операторе
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 - Принимается.
На фазе проектирования разработчики
СП
взвешивали все "за" и "против", решая, какую функцию языка реализовать. Передача управления вниз (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
такова:
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
Может, в следующий раз повезет!
Позволяют определить, когда и какой код выполнять. В С# два оператора выбора:
switch,
управляющий ветвлением программы на основе некоторого значения, и //, который выполняет код в зависимости от логического условия. Чаще используется //.
За всю историю программирования, пожалуй, ни один оператор не вызывал столько нареканий, как
goto.
Так что прежде чем рассматривать синтаксис и варианты применения
goto,
ознакомимся с мнениями людей, которые категорически против применения этого оператора, и с проблемами, которые он помогает решить.
Условные операторы С# позволяют управлять ходом программы. Три категории операторов управления ходом программы включают операторы выбора
(if и switch),
итерационные операторы
(while, for
и
foreach)
и операторы перехода
(break, continue, goto
и
return).
Основываясь на материале этой главы вы сможете выбрать операторы для создания хорошо структурированных и удобных в сопровождении приложений.
Оператор
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.
Во встроенных операторах любого из рассмотренных нами итерационных операторов вы можете управлять ходом исполнения программы с помощью одного из операторов перехода:
break, continue, goto
и
return.
Встроенный 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("");
}
}
>
Переменная / внешнего цикла остается в поле видимости внутреннего цикла. А вот переменная у во внешнем цикле недоступна.
Другое применение оператора
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 (;;).
Работают они одинаково, так что их применение — дело вкуса.