Поиск

Печально известный оператор 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.