Вы начинаете изучать новый язык программирования. Это может начаться как простое хобби или по необходимости, когда вы посещаете компьютерный класс, который требует от вас изучения языка программирования; скажем, C++ или JavaScript (возможно, вы стремитесь стать веб-разработчиком и обнаружите, что знание JavaScript является более или менее важным навыком). Итак, вы начинаете свое путешествие с создания обязательной программы «Hello World» для своей консоли, изучения встроенных типов данных, поддерживаемых вашим языком программирования, и, в конце концов, вы столкнетесь с, пожалуй, самым интересным из них: массивом (или список в зависимости от жаргона, используемого в языке программирования, который вы изучаете). В этот момент вы, возможно, поняли, что Array пригодится всякий раз, когда возникает необходимость хранить и получать доступ к нескольким данным. Но вы можете спросить, почему индекс массива должен начинаться с 0? Разве не удобнее, если вместо этого индекс будет начинаться с 1?

Одной из областей, где часто используется массив, является зацикливание. Но давайте сначала рассмотрим зацикленный код, который вообще не использует массив, и я попытаюсь показать, что проблема на самом деле возникает из-за того, как компьютер считает. Например, представьте, что мы собираемся написать простой цикл, который будет создавать последовательность Фибоначчи от 1 до указанной точки (как указано пользователем). Каждое число будет напечатано на стандартный вывод, а за ним следует символ новой строки «\n». Код, вероятно, будет выглядеть примерно так (в JavaScript):

var timesArgument = 3;
function printFibonacci(times) {
  let n1 = 0;
  let n2 = 1;
  for (let i = 1; i <= times; i++) {
    console.log(n2);
    n1t = n1;
    n1 = n2;
    n2 += n1t;
  }
}
printFibonacci(timesArgument);
// which will print:
// 1
// 1
// 2

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

  1. То, как он колеблется (или аргумент Дейкстры)

В приведенном выше примере счетчик i начинается с 1, проходит через 2, 3 и, наконец, останавливается, когда становится равным 4. Таким образом, допустимый диапазон счетчика: [1, 2, 3]; который является симметричным включительно. Это означает, что граничные элементы включены в сам диапазон. Если у нас есть диапазон в виде [a, …, z], то фактическое количество содержащихся элементов равно z - a +1. Кроме того, рассмотрите тип диапазона, который генерируется, если мы указываем 0 в качестве аргумента, как выглядит диапазон? Ну, это не может быть в виде [1, 1], так как в этом диапазоне все еще есть один элемент. Единственный способ описать пустой диапазон — сделать граничный элемент в конце меньшим, чем начальный элемент; поэтому ответ [1, 0]. Тот факт, что конец диапазона может быть меньше начала, может вызвать проблемы при разработке программы.

2. Это несколько усложняет определение инварианта цикла

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

var timesArgument = 3;
function printFibonacci(times) {
  let n1 = 0;
  let n2 = 1;
// invariant: we have printed fibonacci number less or equal to (i - 1) times
// invariant is true at this point
  for (let i = 1; i <= times; i++) {
    console.log(n2);
// invariant is false after we print number to the console
    n1t = n1;
    n1 = n2;
    n2 += n1t;
// invariant is true again after incrementing i
  }
// after the loop we can be sure that the invariant still holds true
}
printFibonacci(timesArgument);

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

3. Это не отражает поведение внутреннего языка

В таком языке, как C, имя массива по сути является указателем, ссылкой на ячейку памяти, поэтому выражение array[n]относится к ячейке памяти n-элементов от начального элемента. Это означает, что индекс используется как смещение. Первый элемент массива точно содержится в ячейке памяти, на которую ссылается array (0 элементов), поэтому его следует обозначать как array[ 0]. Большинство языков программирования были разработаны таким образом, поэтому индексация с 0 в значительной степени присуща языку.

С учетом сказанного давайте посмотрим на другой подход к той же проблеме выше:

var timesArgument = 3;
function printFibonacci(times) {
  let n1 = 0;
  let n2 = 1;
// invariant: we have printed exactly i fibonacci numbers
// invariant is true at this point
  for (let i = 0; i != times; i++) {
    console.log(n2);
// invariant is false after we print number to the console
    n1t = n1;
    n1 = n2;
    n2 += n1t;
// invariant is true again after incrementing i
  }
// after the loop we can be sure that the invariant still holds true
}
printFibonacci(timesArgument);

Этот подход имеет ряд преимуществ по сравнению с предыдущим подходом:

Во-первых, диапазон стал асимметричным в виде (a, z] или (0, 3]. В таком обозначении хорошо видно, сколько элементов содержится в этом диапазоне: просто z -a или 3 - 0; пустой диапазон также выглядит гораздо более интуитивно понятным: (0, 0], сохранение нам нужна своеобразная ситуация, когда конечная граница меньше начальной.

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

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

И это, возможно, причины, по которым многие компьютерные языки предназначены для подсчета от 0 (и индекс вашего массива тоже). Что вы думаете, ребята?