С тех пор, как я посмотрел легендарное видео Wat Гэри Бернхардта, меня завораживает странное поведение некоторых языков программирования. Некоторые из них таят больше сюрпризов, чем другие. Например, для Java написана целая книга с описанием пограничных ситуаций и странной специфики. Для C++ вы просто можете почитать сами спецификации всего за $200.

Далее поделюсь с вами моей коллекцией самых неожиданных, забавных и всё-таки валидных «заклинаний» программирования. По сути, использование этих особенностей поведения ЯП считается пагубным, поскольку ваш код никоим образом не должен быть непредсказуемым. Хорошо, что многие линтеры уже осведомлены и готовы посмеяться над вами, если попробуете какое-то из перечисленных дурачеств. Но как говорится, знание — сила, так что начнём.

Вражеское переназначение True в Python 2


Рифмуется с true, так что вы знаете, что это poo («какашка»).

>>> True = False
>>> True
False

К счастью, такой код выводит SyntaxError в версии Python 3, поскольку True, False и None теперь стали зарезервированными словами. Такая шалость всё-таки далека от подлости в C++, когда вы вставляете #define true false в стандартный заголовочный файл на рабочей машине коллеги.

Призрачное взаимодействие с объектом в Java и Python


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

Integer a = 100;
Integer b = 100;
System.out.print(a == b); // prints true

Integer c = 200;
Integer d = 200;
System.out.print(c == d); // prints false

JVM использует однотипный справочник для значений в диапазоне [-128, 127]. Что ещё более странно, так это соответствующее поведение Python.

>>> x = 256
>>> y = 256
>>> x is y
True

>>> x = 257
>>> y = 257
>>> x is y
False

Пока ничего слишком удивительного.

>>> x = -5
>>> y = -5
>>> x is y
True

>>> x = -6
>>> y = -6
>>> x is y
False

Похоже, нижний предел для интерпретатора Python такой же… -5. Целые числа в диапазоне [-5, 256] получают одинаковые ID. Но всё равно это работает как-то странно.

>>> x = -10
>>> y = -10
>>> x is y
False
>>> x, y = [-10, -10]
>>> x is y
True

Видимо, применение деструктурирующего присваивания сразу меняет правила. Я не уверен, почему так происходит, и даже задал вопрос на Stack Overflow в попытке разобраться. Может быть, повторяющиеся значения в списке указывают на тот же объект для экономии памяти.

Обратная запись с индексом в C


Обратная запись с индексом мгновенно доставляет головную боль любому разработчику.

int x[1] = { 0xdeadbeef };

printf("%x\n", 0[x]); // prints deadbeef

Причина работы такого кода в том, что array[index] на самом деле просто синтаксический сахар для *(array + index). Благодаря коммутативному свойству сложения можно поменять их местами и получить тот же результат.

Оператор «перехода» в C


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

for (x = 3; x --> 0;) {
    printf("%d ", x); // prints 2 1 0
}

«Оператор» --> — это на самом деле два оператора, которые в этом контексте разбираются как (x--) > 0. Известно, что такая штука вызывает немалую путаницу при использовании в продакшне — чистое зло.

Оператор sizeof в C


Оператор sizeof обрабатывается в процессе компиляции, что даёт ему интересные свойства.

int x = 0;
sizeof(x += 1);
if (x == 0) {
    printf("wtf?"); // this will be printed
}

Поскольку объекты оператора sizeof анализируются в процессе компиляции, то выражение (x += 1) никогда не будет запущено. Также любопытно: исследования показывают, что printf("wtf?") — самая популярная строчка кода, которая никогда не поступает в продакшн.

Начало индексов с единицы в Lua, Smalltalk, MATLAB и др…


На форумах /r/programminghumor полно мемов об «индексах, которые начинаются с единицы». Поразительно, но немало языков программирования в реальности используют 1-индексированные массивы. Более полный список см. здесь.

0 соответствует true в Ruby


…и только в Ruby. *

if 0 then print 'thanks, ruby' end # prints thanks, ruby

* правка: В обсуждении Reddit мне указали, что такое справедливо также для Lua, Lisp и Erlang.

Триграфы, диграфы и токены в C


По историческим причинам в C остались альтернативные варианты написания для нецифробуквенных символов.

Триграф Символ Диграф Символ Токен Символ
??= # <: [ %:%: ##
??/ \ :> ] compl ~
??' ^ <% { not !
??( [ %> } bitand &
??) ] %: # bitor |
??! | and &&
??< { or ||
??> } xor ^
??- ~ and_eq &=
or_eq |=
xor_eq ^=
not_eq !=

if (true and true) { // same as if (true && true)
    printf("thanks, c");
}

Некоторое чужеродное оборудование вроде IBM 3270 не позволяло набрать некоторые часто используемые символы в C/C++, так что ввели использование диагрфы, триграфы и токены, чтобы сохранить совместимость с определёнными кодировками.

Надеюсь, статья была интересной. Можете почитать обсуждение на Reddit.

Комментарии (56)


  1. mayorovp
    27.12.2017 10:55

    0 соответствует true в Ruby

    А в языке Perl существует "истинный ноль" — как число он равен 0, но как булево значение он равен истине. Кажется, он записывается как "0 but true".


    1. Idot
      27.12.2017 12:49
      -1

      А чему во всех этих языках равен False?


      1. deril
        28.12.2017 00:09

        В ruby false это nil(аналог null) и false. Всё остальное true.


  1. izzholtik
    27.12.2017 11:36

    В стопервый раз одни и те же особенности работы популярных языков..


  1. Koyanisqatsi
    27.12.2017 11:54
    -2

    Оператор sizeof обрабатывается в процессе компиляции, что даёт ему интересные свойства

    Вы в примере неправильно применили sizeof. Он нужен для переносимости программ с одной машины на другую, т.к. не везде количество байт в каком-то типе переменной одинаково. Какой смысл вызывать sizeof(i++), если переменная i будет иметь всегда одно и тоже количество байт?


    1. iig
      27.12.2017 17:14

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


      1. Koyanisqatsi
        27.12.2017 18:23

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

        И его можно использовать с разными целями.

        У него одна цель — сколько байт занимает тот или иной тип данных. Зачем в него пихать выражение?


        1. iig
          27.12.2017 18:57

          Он нужен для переносимости программ с одной машины на другую

          Его можно использовать в том числе и для этого.


          Зачем в него пихать выражение?

          sizeof(str)-1
          sizeof(str-1)
          Бывают и просто очипятки.


  1. Aeroapplabs
    27.12.2017 11:54

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


    1. Zibx
      27.12.2017 14:24

      Наоборот. === проще — это просто сравнение. Т.е. сравнение по ссылки, а в случае примитивов — по значению. А вот == — приведение к общему типу и последующее сравнение. Приведение осуществляется методами toString или valueOf.


  1. fireSparrow
    27.12.2017 12:21
    +1

    В питоне «is» и не предназначен для сравнения значения переменных, для этой цели служит вполне стандартное "=="
    А «is» проверяет, указывают ли две переменный на один и тот же объект в памяти. Это достаточно специальная операция, и её нужно применять только в тех редких случаях, когда вы чётко понимаете, зачем вам нужна именно такая проверка, а не простая проверка на равенство значений.

    Единственный вариант, когда «is» можно использовать просто так — это для сравнения с None.


  1. Dim0v
    27.12.2017 12:43
    +1

    Касательно Python.


    >>> a = [-10, -10]
    >>> a[0] is a[1]
    True
    >>> b = [-10]
    >>> b.append(-10)
    >>> b[0] is b[1]
    False
    >>> b[0] is a[0]
    False

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


    К слову, если код выше поместить в файл и выполнить его, то вывод станет


    True
    True
    True

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


    1. alix_ginger
      27.12.2017 15:45

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


      1. Dim0v
        27.12.2017 16:52

        Конечно, с этим сложно поспорить. Просто немного прояснил природу явления для тех, кому интересно.


    1. Yngvie
      27.12.2017 17:57

      Видимо это применимо к Python2. Там если записать в одну строку, то результат поменяется


      >>> b = [-10]; b.append(-10); b[0] is b[1]
      True

      Но в Python 3 эта же строка вернет False. Более того, даже для создания списка сразу из двух элементов будет False


      >>> b = [-10, -10]
      >>> b[0] is b[1]
      False

      Даже если положить это в файл.


      1. Dim0v
        27.12.2017 19:37

        На Python 3 «-10» воспринимается не как отдельная константа, а как операция унарного минуса, применённая к константе 10. Замените «-10» на 500 и поведение в третьем питоне станет аналогичным поведению во втором.


  1. rraderio
    27.12.2017 12:55

    Начало индексов с единицы в Lua, Smalltalk, MATLAB и др…
    А почему это плохо? Первый элемент — первый индекс.


    1. Zibx
      27.12.2017 14:27

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


      1. rraderio
        28.12.2017 12:36

        Ну да, в Lua, Smalltalk, MATLAB ведь не умеют манипулировать указателями :)


  1. Akdmeh
    27.12.2017 12:56

    Ни одной шутки про PHP.
    Со спецификации языка (то есть, документированное поведение, не баг!), которое мне на днях попортило нервы как наследие от предыдущего программиста:

    <?php count(false) == 1 //true


    1. cher11
      27.12.2017 14:00

      Ну так а зачем пытаться считать количество элементов в false?

      count(null) // 0
      count([]) // 0
      тут все логично.


      1. Akdmeh
        27.12.2017 14:02

        Функция может возвращать [], если количество результатов равно нолю или false, если возникла ошибка при запросе.
        Да, логичнее бросать Exception, но о них не все PHP-программисты знают.


  1. ivan386
    27.12.2017 12:59

    Недавно узнал что можно делать так:


    BOOL CTigerTree::IsZeroBlock(uint32 nBlock) const
    {
        static const uint64 ZeroHash[37][3] =
        {
    ...
        };
    ...
    CTigerNode* pBase = m_pNode + m_nNodeCount - m_nNodeBase + nBlock;
    return memcmp( ZeroHash[ m_nActualHeight - m_nHeight ], pBase->value, sizeof( pBase->value ) ) == 0;

    Если не указывать второй индекс то возвращается адрес.


    1. mayorovp
      27.12.2017 14:03

      Передача в memcmp указателя и размера этого указателя выглядит как баг…


      1. ivan386
        27.12.2017 15:13

        Там тоже массив.


        class HASHLIB_API CTigerNode
        {
        public:
            CTigerNode();
            uint64  value[3];
            bool bValid;
        };


  1. F0iL
    27.12.2017 14:48

    В C++17 триграфы наконец-то выпилили из стандарта :)


  1. devalone
    27.12.2017 15:27

    >>> x = -5
    >>> y = -5
    >>> x is y
    True
    
    >>> x = -6
    >>> y = -6
    >>> x is y
    False

    А что странного? В документации указано, что is проверяет, одинаковые ли это объекты(не значения), никто не гарантирует, какие будут одинаковыми, а какие нет. Лучше было бы показать неочевидное поведение параметров по умолчанию в функции, например:
    
    >>> def append_to_array(value, array=[]):
    ...     array.append(value)
    ...     return array
    >>> print(append_to_array(10))
    [10]
    >>> print(append_to_array(11))
    [10, 11]
    >>> print(append_to_array(12))
    

    Это происходит потому что параметры по умолчанию создаются во время объявления функции(например при импорте модуля) и array всегда указывает на один и тот же массив.


    1. alix_ginger
      27.12.2017 15:43

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


      1. fireSparrow
        27.12.2017 16:19

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

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


      1. Dim0v
        27.12.2017 17:39

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


        Собственно, решение простое — использовать immutable значения в качестве параметров по умолчанию. Как правило используют None с последующей инициализацией в теле функции:


        def my_func(my_arg=None):
            if my_arg is None:
                my_arg = []
            ...

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


  1. Gryphon88
    27.12.2017 15:35

    Кто-то еще пишет сишный код с триграфами? Кстати, даже с ними нельзя конструировать строку дефайна в макросе.


  1. olegy
    27.12.2017 16:21

    Когда то нарвался в С когда переменные назывались 'and' и 'or': не мог понять почему выпадают ошибки


  1. fireSparrow
    27.12.2017 16:25

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

    False ** False == True
    # Результат этого сравнения - True


    1. Alcor
      27.12.2017 17:06

      А почему должно быть иначе?
      0^0 = 1


      1. fireSparrow
        27.12.2017 17:22

        Для тех, кто уже знает, что bool наследуется от int, всё действительно очевидно. А вот остальным это вполне может взорвать мозг.


      1. Dim0v
        27.12.2017 17:42

        Ну так-то, 0^0 — это неопределенность, а не единица. Так что в любом случае сложно назвать это очевидным.
        Хотя если знать, что с интами питон ловко возвращает 1 для 0**0, вместо выбрасывания ошибки, то действительно все встает на свои места)


        1. fireSparrow
          27.12.2017 17:55

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


          1. Dim0v
            27.12.2017 19:47

            В математике единице равен предел x^x при x стремящемся к нулю справа. А именно 0^0 — это неопределенность, как не крути. И то, что ее принимают за единицу — это именно особенность языка. И факториал нуля тут совершенно не к месту. Он равен единице как «empty product» (хз, есть ли устоявшийся перевод на русский) https://en.m.wikipedia.org/wiki/Empty_product.


            1. fireSparrow
              27.12.2017 19:59

              Тогда уж посмотрите и эту статью:
              en.wikipedia.org/wiki/Zero_to_the_power_of_zero
              Вы не поверите, но там тоже упоминается empty product.
              И, кстати, устоявшийся перевод есть — «пустое произведение»


            1. mayorovp
              28.12.2017 12:37

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

              Например, известная формула (x+y)n = ?k Ckn xk yn-k была бы неприменима при x=0 или y=0 если бы 00 было бы определено как-то кроме 1.


  1. Rsa97
    27.12.2017 17:31

    Erlang:

    > if 
        0 -> true; 
        true -> false 
      end.    
    
    false

    true и false в Erlang — это отдельные атомы, ни с нулём, ни с единицей, ни с каким-либо другим числом не связанные.


  1. Yngvie
    27.12.2017 18:13

    А мне в Python нравился трюк с изменением tuple


    >>> t = ([1, 2], [3, 4])
    >>> t[0].append(10)
    >>> t  # сработало, кортеж содержит тот же список, но в нем новый елемент
    ([1, 2, 10], [3, 4])
    >>> t[0] = t[0] + [20] #  TypeError: 'tuple' object does not support item assignment
    >>> t[0] += [30]  # Сокращенная запись, тот же TypeError, но...
    >>> t
    ([1, 2, 10, 30], [3, 4])

    И пока я искал этот кусочек кода наткнулся на wtfpython @ Github с подборкой таких моментов. Кажется на Хабре даже был перевод той статьи. Вот с chained operations по ссылке мне понравился


    1. fireSparrow
      27.12.2017 23:34

      А в чём трюк-то?
      В строчке, где происходит ошибка, вы говорите питону «Запиши в нулевой элемент тупла результат выражения». Естественно питон отказывается, потому что в тупл нельзя ничего записывать.
      В следующей строчке вы говорите питону «Возьми нулевой элемент тупла, и проделай с ним вот такую манипуляцию». И здесь всё нормально.


      1. Yngvie
        28.12.2017 00:02

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


        Потому как list.__iadd__ не создает новый список, в отличии от list.__add__. Так что + в += отрабатывает, а вот = выдает ошибку.


        Может слово трюк не подходит, но "wtf tuple изменился" реакцию у некоторых вызывает


  1. Hazactam
    27.12.2017 21:59

    Хватает в языках весёлухи:

    Неопределённое поведение

    В качестве еще одного примера неопределенного поведения можно привести код:

    int i = 5;
    i = ++i + ++i;
    При его выполнении переменная i может принять значения 13 или 14 для C/C++, 13 для Java, PHP и C#, 12 при реализации на LISP.


    1. 4031651
      28.12.2017 01:24

      Оффтоп, но по этому поводу вспоминается операция передёргивания

      --i++


  1. Hazactam
    27.12.2017 22:09

    Ну и лурк можно вспомнить :)

    lurkmore.to/++i_+_++i

    У меня основной рабочий язык — Delphi, там подобных чудес, к счастью, минимум. За что и нравится, среди прочего.


  1. master1312
    28.12.2017 12:29

    Питон мне вообще мозг вынес, учитывая то, что еще в комментах понаписали.

    0 соответствует true в Ruby

    Еще в линуксовых шеллах во всех.


    1. mayorovp
      28.12.2017 12:42

      Ну, в шеллах-то все логично. Там 0 соответствует выполнению без ошибки (true), а любое другое число — выполнению с ошибкой (false).

      Это так не только в линуксовых шеллах, кстати. Виндовый cmd.exe тоже так считает…


      1. master1312
        28.12.2017 13:22

        А в Ruby и прочих зачем так сделано? Там же наверное тоже какая-то логика была.


        1. ivan386
          28.12.2017 16:34

          Ну в Lua например всё просто. Есть false и nil остальное всё воспринимается как true. При этом false не равен nil. Nil это отсутсвие значения.


  1. master1312
    28.12.2017 12:58

    sizeof(x+=1);

    Кстати, компилятор люто паникует.
    предупреждение: statement has no effect [-Wunused-value]
    sizeof(x+=1);

    Так что, не забывайте посматривать на ворнинги компилятора.


  1. aragaer
    28.12.2017 16:03

    В том С, который есть у меня под рукой (gcc 5.4.0) нет диграфов и «слов» типа bitand — только что проверил. Но оно есть в С++. В С только триграфы.


    1. Foxeed
      29.12.2017 11:14

      Начиная с gcc 5.1 и clang 3.1 триграфов уже нет, но диграфы еще остались. Забавно, но IBM использует триграфы (Appendix A).


  1. Sirikid
    28.12.2017 19:56

    JVM использует однотипный справочник для значений в диапазоне [-128, 127].

    The JVM will use the same reference for values in the range [-128, 127].

    ...


    Неужели вы не понимаете что переводите?


    1. Hazactam
      29.12.2017 10:07

      Увы, вечная беда переводчиков.