Рецепты хорошего кода

(Конспект лекции, на которой происходил разбор основных проблем и ошибок, возникших при выполнении домашнего задания.)

Качество написанного кода складывается из трех главных составляющих: понятности, эффективности и правильности.

1 Понятный код

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

1.1 Используйте каскады if

ПЛОХОХОРОШО
if (...) {
  ...
} else
  if (...) {
    ...
  } else {
    ...
  }
}
if (...) {
  ...
} else if (...) {
  ...
} else {
  ...
}

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

1.2 Не злоупотребляйте break и continue

ПЛОХОХОРОШО
for (...; ...; ...) {
  if (...) {
    ...
    continue;
  }
  if (...) {
    ...
    continue;
  }
  ...
}
for (...; ...; ...) {
  if (...) {
    ...
  } else if (...) {
    ...
  } else {
    ...
  }
}

Код слева выглядит уже лучше, чем в примере 1.1, но в нем слишком часто встречается оператор continue.

Операторы break и continue стоит употреблять только в аварийных ситуациях, когда встретилось что-то, что мы не умеем осмысленно обрабатывать и хотим пропустить.

1.3 Не выпендривайтесь

ПЛОХОХОРОШО
x &= 0;
x = 0;

Конструкции вроде x&=0 трудно понять.

Эта конструкция иногда допустима для единообразия, например, если в коде часто встречаются строки вида x&=a и вы хотите подчеркнуть их взаимосвязь.

1.4 Если вы применяете к числам битовые операции, записывайте их в 16-ичной форме

ПЛОХОХОРОШО
x &= 64;
x &= 0x40;
x &= (1 << 6);

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

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

1.5 Не пишите сложно

ПЛОХОХОРОШО
//это невозможно понять
int x = (~0)^((1 << 7) + (1 << 6));
int x = 0x3F;

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

1.6

ПЛОХОХОРОШО
if (a < b)
  return false;
return true;
return a >= b;

Даже несмотря на то, что запись слева может быть для кого-то понятнее записи справа, ее не стоит использовать — она слишком длинная и многих раздражает.

1.7 Не пишите длинных строк

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

1.7.1 Не пишите длинных if'ов

ПЛОХОХОРОШО
if ((...) && (...) && (...) && (...) &&  (...) && (...)) {
  ...
}
if ((...) &&
(...) &&
(...) &&
(...) &&
(...) &&
(...)) {
  ...
}

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

1.7.2 Не пишите комментарии справа

ПЛОХОХОРОШО
while (...) { //Такой комментарий
  ...         //будет очень
  ...         //неудобно редактировать
  ...         //при удалении
  ...         //или добавлении
  ...         //строк в цикл
}
//А такой комментарий не зависит от изменений в коде
while (...) {
...
...
...
}

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

1.8 Используйте сокращенный switch

ПЛОХОХОРОШО
switch (bt) {
  case 0:
    return 0;
  case 1:
    return 0;
  ...
  case 127:
    return 0;
  case -128:
    return 1;
} 
switch (bt) {
  case 0:
  case 1:
  ...
  case 127:
    return 0;
  case -128:
   return 1;
} 

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

2 Правильный код

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

2.1 Помните, что в Java все типы знаковые

ПЛОХОХОРОШО
byte[] array;
int[256] table = {...};
for (int i = 0; i < N; i++) {
  count += table[array[i]];
}
byte[] array;
int[256] table = {...};
for (int i = 0; i < N; i++) {
  int bt = (array[i] > 0) ? array[i] : 256 + array[i];
  count += table[bt];
}

Число array[i] может оказаться отрицательным, и произойдет обращение за пределы массива table.

2.2 Различайте >> и >>>

ПЛОХОХОРОШО
while (N != 0) {
  count += N & 1;
  N = N >> 1;
}
while (N != 0) {
  count += N & 1;
  N = N >>> 1;
}

Цикл слева будет повторяться бесконечно, потому что оператор >> заполняет освободившиеся биты единицами, а не нулями, как >>>. В таком цикле число N никогда не станет равным нулю.

2.3 Запускайте свои программы

ПЛОХОХОРОШО
x = 1 << 7 + 1 << 6;
x = (1 << 7) + (1 << 6);

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

2.4 Думайте о крайних случаях

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

3 Эффективный код

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

3.1 Думайте про количество действий в выражениях

ПЛОХОХОРОШО
//Сдвиг ычисляется по ходу выполнения
y = 1 & (x << 7) 
//Сдвиг вычисляется при компиляции
y = x & (1 << 7)

Там, где это можно, константы нужно объединять.

3.2 Считайте обращения к памяти

Этот пример не на Java, а на C++.
ПЛОХОХОРОШО
unsigned char mask = 0x80;
while (x & mask) {
  ...
}
const  unsigned char MASK = 0x80;
while (x & MASK) {
  ...
}

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

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

3.3 Никогда не вызывайте рядом одну и ту же функцию

ПЛОХОХОРОШО
LinkedList<Byte>  list;
if ((list.get(i) > 0x04) || (list.get(i) < 0x08)) {
  ...
}
LinkedList<Byte>  list;
byte bt = list.get(i);
if ((bt > 0x04) || (bt < 0x08)) {
  ...
}

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