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

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

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();
}

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


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

что более читаемо...

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

...или...

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

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

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

setCaseInsenstive(true);

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


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

Если вы сделаете это привычкой, вы в конечном итоге начнете задавать вопросы всякий раз, когда увидите переменную non-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?

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

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

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


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

несколько дней назад, добавляя новый случай к коммутатору, я забыл закончить дело с перерывом. Как только я нашел ошибку, я изменил отступ моего оператора 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)

Конрад Рудольф писал:

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

Итак, вы говорите, что вместо использования соглашения об именовании префиксов вместо создания и всегда использовать два новых класса: 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

но это более читаемый и самодокументирующийся IMO:

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

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

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

всегда использовать скобки.


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

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

стандарт кодирования IDesign C# для руководящих принципов разработки и лучших практик Juval Lowy

содержание:

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


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

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

Similairy для струнного contacination на длинных линиях я поставил + на фронте вместо окончания"

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

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

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