Pull to refresh

GOTO в прикладном программировании

Reading time6 min
Views17K

Картинка из книги Thinking Forth

По мотивам «обобщенной» статьи про GOTO и статьи про GOTO в системном программировании:

Мотивы для использования GOTO и альтернативы ему принципиально отличаются для системного и прикладного программирования — это является и важной причиной холиваров. Для прояснения ситуации рассмотрим GOTO только в разрезе прикладного программирования.

Основной тезис: в прикладном программировании GOTO однозначно лучше обходить.

Как докажем:
  1. В прикладном программировании критически важен один параметр кода — сопровождаемость.
  2. Goto не ухудшает однозначно сопровождаемость только в небольшом проценте случаев, и даже в этих случаях принципиально от альтернатив не отличается
  3. Ради небольшого процента случаев его использовать вредно:
    1) очень низкоуровневое, поэтому сильно развращает программиста (возникает соблазн использовать и в других местах) — большой вред из-за небольшого процента случаев, когда GOTO можно применить;
    2) даже в таких случаях есть более красивые альтернативы.



GOTO — свойства и влияние на качество кода


Параметры качества кода
(считаем, что код написан правильно, все нештатные ситуации учтены):
  1. Потребление ресурсов (памяти, процессорных тактов) — приоритет «от машины»
  2. Сопровождаемость (легкость сопровождения) кода — приоритет «от человека»:
    1) читаемость кода — насколько легко понять написанный код (соответственно, насколько легко и написать, и отладить),
    2) ошибкоустойчивость при изменениях — насколько сложно внести ошибку/насколько легко ее заметить при изменении кода,
    3) легкость поддержки общего стандарта — насколько написание кода приучает программиста к отклонениям от неких общих стандартов
Общие свойства GOTO:
  1. неструктурировано: можно вставить в почти произвольное место, сложно понять, как мы туда попали, в отличие от остальных конструкций ветвления;
  2. закрепощает исходный код: если структурированные блоки можно менять по-разному, перестраивать их порядок, как в конструкторе, то GOTO — это гвоздь, соединяющий какие-то блоки конструктора — после его внедрения код изменить уже очень непросто.

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

Что не является GOTO:
  1. другие конcтрукции управления потоком выполнения — if,switch,while и т.п.: в них всех ветвления потока жестко заданы синтаксисом, находятся на границе того же блока — верхней или нижней (для return — граница функции), а GOTO можно размещать произвольно
  2. автоматически сгенерированный код — как и в сгенерированном ассемблере, в нем копаться и его поддерживать не приходится.

Особенности GOTO в прикладном программировании


Прикладное программирование здесь — программирование на языках высокого уровня, поддерживающих структурирование кода, в том числе структурный подход к обработке исключений: Java, C#, C++, интерпретируемые языки и т.п. — в общем, стандартный прикладной мэйнстрим. C не рассматриваю как низкоуровневый язык, используемый сейчас в основном для системного программирования.

Особенности прикладного программирования:
  1. нет необходимости в точечных оптимизациях — отдельных тактов или ячеек памяти, поэтому экономию ресурсов можно отбросить из рассмотрения GOTO — остается только сопровождаемость
  2. есть возможность как угодно структурировать логику — как угодно объединять в функции/методы, заводить сколько угодно переменных, классов и т.п. с любыми названиями, возможность бросать исключения
GOTO только ухудшает сопровождаемость кода

В системном программировании важна максимальная экономия ресурсов, поэтому там, возможно, применение GOTO для этой цели оправдано.
А в прикладном программировании параметр «потребление ресурсов» можно отбросить, остается только параметр сопровождаемости, который GOTO ухудшает.


GOTO — проблемы и варианты исправлений


Рассмотрим применение GOTO в различных вариантах перемещения по структуре кода и альтернативы ему:

1. Вход в блок извне:

1.1 Вход в «не цикл»:
легко и очевидно переписывается без GOTO:

		if(a)
			GOTO InsideIf;

		if(b) {
			foo();
	InsideIf:
			bar();
		}

=>

		if(b) {
			foo();
		}
		
		if(a || b) {
			bar();
		}

1.2 Вход в цикл:
нельзя: вообще непонятен поток выполнения:

		if(goIntoCycle)
			GOTO MiddleOfCycle;
			
		for(...) {
			foo();
			
	MiddleOfCycle:
			bar();
		}


2. Переход внутри одного блока:
нет необходимости, легко переписывается, обычно на if/else:

		if(bNotNeeded)
			GOTO startFromC:
		
		B();
		
	startFromC:
		C();

=>

		if(bNeeded) {
			B();
		}
		
		C();


3. Выход из блока наружу

Это основной случай возможного применения GOTO. Разобьем его на еще более мелкие и рассмотрим подробно на примерах.
Общий подход — максимально декомпозируем: разбиваем на методы по смыслу, логику фиксируем в флагах с говорящими названиями — получаем читабельный и самодокументированный код.

Важные правила:
  1. НЕ ИСПОЛЬЗУЕМ ИСКЛЮЧЕНИЯ:

    1) исключения всегда используем для обработки ошибок и внештатных ситуаций, поэтому не используем их для чего-либо еще, чтобы не мозолить глаз;

    2) можем случайно «проглотить» исключение с внутреннего уровня вложенности;

    3) это дорогое удовольствие — в прикладном программировании крохоборствовать негоже, но и так разбрасываться ресурсами при наличии простых альтернатив нет смысла;
  2. Избегаем множественных return, особенно с разных уровней вложенности: иначе мало отличий от GOTO — читабельность кода так же затруднена в большинстве случаев. (Возможно, это требует отдельного обоснования — тоже тот еще холивар).

3.1. Единственный выход из одного уровня вложенности:
тривиально заменяется if/break и т.п.

3.2. Несколько выходов из одного уровня вложенности:

3.2.1 Обработка ошибок — только через исключения
(надеюсь, это очевидно; если нет — могу объяснить в отдельной статье)

3.2.2 Перебор вариантов — на примере if:

	class Tamagochi {

		function recreate() {
			if(wannaEat) {
				eat();
				GOTO Cleanup;
			}		
			
			if (wannaDrink) {
				drink();
				GOTO Cleanup;
			}
			
			if(wannaDance) {
				Dance();
			}		
			
			return HAPPY; //true
			
	Cleanup:
			return washHands() && cleanKitchen();
		}
	}

Проблемы (кроме всегда присущей GOTO неочевидности потока выполнения):
захотели добавить поведение sleep в случаях wannaEat и wannaDance — все, обобшение для wannaEat и wannaDrink разрушено.

Как сделать красиво (сразу расширенный вариант):

		function recreate() {
			if(wannaEat) {
				eat();
				needCleanup = true;
				needSleep = true;
			}		
			
			if (wannaDrink) {
				drink();
				needCleanup = true;
			}
			
			if(wannaDance) {
				Dance();
				needSleep = true;
			}		
			
			result = HAPPY; //true
			
			if(needCleanup)
				result = result && washHands() && cleanKitchen();
			
			if(needSleep)
				result = result && sleep();
			return result;
		}


3.3. Выход из нескольких уровней вложенности.

3.3.1 Если легко выделить разную логику (разные ответственности):

	class BatchInputProcessor {

		function processInput(inputRecords) {
		
			for (inputRecord in inputRecords) {
				for (validator in validators) {
					if (!validator.check(inputRecord)
						GOTO ValidationFailed;
				}
				
				item = new Item();
				for (fieldValue in inputRecord.fieldValues)	{
					setFieldValue(fieldValue.field, fieldValue.value);
				}
				
				itemsToStore.add(item);
			}

			return store(itemsToStore);
			
	ValidationFailed:
			log(failedValidation);
			
			tryToCorrect(inputRecords);
			...
		}
	}

=>

		function processInput(inputRecords) {
			allRecordsAreValid = true;
			for(inputRecord in inputRecords) {
				recordIsValid = validateRecord(inputRecord, validators);
				
				if(!recordIsValid) {
					allRecordsAreValid = false;
					break;
				}
				
				else {
					itemToStore = createItemFromRecord(inputRecord);
					itemsToStore.add(item);
				}
				
			}
			
			if(allRecordsAreValid) {
				result = store(itemsToStore);
			}
			
			else {
				log(failedValidation);			
				tryToCorrect(inputRecords);
				...
			}
		}


3.3.2 Сложнее выделить разную логику или при этом усложняется код.

Как правило, это может быть в случае однотипных вложенных циклов:

	function findOrCreateTriadaOfEqual(firstArray, secondArray, thirdArray) {
		result = null;
		for(first in firstArray) {
			for(second in secondArray) {
				for(third in thirdArray) {
					if(first == second && second == third) {
						result = array(first, second, third);
						GOTO Found:
					}
				}
			}
		}
		
		equal = new Item();
		result = array(equal, equal, equal);
		
	Found:
		log(result);
		return result;
	}


Вариантов все равно много:
  1. вынести в отдельную подфункцию с return из внутреннего цикла — самый простой
  2. обобщить — сделать рекурсивную функцию вида findEqualInArrays(arrayOfArrays, currentArrayIndex, currentFoundItemsArray);
  3. «if(result) break» — самый топорный:
		result = null;
		for(first in firstArray) {
			for(second in secondArray) {
				for(third in thirdArray) {
					if(first == second && second == third) {
						result = array(first, second, third);
						break;
					}
				}
			
				if(result)
					break;
			}
			
			if(result)
				break;
		}


Это — единственный вариант, который смотрится хуже, чем GOTO, и GOTO даже понятнее. Но практически всегда есть и другие варианты.

Для оставшегося исчезающе малого процента случаев, когда других вариантов нет, нужно просто решить, что все равно можно сделать хотя бы флагами, зато гайдлайны будут проще — «Без GOTO!».

Резюме:


Важнее всего — сопровождаемость.

GOTO всегда ухудшает сопровождаемость, поэтому

нужно обходиться без GOTO — достаточно стандартных средств:
  1. обработка ошибок через исключения;
  2. декомпозиция — большой метод, решающий много задач, разбивается на маленькие, решающщие отдельные задачи;
  3. фиксация логики (вычисленных условий и выражений) в переменных с говорящими названиями.
Tags:
Hubs:
Total votes 52: ↑36 and ↓16+20
Comments46

Articles