Всегда пишите спецификацию, часть 1

У Джоэла Спольски несколько лет назад была отличная серия статей о преимуществах написания функциональных спецификаций, определяющих, как продукт выглядит для пользователя. Я хочу немного поговорить о технических спецификациях, которые содержат описания внутреннего устройства решения. Немного ранее, я писал о том, как для вроде бы простого фрагмента кода я начал с написания технической спецификации, а уже потом приступил к написанию кода, четко соответствующего этой спецификации, и написанию тестовых сценариев для проверки каждого пункта спецификации. Я часто пользовался этой техникой, и это почти всегда экономило мне время и усилия. Очень легко застрять в болоте ошибок и сложно определить, когда же код на самом деле готов, если сначала вы не напишите спецификацию.

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

Я думаю, теперь могу пройтись по примерам менее тривиальных проблем, с которыми столкнулся в последнее время. В компиляторе мне понадобилась функция, которая бы принимала корректный с точки зрения метода М список аргументов A1, и возвращала два списка. Первый – список объявлений локальных переменных L, второй – список аргументов A2. Хитрость в том, что вычисление каждого члена списка A2 не должно иметь побочных эффектов. И комбинация «выполнение выполнение L с последующим выполнением M(A2)», должна давать в точности те же результаты, что и выполнение M(A1). (Я знаю, что для всех членов списка A1 выполнены все преобразования для соответствия списку параметров метода M и т.д., и к этому моменту перегруженные методы полностью разрешены).

Не имеет большого значения, для чего мне понадобилась эта функция, вы можете пофантазировать на этот счет самостоятельно.

Итак, я приступил к написанию спецификации, начав с приведенного выше описания: функция T принимает корректный список и т.д. и т.п.

Но такой спецификации не достаточно, чтобы приступить к написанию кода.

Я понял, что некоторые выражения в списке аргументов A1 могут иметь побочные эффекты, а некоторые – нет. Если выражение обладает побочными эффектами, тогда я мог объявить локальную временную переменную, произвести вычисления с побочными эффектами, присвоить результат локальной переменной и затем использовать локальную переменную в качестве выражения в списке A2.

Здорово! Теперь я мог добавить еще несколько предложений в мою спецификацию.


Для каждого аргумента x в списке A1, мы можем генерировать локальную переменную следующим образом: если x не обладает побочными эффектами – сохраняем x в соответствующую позицию списка A2. В противном случае, если x обладает побочными эффектами, генерируем временную локальную переменную в списке L, var temp = x. Затем считаем выражение, вычисляющее временную переменную, и сохраняем локальную переменную в соответствующую позицию списка A2.


Потом я подумал, «а можно ли корректно реализовать эту спецификацию?»

Я начал обдумывать возможные варианты входных параметров в списке A1. Члены списка A1 могут передаваться по значению, как out- или ref-параметры. Я точно знаю, что аргументы являются корректными, поскольку конкретный метод М применим для соответствующего списка аргументов A1. В этой конкретной точке процесса компиляции не нужно беспокоиться о наличии аргументов params, о недостающих параметрах со значениями по умолчанию и т.п. Обо всем этом уже позаботились ранее. Я также обратил внимание, что совершенно не важно, являются ли параметры ref- или out-параметрами (поскольку они, по сути, не отличаются). Единственное отличие для компилятора заключается в различиях правил присваивания для каждого из этих вариантов. Итак, у нас есть два варианта для каждого аргумента: передача параметра по значению или через ref/out параметр.

Если параметр передается, как ref/out-параметр, значит, на самом деле в метод нужно передать управляемый адрес (managed address) переменной. В методе M(ref int x), тип переменной х на самом деле относится к специальному типу int&, который называется «управляемая ссылка на переменную типа int», и который не является корректным типом в языке C#. Вот почему мы скрываем это от вас и требуем глупое ключевое слово «ref» каждый раз, когда вы хотите создать управляемую ссылку на переменную.

К сожалению, при компиляции на язык IL предполагалось, что локальные переменные никогда не будут являться этими магическими типами «управляемых ссылок на переменные». У меня было два варианта. Либо предложить вариант работы с этими ограничениями, либо переписать наиболее сложную часть кода, отвечающего за генерацию IL для поддержки этого сценария. (Код, выясняющий, как работать с управляемыми ссылками весьма сложен.) Я предпочел первый вариант. А это означало расширение спецификации, поскольку наша спецификация не рассматривала эти варианты!


Для каждого аргумента x в списке A1 мы можем генерировать временную переменную следующим образом:

Если x не содержит побочных эффектов, мы просто сохраняем x в соответствующую позицию списка A2.

Иначе, если x обладает побочными эффектами и передается по значению – генерируем … и т.д.

Иначе, если x обладает побочными эффектами и передается в качестве ref/out параметра – происходит чудо.


Понятно, что над последним высказыванием нужно было еще немного поработать.

Итак, я расписал все возможные варианты, которые пришли мне в голову. Ясно, что x должна быть переменной, если ее типом является «ссылка на переменную». Это давало небольшое количество вариантов:


Иначе, если x обладает побочными эффектами, передается в качестве ref/out параметра, мы имеем следующие варианты:

* x – локальная переменная

* x – параметр, передаваемый по значению

* x – out-параметр

* x – ref-параметр

* x – поле экземпляра

* x – статическое поле

* x – элемент массива

* x – разыменованый указатель с помощью *

* x – разыменованый указатель с помощью []

и в каждом случае происходит чудо.


И опять же, совершенно ясно, что этого недостаточно. Я проверил заново мой список, чтобы посмотреть, можно ли избавиться от некоторых вариантов. Можно выкинуть локальные переменные, параметры, статические поля, поскольку они никогда не обладают побочными эффектами. Также, к этому момента анализа я знал, что разыменовывание указателя в форме «pointer[index]» уже заменено на форму «*(pointer + index)». На практике это означает, что мы никогда не можем встретить последний вариант в нашем алгоритме, поскольку предпоследний вариант позаботится обо всем.


Иначе, если x обладает побочными эффектами и передается в качестве ref/out параметров, тогда возможны следующие варианты:

* x – поле экземпляра

* x – элемент массива

* x – разыменованный указатель с помощью *

и в каждом случае происходит чудо.


Затем я начал обдумывать, какие побочные эффекты могут происходить для каждого этого случая. У нас может быть «ref instance.field», «ref array[index]» или «ref *pointer», при этом «instance», «array», «index» и «pointer» могут представлять из себя выражения, обладающие побочным эффектом. («field» не может обладать побочным эффектом, поскольку это всего лишь имя поля.) Итак, мы можем использовать ту же саму спецификацию, что и раньше:


Иначе, если x обладает побочными эффектами и передается в качестве ref/out параметра, тогда возможны следующие варианты:

  • x – это поле экземпляра, которое передается как ref/out-параметр: в этом случае добавляем var temp=instance в список L и ref/out temp.field в список A2.
  • x – это элемент массива, который передается как ref/out-параметр: в этом случае добавляем var t1 = array и var t2 = index в список L и ref/out t1[t2] в список A2.
  • x – это разыменованный указатель, который передается как ref/out-параметр: в этом случае добавляем var temp = point в список L и ref/out *temp в список A2.

И вот теперь мы получили что-то, что я мог реализовать, поэтому я разослал это предложение для анализа, и сразу же погрузился в реализацию.

Эта спецификация содержала ошибки. Можете ли вы указать на ошибки в моей реализации, которые нашли мои коллеги, читая спецификацию?

В следующий раз вас ждет волнующее завершение.

Оригинальное сообщение: Always write a spec, part one