Как вы делаете неправильный код выглядит неправильно? Какие шаблоны вы используете, чтобы избежать семантических ошибок?

С тех пор как я впервые сделал ошибку, выполнив задание в if Я всегда писал свои ifs так:

if (CONST == variable) {

чтобы избежать распространенной (по крайней мере для меня) ошибки в этом:

if (variable = CONST) { //WRONG, assigning 0 to variable

И с тех пор, как я прочитал эссе Джоэла Спольски " Неправильный код выглядел неправильно", я пытался применить его советы на практике.

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

21 ответ

Решение

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

То же самое касается координат документа Word. В некотором смысле, Apps Hungarian - это только обходной путь для компиляторов / языков, которые не имеют достаточно строгой проверки типов.

Одна из практик, которую я использую (и с которой не все согласны), всегда заключает в себе блоки кода (в C++) с {и}. Итак, вместо этого:

if( true )
    DoSomething();
else
    DoSomethingElse();

Я бы написал это:

if( true ) {
    DoSomething();
} else {
    DoSomethingElse();
}

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

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

Что более читабельно...

compare("Some text", "Some other text", true);

...или же...

compare("Some text", "Some other text", Compare.CASE_INSENSITIVE);

Следует признать, что иногда это может быть немного излишним, но его нетрудно настроить, улучшает читабельность и снижает вероятность того, что автор ошибочно вспомнит, означает ли "true" "да, делать сравнение с учетом регистра" или " Да, сравните без учета регистра ".

Конечно, такие случаи, как...

setCaseInsenstive(true);

... прост и очевиден, чтобы его оставили в покое.

Всегда объявляйте переменные как "const" (или эквивалент в вашем языке программирования), если нет причин для изменения значения после инициализации.

Если вы сделаете это привычкой, вы в конечном итоге начнете задавать вопросы всякий раз, когда увидите неконстантную переменную.

@ Мэтт Диллард:

Еще одна мера защитного программирования - всегда использовать оператор break в каждом блоке кода переключателя (т. Е. Не допускать, чтобы оператор case "проваливался" до следующего). Единственное исключение - если несколько операторов case должны обрабатываться одинаково.

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

switch( type ) {
case TypeA:
   do some stuff specific to type A
   // FALL THROUGH
case TypeB:
   do some stuff that applies to both A and B
   break
case TypeC:
   ...
}

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

case TypeA:
case TypeB:
   // code for both types

В том же ключе, что и в вашем первоначальном примере, используется этот трюк сравнения строк. Если вы сравните строковую ссылочную переменную со строковым литералом, у вас есть шанс бросить NullPointerException,

if(variable.equals("literal")) { // NullPointerExceptionpossible
    ...
}

Вы можете избежать этой возможности, если перевернете вещи и поместите литерал первым.

if("literal".equals(variable)) { // avoids NullPointerException
    ...
}

@Zack:

Итак, вы говорите, что вместо того, чтобы использовать соглашение об именовании префиксов, вместо этого создавать и всегда использовать два новых класса: SafeString и UnsafeString?

Похоже, гораздо лучший выбор для меня. Ошибки компиляции намного лучше, чем ошибки времени выполнения.

Именно так. Брюс Экель написал эссе, утверждая, что статическая типизация излишня, потому что, в любом случае, вы пишете контрольные примеры, верно? Неправильно. Конечно, я пишу тестовые случаи, но написание хороших тестовых заданий - сложная задача и много работы. Хорошо взять всю помощь, которую вы можете получить, и проверка типов во время компиляции - почти лучшая помощь, которую вы можете получить. Кроме того, при использовании тестов, даже при использовании автоматического процесса проверки при проверке, неудачный тест будет сигнализироваться намного позже, чем ошибка времени компиляции, что приведет к задержке исправления ошибок. Компиляторы могут дать более прямую обратную связь.

Это не значит, что я не вижу преимуществ интерпретируемых языков - я понимаю, но динамическая типизация может быть огромным недостатком. Я на самом деле разочарован тем, что не существует современного интерпретируемого языка со статической типизацией, потому что, как показывает Джоэл, это делает написание правильного кода намного сложнее и заставляет нас прибегать к второму классу хаков, таких как Apps Hungarian.

Хех, я уже наполовину набирал это и думал, было ли это действительно так полезно, но, поскольку Мэтт и Грэм оба опубликовали ответы по этому поводу, я продолжу.

Несколько дней назад при добавлении нового дела в коммутатор я забыл завершить дело перерывом. Как только я нашел ошибку, я изменил отступ моего оператора switch с:

switch(var) {
   case CONST1:
      statement;
      statement;
      statement;
      break;  
   case CONST2:
      statement;
      statement;
      statement;
   case CONST3:
      statement;
      statement;
      break;  
   default:
      statement;
}

(как обычно догадывается большинство людей):

switch(var) {
   case CONST1:
      statement;
      statement;
      statement;
   break;  
   case CONST2:
      statement;
      statement;
      statement;
   case CONST3:
      statement;
      statement;
   break;  
   default:
      statement;
}

Выделить пропущенный разрыв и повысить вероятность того, что я не забуду добавить его, когда добавляю новое дело. (конечно, вы не можете сделать это, если вы условно разбили более чем в одном месте, что я и делал иногда)

Если я делаю что-то тривиальное, например, устанавливаю переменную или вызываю функции из операторов case, то я часто структурирую их так:

switch(var) {
   case CONST1: func1();  break;  
   case CONST2: func2();  break;  
   case CONST3: func3();  break;  
   default: statement;
}

Это делает это совершенно очевидно, если вы пропустите перерыв. Если ваши операторы имеют разную длину, добавьте пробел до выравнивания разрывов вместе со всем, что имеет смысл:

switch(var) {
   case CONST1:          func1("Wibble", 2);  break;  
   case CONST2: longnamedfunc2("foo"   , 3);  break;  
   case CONST3: variable = 2;                 break;  
   default: statement;
}

Хотя, если я передаю одинаковые параметры каждой функции, я бы использовал указатель на функцию (ниже приведен фактический код из рабочего проекта):

short (*fnExec) ( long nCmdId
        , long * pnEnt
        , short vmhDigitise
        , short vmhToolpath
        , int *pcLines
        , char ***prgszNCCode
        , map<string, double> *pmpstrd
        ) = NULL;
switch(nNoun) {
    case NOUN_PROBE_FEED:       fnExec = &ExecProbeFeed;    break;
    case NOUN_PROBE_ARC:        fnExec = &ExecProbeArc;     break;
    case NOUN_PROBE_SURFACE:    fnExec = &ExecProbeSurface; break;
    case NOUN_PROBE_WEB_POCKET: fnExec = &ExecProbeWebPocket;   break;
    default: ASSERT(FALSE);
}
nRet = (*fnExec)(nCmdId, &nEnt, vmhDigitise, vmhToolpath, &cLines, &rgszNCCode, &mpstrd);

В моих инженерных приложениях я делаю единицы измерения и системы отсчёта частью имен переменных. Таким образом, я могу легко обнаружить несоответствия. Примеры:

r1_m  = r2_ft; //wrong, units are inconsistent (meters vs feet)
V1_Fc = V2_Fn; //wrong, reference frames are inconsistent 
               //(Fc=Local Cartesian frame, Fn=North East Down frame)

Konrad Rudolph написал:

Apps Hungarian - это только обходной путь для компиляторов / языков, которые не имеют достаточно строгой проверки типов.

Итак, вы говорите, что вместо того, чтобы использовать соглашение об именовании префиксов, вместо этого создавать и всегда использовать два новых класса: SafeString и UnsafeString?

Похоже, гораздо лучший выбор для меня. Ошибки компиляции намного лучше, чем ошибки времени выполнения.

Я всегда использую скобки в своем коде.

Хотя может быть приемлемо написать:

while(true)
   print("I'm in a loop")

Это было легче читать с брекетами в такте.

while(true){
   print("Obviously I'm in a loop")
}

Я думаю, что это в основном происходит от того, что Java является моим первым языком

бить в удар

Если язык программирования это позволяет, объявляйте переменные только в том месте, где вы их инициализируете. (Другими словами, не объявляйте их все в начале каждой функции.) Если вы последовательно объявляете и инициализируете в одном и том же месте, у вас гораздо меньше шансов иметь неинициализированные переменные.

Еще одна мера защитного программирования всегда заключается в использовании break утверждение в каждом switch кодовый блок (т.е. не позволяйте case Заявление "провалиться" до следующего). Единственное исключение - если несколько операторов case должны обрабатываться одинаково.

switch( myValue ) {
    case 0:
        // good...
        break;
    case 1:
    case 2:
        // good...
        break;
    case 3:
        // oops, no break...
        SomeCode();
    case 4:
        MoreCode();   // WARNING!  This is executed if myValue == 3
        break;
}

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

Лучшее в моих книгах - это шумоподавление. Вот почему я люблю разделение проблем с исключениями, являющимися хорошим примером этого (обеспечение того, чтобы обработка случаев ошибок не была встроенной).

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

Используйте немного венгерского

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

$username = santize($rawusername);

Таким образом, если вы собираетесь сказать echo $rawusername, это будет выглядеть неправильно, потому что это так.

Я играл с трюком (0 == variable), но потеря читабельности - вы должны мысленно переключаться, чтобы прочитать его как "если переменная равна нулю".

Вторая рекомендация Мэтта Дилларда - поставить скобки вокруг однострочных условных выражений. (Я бы проголосовал, если бы мог!)

Еще один прием, который я использую, когда производительность не критична: я определю

void MyClass::DoNothing()
{

}

и использовать его вместо нулевых операторов. Голую точку с запятой легко потерять. Можно добавить числа от 1 до 10 (и сохранить их в сумме) следующим образом:

for (i = 1; i <= 10; sum += i++)
    ; //empty loop body

но это более читабельное и самодокументированное ИМО:

for (i = 1; i <= 10; sum += i++)
{
    DoNothing();
}

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

Вложенные циклы или глубоко вложенный код, как правило, могут быть реорганизованы путем извлечения вложенного кода в функцию / метод. Обычно это облегчает анализ кода.

При определении массивов или объектов в JS (и аналогичных в других языках) я ставлю , перед значением. Это происходило от многократного изменения массива / объекта и получения синтаксической ошибки из-за отсутствия , - легко забыть, если строки длинные.

oPerson = {
    nAge: 18
    ,sFirstName: "Daniel"
    ,sAddress: "..."
    ,...
}

Similairy для заражения струн на длинных линиях я положил + впереди вместо конца "

sLongString = "..........................................................."
    + ".........................................................."
    + ".............."
    + "";

Следующее - очень хорошее чтение от Ювала Лоуи о том, как стилизовать ваш код (C#). Вы можете найти его здесь: http://www.idesign.net/ с правой стороны в разделе "Ресурсы"

или вот прямая ссылка (это сжатый PDF): http://www.idesign.net/idesign/download/IDesign%20CSharp%20Coding%20Standard.zip

Стандарт кодирования IDesign C#, для руководящих принципов разработки и лучших практик Ювала Лоуи

Оглавление:

Предисловие
1. Соглашения об именах и стиль
2. Практика кодирования
3. Настройки проекта и структура проекта
4. Рамочные Руководства
- 4.1 Доступ к данным
- 4.2 ASP.NET и веб-сервисы
- 4.3 Многопоточность
- 4.4 Сериализация
- 4.5 Remoting
- 4.6 Безопасность
- 4.7 Система.Транзакции
- 4.8 Корпоративные услуги
5. Ресурсы

Не используйте имена переменных, которые отличаются только на 1 или 2 символа. Не используйте очень короткие имена переменных. (За исключением петель.)

Используйте как можно меньше глобальных переменных. Это действительно большая проблема, когда у вас есть "int i" как глобальный (да, я видел нечто подобное в реальном проекте:))

Всегда используйте скобки.

Если вы играете на земле HTML, попробуйте получить проверочный код - несколько раз маленький красный крестик из плагина HTML validator давал мне удобный ярлык для решения проблемы, которую я еще даже не заметил.

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

Другие вопросы по тегам