четверг, 2 августа 2012 г.

Табы или пробелы в коде? А зачем выбирать что-то одно?

Некоторое время я задавался этим вопросом, но всегда так или иначе предпочитал табы, и для меня это было гораздо удобнее и универсальнее, чем пробелы, несмотря на все нюансы. Единого стандарта у пробельных отступов нет, и я часто встречал код отступом в 2, 4 и даже 3 пробела (в зависимости от эстетических предпочтений программиста). Я сразу выкладываю карты на стол: в данной статье я не собираюсь приводить доводы предпочитать что-то одно, совсем нет, напротив, эти два метода сочетаются, и ещё как! И ваш код отнюдь не будет разваливаться если был написан на табах отступом в 2 пробела и открыт в редактора с отступом табов хоть в 10 пробелов! Тут уже можно спокойно варьировать шириной таба под самые извращённые эстетические потребности.

Итак, перерыв кучу святых войн (от англ. holly wars) о том, что же лучше, меня вдруг осенило, как же просто решить этот извечный вопрос о смысле жизни! И нигде из этих битв сторонников, которые мне довелось перечитать, — об этом не говорилось. И мне показалось это очень странным, и я решил написать эту статью, может это облегчит жизнь таким же как и я, когда-то искавшим для себя такой ответ.

На самом деле нет ничего проще!
 

Два простых правила:

  1. Табы для блочных отступов;
  2. Пробелы — для декорации.
 

    Ещё не уловили суть?


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

    Парочка примеров 


    Нет ничего нагляднее живых примеров. По-этому я возьму достаточно специфичный кусочек кода (см. исходный файл), который написан в нестандартном стиле автора пакетного менеджера npm для node.js, итак — JavaScript код:
    function localLink (target, where, context, cb) {
    ---→log.verbose("localLink", target._id)
    ---→var jsonFile = path.resolve( npm.globalDir, target.name
    ---→···························, "package.json" )
    ---→··, parent = context.parent
    
    ---→readJson(jsonFile, function (er, data) {
    ---→---→if (er || data._id === target._id) {
    ---→---→---→if (er) {
    ---→---→---→---→install( path.resolve(npm.globalDir, "..")
    ---→---→---→---→·······, target._id
    ---→---→---→---→·······, function (er) {
    ---→---→---→---→·········---→if (er) return cb(er, [])
    ---→---→---→---→·········---→thenLink()
    ---→---→---→---→·········}
    ---→---→---→---→·······)
    ---→---→---→} else thenLink()
    
    ---→---→---→function thenLink () {
    ---→---→---→---→npm.commands.link([target.name], function (er, d) {
    ---→---→---→---→---→log.silly("localLink", "back from link", [er, d])
    ---→---→---→---→---→cb(er, [resultList(target, where, parent && parent._id)])
    ---→---→---→---→})
    ---→---→---→}
    
    ---→---→} else {
    ---→---→---→log.verbose("localLink", "install locally (no link)", target._id)
    ---→---→---→installOne_(target, where, context, cb)
    ---→---→}
    ---→})
    }
    Табуляция здесь выделена как ---→ (предполагается, что она шириной 4 пробела), а пробельный отступ как ·. Следует заметить, что я очень удачно выбрал пример, потому как он вскрывает и недостаток такого приёма [1]. Если вы обратили внимание, в коде есть место где идут табы, пробельное декорирование и дальше снова табы для блочного отступа функции, переданной как аргумент другой функции.

    Недостаток


    Многие редакторы кода воспринимают таб как декоратор [2], и иной раз учитывают количество символов (включая пробелы) перед последним табом, и исходя из этого количества — вычитают от последнего таба длину, чтобы в итоге выглядело так, как если бы вместо символов от начала строки до этого места стояли одни табы (надеюсь понятно сформулировал). В итоге при длине таба в 4 пробела мы можем получить отступ у этой вложенной функции в 3 пробела, но как мне кажется это не сильно критично.

    Пример с шириной таба в 2 пробела


    Итак, а теперь для наглядности этого способа приведу этот же пример, но с шириной в 2 табуляции:
    function localLink (target, where, context, cb) {
    -→log.verbose("localLink", target._id)
    -→var jsonFile = path.resolve( npm.globalDir, target.name
    -→···························, "package.json" )
    -→··, parent = context.parent
    
    -→readJson(jsonFile, function (er, data) {
    -→-→if (er || data._id === target._id) {
    -→-→-→if (er) {
    -→-→-→-→install( path.resolve(npm.globalDir, "..")
    -→-→-→-→·······, target._id
    -→-→-→-→·······, function (er) {
    -→-→-→-→·········-→if (er) return cb(er, [])
    -→-→-→-→·········-→thenLink()
    -→-→-→-→·········}
    -→-→-→-→·······)
    -→-→-→} else thenLink()
    
    -→-→-→function thenLink () {
    -→-→-→-→npm.commands.link([target.name], function (er, d) {
    -→-→-→-→-→log.silly("localLink", "back from link", [er, d])
    -→-→-→-→-→cb(er, [resultList(target, where, parent && parent._id)])
    -→-→-→-→})
    -→-→-→}
    
    -→-→} else {
    -→-→-→log.verbose("localLink", "install locally (no link)", target._id)
    -→-→-→installOne_(target, where, context, cb)
    -→-→}
    -→})
    }
    Как видите, — декорирование сохранилось и код не рассыпался.

    Супер наглядность!


    А для полной наглядности, берём подготовленный мною код вот здесь, смотрим для начала в браузере (у меня в FireFox отступ таба по умолчанию аж 8 пробелов), потом открываем его в своём любимом редакторе, меняем ширину таба и любуемся. Напоминаю, что оригинальный код автора можно найти тут.

    Ещё парочка примеров


    Декорирование кода посимвольно везде и всюду, — это я бы сказал причуда для особых ценителей (за редкими исключениями, когда это имеет смысл [3]), но это серьёзная тенденция среди программистов, особенно мною замечено среди C++ кода, посему следующий пример будет на этом языке. Это пример из заголовочных файлов API библиотеки FMOD:
    /*
    ---→'DSPConnection' API
    */
    class DSPConnection
    {
    ··private:
    
    ---→DSPConnection();····/* Constructor made private so user cannot statically instance a DSPConnection class.
    ---→·······················Appropriate DSPConnection creation or retrieval function must be used. */
    
    ··public:
    
    ---→FMOD_RESULT F_API getInput··············(DSP **input);
    ---→FMOD_RESULT F_API getOutput·············(DSP **output);
    ---→FMOD_RESULT F_API setMix················(float volume);
    ---→FMOD_RESULT F_API getMix················(float *volume);
    ---→FMOD_RESULT F_API setLevels·············(FMOD_SPEAKER speaker, float *levels, int numlevels);
    ---→FMOD_RESULT F_API getLevels·············(FMOD_SPEAKER speaker, float *levels, int numlevels);
    
    ---→// Userdata set/get.
    ---→FMOD_RESULT F_API setUserData···········(void *userdata);
    ---→FMOD_RESULT F_API getUserData···········(void **userdata);
    
    ---→FMOD_RESULT F_API getMemoryInfo·········(unsigned int memorybits, unsigned int event_memorybits, unsigned int *memoryused, FMOD_MEMORY_USAGE_DETAILS *memoryused_details);
    };
    В данном примере мы видим равномерно выравненные описания аргументов к функциям. Также здесь затронуто посимвольное декорирование комментариев в коде, что не менее важно! И хочу сообщить, что автор сделал всё за меня, он как и следовало, использовал табы для блочного отступа и пробелы для декорирования. Так же он поставил по 2 пробела перед private: и public: (обычно перед ними нет отступов и они находятся наравне с class), я решил так и оставить для демонстрации с сохранением стиля автора. В итоге код я не менял.

    Также очень популярно использовать посимвольное декорирование для пар ключ-значение и имён переменных и их значений. Приведу пример от себя снова на языке JavaScript:
    //декорированное объявление переменных
    var myVar············= 123
    ··, variable·········= 'check'
    ··, myLovelyVariable·= 'yes!'
    ··, flag·············= false;
    
    //декорированный объект пар ключ-значение (хеш)
    {
    ---→keyFirst····················: 123,
    ---→myLovelyKey·················: 'yes!',
    ---→veryVeryVeryVeryLongKeyName·: 'ok',
    ---→oneMore·····················: true
    }
    
    Вот собственно и всё, на этом примеров по-моему достаточно, чтобы понять суть.

    Итоги


    Следует привести выжимку за и против применения такого метода, немного самокритики не повредит.

    Аргументы за:
    1. Снимается вопрос об стандартизации отступа блоков и отдаётся дань личному предпочтению каждого программиста, один и тот же код, выглядит так, как удобно отдельно взятому человеку;
    2. Не нужно больше выбирать, табы или пробелы, вы используете их вместе;
    3. Вы используете табы и ваш код не разваливается и не разъезжается в зависимости редактора или просмотрщика кода;
    4. Табы легче набирать, во всяком случае быстрее чем 2 или даже 4 нажатия по пробелу, хотя плюсом это считать нельзя, потому как умные современные редакторы кода спокойно делают нужное количество пробелов по нажатию табуляции.
    Аргументы против:
    1. Есть узкие места, которые были описаны, связанные с тенденциями редакторов и просмотрщиков применять к табам декоративные свойства, но во многих языках программирования такие ситуации отсутствуют;
    2. Требуется много внимания, чтобы не ошибиться и правильно расставить табы и пробелы, в некоторых случаях нужно подумать, перепроверить себя, хотя ответ и однозначен.
     

    Заключение


    Хотелось бы завершить парой слов. Я никому ничего не пытаюсь привить и навязать, эта статья не несёт революции, я лишь демонстрирую удачное совмещение инь и янь :), которое на практике разрушает традиционную критику табов. Я могу и ошибаться, что-то не учесть, хотя давно практикую этот метод и у меня пока нет к нему особых претензий.

    Спасибо за внимание, всех благ, читатель! Надеюсь этот материал помог открыть что-то новое для тебя.
     

    Примечания


    [1] Стоит оговориться, что упомянутый недостаток не абсолютен как недостаток и имеет место из-за специфичного стиля написания кода программистом npm.

    [2] Это по-моему следовало бы запретить, отсюда и эти самые святые войны растут, но также я считаю, что не следует утверждать ширину таба, т.к. в этом весь сок, как программисту удобно, такой ширины пусть таб себе и настраивает в редакторе, а другие могут настроить под себя, но в конечном итоге код будет одним и тем же.

    [3] Иногда это полезно для избежания ошибок, например в моём js-коде можно встретить нечто подобное программисту npm, но только касательно секций var для переменных, потому как пропусти ты запятую справа, память может потечь как из ручья, а слева их видно сразу, для js-объектов это не актуально — сразу ошибка синтаксиса. Следует также брать в счёт моменты, когда посимвольное декорирование добавляет очевидности в код, например при разросшихся в длинну if-условий.

    Комментариев нет:

    Отправить комментарий