Są pewne ważne kwestie, które moim zdaniem przeoczyły wszystkie istniejące odpowiedzi.
Słabe pisanie oznacza umożliwienie dostępu do podstawowej reprezentacji. W C mogę utworzyć wskaźnik do znaków, a następnie powiedzieć kompilatorowi, że chcę go użyć jako wskaźnika do liczb całkowitych:
char sz[] = "abcdefg";
int *i = (int *)sz;
Na platformie little-endian z 32-bitowymi liczbami całkowitymi tworzy i
to tablicę liczb 0x64636261
i 0x00676665
. W rzeczywistości możesz nawet rzutować same wskaźniki na liczby całkowite (o odpowiednim rozmiarze):
intptr_t i = (intptr_t)&sz;
I oczywiście oznacza to, że mogę nadpisać pamięć w dowolnym miejscu w systemie. *
char *spam = (char *)0x12345678
spam[0] = 0;
* Oczywiście współczesny system operacyjny korzysta z pamięci wirtualnej i ochrony strony, więc mogę tylko nadpisać pamięć własnego procesu, ale nie ma nic w samym C, które oferuje taką ochronę, jak każdy, kto kiedykolwiek napisał kod, na przykład Classic Mac OS lub Win16.
Tradycyjne Lisp pozwalało na podobne hakowanie; na niektórych platformach liczby zmiennoprzecinkowe i minusowe komórki były tego samego typu, i można było po prostu przekazać jedną funkcję, oczekując drugiej, i „zadziałałaby”.
Obecnie większość języków nie jest tak słaba, jak C i Lisp, ale wiele z nich wciąż jest nieszczelnych. Na przykład, każdy język OO, który ma odznaczony „downcast”, * to przeciek typu: w zasadzie mówisz kompilatorowi „Wiem, że nie podałem ci wystarczającej ilości informacji, aby wiedzieć, że jest to bezpieczne, ale jestem całkiem pewien tak jest, „gdy cały punkt systemu typów polega na tym, że kompilator zawsze ma wystarczającą ilość informacji, aby wiedzieć, co jest bezpieczne.
* Sprawdzony downcast nie osłabia systemu typów języka tylko dlatego, że przenosi czek do środowiska wykonawczego. Gdyby tak było, wówczas polimorfizm podtypów (inaczej wirtualne lub w pełni dynamiczne wywołania funkcji) stanowiłby to samo naruszenie systemu typów i nie sądzę, aby ktokolwiek chciał to powiedzieć.
Bardzo niewiele języków „skryptowych” jest słabych w tym sensie. Nawet w Perlu lub Tcl nie można pobrać łańcucha i po prostu zinterpretować jego bajty jako liczbę całkowitą. * Warto jednak zauważyć, że w CPython (i podobnie w przypadku wielu innych tłumaczy dla wielu języków), jeśli jesteś naprawdę wytrwały, można użyć ctypes
załadować libpython
, rzutować obiektu id
do POINTER(Py_Object)
i zmusić system typu przeciekać. To, czy powoduje to osłabienie systemu typów, zależy od przypadków użycia - jeśli próbujesz zaimplementować piaskownicę z ograniczeniami wykonania w języku w celu zapewnienia bezpieczeństwa, musisz poradzić sobie z tego rodzaju ucieczkami…
* Możesz użyć funkcji takiej jak struct.unpack
odczyt bajtów i zbudowanie nowej int z „jak C reprezentowałby te bajty”, ale to oczywiście nie jest nieszczelne; nawet Haskell na to pozwala.
Tymczasem niejawna konwersja naprawdę różni się od słabego lub nieszczelnego systemu typu.
Każdy język, nawet Haskell, ma funkcje, powiedzmy, konwersji liczby całkowitej na ciąg lub liczbę zmiennoprzecinkową. Ale niektóre języki wykonują dla Ciebie niektóre z tych konwersji automatycznie - np. W C, jeśli wywołasz funkcję, która chce float
, a przekażesz ją int
, zostanie ona dla Ciebie przekonwertowana. To z pewnością może prowadzić do błędów, np. Z nieoczekiwanymi przepełnieniami, ale nie są to te same rodzaje błędów, które otrzymujesz z systemu słabego typu. A C tak naprawdę nie jest tutaj wcale słabszy; możesz dodać liczbę całkowitą i liczbę zmiennoprzecinkową w Haskell lub nawet połączyć zmiennoprzecinkową z łańcuchem, po prostu musisz to zrobić wyraźniej.
A w przypadku dynamicznych języków jest to dość mętne. W Pythonie i Perlu nie ma czegoś takiego jak „funkcja, która chce liczby zmiennoprzecinkowej”. Ale są przeciążone funkcje, które robią różne rzeczy z różnymi typami, i istnieje silne intuicyjne poczucie, że np. Dodanie łańcucha do czegoś innego jest „funkcją, która chce łańcucha”. W tym sensie Perl, Tcl i JavaScript wydają się robić wiele niejawnych konwersji ( "a" + 1
daje ci "a1"
), podczas gdy Python robi o wiele mniej ( "a" + 1
podnosi wyjątek, ale 1.0 + 1
daje 2.0
*). Po prostu trudno jest sformułować ten sens w kategoriach formalnych - dlaczego nie powinien istnieć ciąg, +
który bierze ciąg i liczbę całkowitą, skoro oczywiście istnieją inne funkcje, takie jak indeksowanie?
* W rzeczywistości we współczesnym Pythonie można to wytłumaczyć podtypami OO, ponieważ isinstance(2, numbers.Real)
jest to prawda. Nie wydaje mi się, żeby istniała jakaś 2
instancja typu łańcucha w Perlu lub JavaScript… chociaż tak naprawdę jest w Tcl, ponieważ wszystko jest instancją łańcucha.
Wreszcie istnieje inna, całkowicie ortogonalna, definicja „silnego” vs. „słabego” pisania, gdzie „mocny” oznacza mocny / elastyczny / ekspresyjny.
Na przykład Haskell pozwala zdefiniować typ, który jest liczbą, łańcuchem, listą tego typu lub mapą ciągów znaków na ten typ, co jest doskonałym sposobem na przedstawienie wszystkiego, co można zdekodować z JSON. Nie ma możliwości zdefiniowania takiego typu w Javie. Ale przynajmniej Java ma typy parametryczne (ogólne), więc możesz napisać funkcję, która pobiera Listę T i wie, że elementy są typu T; inne języki, takie jak wczesna Java, zmusiły cię do użycia Listy Obiektów i spuszczenia. Ale przynajmniej Java pozwala tworzyć nowe typy własnymi metodami; C pozwala tylko tworzyć struktury. I BCPL nawet tego nie miał. I tak dalej, aż do montażu, gdzie jedynymi typami są różne długości bitów.
W tym sensie system typów Haskella jest silniejszy niż współczesny Java, który jest silniejszy niż wcześniejszy Java, który jest silniejszy niż C, który jest silniejszy niż BCPL.
Gdzie więc Python pasuje do tego spektrum? To trochę trudne. W wielu przypadkach pisanie kaczek pozwala symulować wszystko, co możesz zrobić w Haskell, a nawet niektóre rzeczy, których nie możesz; Oczywiście błędy są wychwytywane w czasie wykonywania zamiast czasu kompilacji, ale nadal są wychwytywane. Są jednak przypadki, w których pisanie kaczek nie jest wystarczające. Na przykład w Haskell możesz powiedzieć, że pusta lista liczb całkowitych jest listą liczb wewnętrznych, więc możesz zdecydować, że zmniejszenie +
tej listy powinno zwrócić 0 *; w Pythonie pusta lista jest pustą listą; nie ma informacji o typie, które pomogłyby ci zdecydować, co +
powinno zrobić redukcja .
* W rzeczywistości Haskell nie pozwala ci tego zrobić; jeśli wywołasz funkcję zmniejszania, która nie przyjmuje wartości początkowej na pustej liście, pojawi się błąd. Ale jej system typ jest wystarczająco silny, aby mógł dokonać tej pracy, a Python nie jest.