Есть некоторые важные вопросы, которые, я думаю, все существующие ответы упустили.
Слабая типизация означает предоставление доступа к базовому представлению. В C я могу создать указатель на символы, а затем сказать компилятору, что я хочу использовать его как указатель на целые числа:
char sz[] = "abcdefg";
int *i = (int *)sz;
На платформе с прямым порядком байтов с 32-разрядными целыми числами это превращается i
в массив чисел 0x64636261
и0x00676665
. На самом деле, вы можете даже привести указатели к целым числам (соответствующего размера):
intptr_t i = (intptr_t)&sz;
И, конечно, это означает, что я могу перезаписывать память в любом месте системы. *
char *spam = (char *)0x12345678
spam[0] = 0;
* Конечно, современные ОС используют виртуальную память и защиту страниц, так что я могу перезаписывать только память своего собственного процесса, но в самом C нет ничего, что предлагало бы такую защиту, как вам скажет любой, кто когда-либо программировал, скажем, на Classic Mac OS или Win16.
Традиционный Лисп допускал подобные виды взлома; на некоторых платформах двойные слова с плавающей и против ячейками были одного типа, и вы могли просто передать одну функцию, ожидающую другую, и она «сработала».
Большинство языков сегодня не так слабы, как C и Lisp, но многие из них все еще несколько утечек. Например, любой ОО-язык, который имеет неконтролируемый «downcast», * это утечка типа: вы, по сути, говорите компилятору: «Я знаю, я не дал вам достаточно информации, чтобы знать, что это безопасно, но я уверен, что это так, «когда весь смысл системы типов заключается в том, что компилятор всегда имеет достаточно информации, чтобы знать, что безопасно.
* Проверенное снижение не делает систему типов языка более слабой только потому, что она переводит проверку во время выполнения. Если бы это было так, то полиморфизм подтипов (то есть виртуальные или полностью динамические вызовы функций) был бы таким же нарушением системы типов, и я не думаю, что кто-то хочет это сказать.
В этом смысле очень немногие языки сценариев являются слабыми. Даже в Perl или Tcl вы не можете взять строку и просто интерпретировать ее байты как целое число. * Но стоит отметить, что в CPython (и аналогично для многих других интерпретаторов для многих языков), если вы действительно постоянны, вы можно использовать ctypes
для загрузки libpython
, приведения объекта id
к объекту POINTER(Py_Object)
и принудительной утечки системы типов. Делает ли это систему типов слабой или нет, зависит от ваших вариантов использования - если вы пытаетесь внедрить изолированную программную среду с ограниченным исполнением на языке для обеспечения безопасности, вам придется иметь дело с подобными побегами…
* Вы можете использовать такую функцию, как struct.unpack
чтение байтов и построение нового int из «того, как C будет представлять эти байты», но это явно не утечка; даже Haskell позволяет это.
Между тем, неявное преобразование действительно отличается от слабой или негерметичной системы типов.
Каждый язык, даже Haskell, имеет функции, скажем, для преобразования целого числа в строку или число с плавающей точкой. Но некоторые языки будут выполнять некоторые из этих преобразований автоматически - например, в C, если вы вызываете функцию, которая хочет float
, и вы передаете ее int
, она преобразуется для вас. Это может определенно привести к ошибкам, например, с неожиданным переполнением, но это не те ошибки, которые вы получаете от системы слабого типа. И С здесь на самом деле не слабее; Вы можете добавить int и float в Haskell или даже объединить float в строку, вам просто нужно сделать это более явно.
А с динамическими языками это довольно мутно. В Python или Perl нет такой вещи, как «функция, которая хочет плавать». Но есть перегруженные функции, которые делают разные вещи с разными типами, и есть сильный интуитивный смысл, что, например, добавление строки к чему-то другому - это «функция, которая хочет строку». В этом смысле Perl, Tcl и JavaScript, по-видимому, выполняют много неявных преобразований ( "a" + 1
дает вам "a1"
), тогда как Python делает намного меньше ( "a" + 1
вызывает исключение, но 1.0 + 1
дает вам 2.0
*). Просто трудно выразить этот смысл в формальных терминах - почему не должно быть выражения, +
которое принимает строку и целое число, когда есть, очевидно, другие функции, такие как индексация, которые делают?
На самом деле, в современном Python это можно объяснить с помощью подтипов ОО, поскольку isinstance(2, numbers.Real)
это правда. Я не думаю, что есть какой-то смысл в том, что 2
является экземпляром строкового типа в Perl или JavaScript ... хотя в Tcl это действительно так, поскольку все является экземпляром строки.
Наконец, есть другое, полностью ортогональное, определение «сильная» или «слабая» типизация, где «сильный» означает мощный / гибкий / выразительный.
Например, Haskell позволяет вам определить тип, который является числом, строкой, списком этого типа или картой из строк в этот тип, что является отличным способом для представления всего, что может быть декодировано из JSON. Нет способа определить такой тип в Java. Но по крайней мере в Java есть параметрические (универсальные) типы, так что вы можете написать функцию, которая принимает список T и знает, что элементы имеют тип T; другие языки, такие как ранняя Java, заставляли вас использовать List of Object и downcast. Но, по крайней мере, Java позволяет создавать новые типы с помощью собственных методов; C только позволяет создавать структуры. И у BCPL даже этого не было. И так далее до сборки, где единственные типы имеют разную длину в битах.
Таким образом, в этом смысле система типов Haskell сильнее, чем современная Java, которая сильнее, чем более ранняя Java, которая сильнее, чем C, которая сильнее, чем BCPL.
Итак, где Python вписывается в этот спектр? Это немного сложно. Во многих случаях типирование утки позволяет имитировать все, что вы можете делать в Haskell, и даже некоторые вещи, которые вы не можете; Конечно, ошибки отлавливаются во время выполнения, а не во время компиляции, но они все еще отлавливаются. Тем не менее, есть случаи, когда утки не достаточно. Например, в Haskell вы можете сказать, что пустой список целых является списком целых, так что вы можете решить, что сокращение +
по этому списку должно возвращать 0 *; в Python пустой список является пустым списком; нет информации о типах, которая бы помогла вам решить, что +
делать с уменьшением .
* На самом деле, Haskell не позволяет вам сделать это; если вы вызываете функцию Reduce, которая не принимает начальное значение в пустом списке, вы получите ошибку. Но его система типов достаточно мощная, чтобы вы могли сделать это, а Python - нет.