Какие есть виды детекта типов у конвертера
Каждый базовый тип данных занимает определенное количество байт памяти. Это накладывает ограничение на операции, в которые вовлечены различные типы данных. Рассмотрим следующий пример:
int a = 4; byte b = a; // ! Ошибка
В данном коде мы столкнемся с ошибкой. Хотя и тип byte, и тип int представляют целые числа. Более того, значение переменной a, которое присваивается переменной типа byte, вполне укладывается в диапазон значений для типа byte (от -128 до 127). Тем не менее мы сталкиваемся с ошибкой на этапе компиляции. Поскольку в данном случае мы пытаемся присвоить некоторые данные, которые занимают 4 байта, переменной, которая занимает всего один байт.
Тем не менее в программе может потребоваться, чтобы подобное преобразование было выполнено. В этом случае необходимо использовать операцию преобразования типов (операция () ):
int a = 4; byte b = (byte)a; // преобразование типов: от типа int к типу byte System.out.println(b); // 4
Операция преобразования типов предполагает указание в скобках того типа, к которому надо преобразовать значение. Например, в случае операции (byte)a , идет преобразование данных типа int в тип byte. В итоге мы получим значение типа byte.
Явные и неявные преобразования
Когда в одной операции вовлечены данные разных типов, не всегда необходимо использовать операцию преобразования типов. Некоторые виды преобразований выполняются неявно, автоматически.
Автоматические преобразования
Стрелками на рисунке показано, какие преобразования типов могут выполняться автоматически. Пунктирными стрелками показаны автоматические преобразования с потерей точности.
Автоматически без каких-либо проблем производятся расширяющие преобразования (widening) — они расширяют представление объекта в памяти. Например:
byte b = 7; int d = b; // преобразование от byte к int
В данном случае значение типа byte, которое занимает в памяти 1 байт, расширяется до типа int, которое занимает 4 байта.
Расширяющие автоматические преобразования представлены следующими цепочками:
byte -> short -> int -> long
short -> float -> double
Автоматические преобразования с потерей точности
Некоторые преобразования могут производиться автоматически между типами данных одинаковой разрядности или даже от типа данных с большей разрядностью к типа с меньшей разрядностью. Это следующие цепочки преобразований: int -> float , long -> float и long -> double . Они производятся без ошибок, но при преобразовании мы можем столкнуться с потерей информации.
int a = 2147483647; float b = a; // от типа int к типу float System.out.println(b); // 2.14748365E9
Явные преобразования
Во всех остальных преобразованиях примитивных типов явным образом применяется операция преобразования типов. Обычно это сужающие преобразования (narrowing) от типа с большей разрядностью к типу с меньшей разрядностью:
long a = 4; int b = (int) a;
Потеря данных при преобразовании
При применении явных преобразований мы можем столкнуться с потерей данных. Например, в следующем коде у нас не возникнет никаких проблем:
int a = 5; byte b = (byte) a; System.out.println(b); // 5
Число 5 вполне укладывается в диапазон значений типа byte, поэтому после преобразования переменная b будет равна 5. Но что будет в следующем случае:
int a = 258; byte b = (byte) a; System.out.println(b); // 2
Результатом будет число 2. В данном случае число 258 вне диапазона для типа byte (от -128 до 127), поэтому произойдет усечение значения. Почему результатом будет именно число 2?
Число a, которое равно 258, в двоичном системе будет равно 00000000 00000000 00000001 00000010 . Значения типа byte занимают в памяти только 8 бит. Поэтому двоичное представление числа int усекается до 8 правых разрядов, то есть 00000010 , что в десятичной системе дает число 2.
Усечение рациональных чисел до целых
При преобразовании значений с плавающей точкой к целочисленным значениям, происходит усечение дробной части:
double a = 56.9898; int b = (int)a;
Здесь значение числа b будет равно 56, несмотря на то, что число 57 было бы ближе к 56.9898. Чтобы избежать подобных казусов, надо применять функцию округления, которая есть в математической библиотеке Java:
double a = 56.9898; int b = (int)Math.round(a);
Преобразования при операциях
Нередки ситуации, когда приходится применять различные операции, например, сложение и произведение, над значениями разных типов. Здесь также действуют некоторые правила:
- если один из операндов операции относится к типу double , то и второй операнд преобразуется к типу double
- если предыдущее условие не соблюдено, а один из операндов операции относится к типу float , то и второй операнд преобразуется к типу float
- если предыдущие условия не соблюдены, один из операндов операции относится к типу long , то и второй операнд преобразуется к типу long
- иначе все операнды операции преобразуются к типу int
int a = 3; double b = 4.6; double c = a+b;
Так как в операции участвует значение типа double, то и другое значение приводится к типу double и сумма двух значений a+b будет представлять тип double.
byte a = 3; short b = 4; byte c = (byte)(a+b);
Две переменных типа byte и short (не double, float или long), поэтому при сложении они преобразуются к типу int , и их сумма a+b представляет значение типа int. Поэтому если затем мы присваиваем эту сумму переменной типа byte, то нам опять надо сделать преобразование типов к byte.
Если в операциях участвуют данные типа char, то они преобразуются в int:
int d = 'a' + 5; System.out.println(d); // 102
C++: Преобразование типов
Как мы уже поняли разные типы данных по-разному хранятся в памяти компьютера. Например, целочисленное значение 3 может быть сохранено как двоичное 0000 0000 0000 0000 0000 0000 0000 0011, тогда как значение с плавающей запятой 3.0 может быть сохранено как двоичное 0100 0000 0100 0000 0000 0000 0000 0000.
Так что же происходит когда мы инициализируем переменную типа float целым числом.
float f_age < 18 >;
А произойдет следующее: поскольку компилятор не может просто сохранить целое число в переменную с плавающей точкой, он преобразует это число в эквивалентное, но типа float .
Процесс преобразования значения из одного типа данных в другой тип данных называется преобразованием типа.
Преобразования типов могут быть неявные — по решению компилятора и явные по решению программиста.
Неявное преобразование типа выполняется компилятором автоматически, когда требуется один тип данных, но предоставляется другой тип. Подавляющее большинство преобразований типов в C++ являются неявными преобразованиями типов.
Неявное преобразование может происходить в следующих случаях:
- если мы делаем арифметическую операцию двух разных типoв. Преобразование идет в тот тип который шире.
double d_result < 4.2 / 3 >// 3 будет пеобразованна в double
- если происходит инициализация переменной другого типа. Тут уже может быть, что мы например сохраним число типа long в переменную типа int и тут уже зависит от способа инициализации переменной.
double d_num < 3 >; // 3 будет преобразованна в double int num = 3.14; // будет потерянна вещевственная часть int num = < 3.14 >; // ошибка компиляции
- Забегая вперед. При использовании небулевого значения в инструкции if
if (5) < >// 5 будет преобразованно в true
Но явное всегда лучше, чем неявное. В C++ существует 5 различных видов приведений типа: приведения в стиле C, статические приведения, константные приведения, динамические приведения и реинтерпретирующие приведения. Последние четыре иногда называют именованными приведениями.
Здесь мы рассмотрим приведение в стиле С и статическое приведение.
В стандартном программировании на C приведение типов выполняется с помощью оператора (), при этом имя типа, в который необходимо преобразовать значение, помещается в круглые скобки. Вы всё еще можете увидеть, что они используются в коде, преобразованном из C.
int main() < int x < 5 >; int y < 4 >; double d_result < (double)x / y >; // x преобразуется в тип double >
В приведенном выше коде мы явно указали компилятору чтобы он преобразовал int x в double .
C++ также позволяет вам использовать приведение в стиле C с синтаксисом, более похожим на вызов функций:
double d_result < double(x) / y >;
Это работает идентично предыдущему примеру, но тут преимущество в том, что преобразуемое значение заключено в скобки — это упрощает определение того, что конвертируется.
Приведение в стиле С не рекомендуется использовать поскольку под капотом он может выполнять множество различных преобразований в зависимости от контекста и даже убирать константность.
В C++ появился оператор приведения типов static_cast , который можно использовать для преобразования значения одного типа в значение другого типа. Этот способ наиболее безопасен и рекомендован в С++.
int main() < int x < 5 >; int y < 4 >; double d_result < static_cast(x) / y >; >
В угловых скобках указывается тип к которому приводим, а в круглые передаем переменную или число которое приводим.
Задание
В прошлом модуле мы писали программу, которая переводила евро в доллары, а доллары в рубли. На тот момент мы не знали типов данных с плавающей точкой и использовали автоматический вывод типов с помощью auto . Отрефакторите ее используя новые знания, используйте явное приведение типов.
Напишите программу, которая берет исходное количество евро, записанное в переменную euros_count , переводит евро в доллары и выводит на экран. Затем полученное значение переводит в рубли и выводит на новой строчке.
Пример вывода для 100 евро:
125.0 7500.0
- 1 евро = 1.65 долларов
- 1 доллар = 60 рублей
Упражнение не проходит проверку — что делать?
Если вы зашли в тупик, то самое время задать вопрос в «Обсуждениях». Как правильно задать вопрос:
- Обязательно приложите вывод тестов, без него практически невозможно понять что не так, даже если вы покажете свой код. Программисты плохо исполняют код в голове, но по полученной ошибке почти всегда понятно, куда смотреть.
В моей среде код работает, а здесь нет
Тесты устроены таким образом, что они проверяют решение разными способами и на разных данных. Часто решение работает с одними входными данными, но не работает с другими. Чтобы разобраться с этим моментом, изучите вкладку «Тесты» и внимательно посмотрите на вывод ошибок, в котором есть подсказки.
Мой код отличается от решения учителя
Это нормально , в программировании одну задачу можно выполнить множеством способов. Если ваш код прошел проверку, то он соответствует условиям задачи.
В редких случаях бывает, что решение подогнано под тесты, но это видно сразу.
Прочитал урок — ничего не понятно
Создавать обучающие материалы, понятные для всех без исключения, довольно сложно. Мы очень стараемся, но всегда есть что улучшать. Если вы встретили материал, который вам непонятен, опишите проблему в «Обсуждениях». Идеально, если вы сформулируете непонятные моменты в виде вопросов. Обычно нам нужно несколько дней для внесения правок.
Кстати, вы тоже можете участвовать в улучшении курсов: внизу есть ссылка на исходный код уроков, который можно править прямо из браузера.
Полезное
- static_cast
- Преобразования типов и безопасность типов
Конверт детектор — Envelope detector
Электронная схема, которая принимает высокочастотный амплитудно-модулированный сигнал в качестве входа и выдает выходной сигнал, который является огибающей исходного сигнала Сигнал A и его огибающая отмечена красным. Простая схема демодулятора огибающей. Сигнал синего цвета, а величина его аналитического сигнала — красным, показывая эффект огибающей.
Детектор огибающей — это электронная схема, которая принимает (относительно) высокие частоты. сигнал, модулированный по амплитуде, в качестве входа и обеспечивает выход, который является огибающей исходного сигнала.
Детектор огибающей иногда называют пиковым детектором.
- 1 Работа схемы
- 2 Общие соображения
- 3 Определение огибающей
- 4 Диодный детектор
- 5 Прецизионный детектор
- 6 Недостатки
- 7 Демодуляция сигналов
- 8 Аудио
- 9 См. Также
- 10 Ссылки
- 11 Внешние ссылки
Работа схемы
Конденсатор в схеме выше накапливает заряд на переднем фронте и медленно отпускает его через резистор , когда амплитуда входного сигнала падает. Последовательный диод выпрямляет входящий сигнал, позволяя току течь только тогда, когда положительный входной терминал имеет более высокий потенциал, чем отрицательный входной терминал.
Общие соображения
Большинство практических детекторов огибающей используют полуволновое или двухполупериодное выпрямление сигнала для преобразования аудиовхода AC в импульсный сигнал DC. Фильтрация затем используется для сглаживания конечного результата. Эта фильтрация редко бывает идеальной, и на выходе повторителя огибающей, вероятно, останется некоторая «рябь», особенно для низкочастотных входов, таких как ноты из басового инструмента. Уменьшение частоты среза фильтра дает более плавный выход, но снижает высокочастотный отклик. Следовательно, в практических конструкциях необходимо найти компромисс.
Определение конверта
Любой AM или FM сигнал x (t) можно записать в следующей форме
x (t) = R (t) cos (ω t + φ (t))
В случае AM, φ (t) (фазовая составляющая сигнала) постоянна и может игнорироваться. В АМ несущая частота ω также постоянна. Таким образом, вся информация в AM-сигнале находится в R (t). R (t) называется огибающей сигнала. Следовательно, сигнал AM задается функцией
x (t) = (C + m (t)) cos (ω t)
с m (t), представляющим исходное сообщение звуковой частоты, C — амплитудой несущей и R (t), равным C + m (t). Таким образом, если можно извлечь огибающую AM-сигнала, можно восстановить исходное сообщение.
В случае FM передаваемый x (t) имеет постоянную огибающую R (t) = R и можно игнорировать. Однако многие FM-приемники в любом случае измеряют огибающую для индикации уровня принимаемого сигнала.
Диодный детектор
Простейшей формой детектора огибающей является диодный детектор который показан выше. Диодный детектор — это просто диод между входом и выходом схемы, подключенный к резистору и конденсатору параллельно от выхода схемы к земле. Если резистор и конденсатор выбраны правильно, выходной сигнал этой схемы должен приблизительно соответствовать версии исходного (baseband ) сигнала со смещенным напряжением. Затем можно применить простой фильтр, чтобы отфильтровать составляющую постоянного тока.
Прецизионный детектор
Детектор огибающей также может быть сконструирован с использованием прецизионного выпрямителя , подаваемого на фильтр нижних частот.
Недостатки
Детектор огибающей имеет несколько недостатков:
- Вход в детектор должен быть полосовым фильтром вокруг полезного сигнала, иначе детектор будет одновременно демодулировать несколько сигналов. Фильтрация может выполняться с помощью настраиваемого фильтра или, что более практично, супергетеродинного приемника
- . Он более восприимчив к шуму, чем детектор продукта
- . Если сигнал перемодулирован возникнет искажение
Большинство из этих недостатков относительно незначительны и обычно являются приемлемым компромиссом для простоты и низкой стоимости использования детектора огибающей.
Демодуляция сигналов
Детектор огибающей может использоваться для демодуляции ранее модулированного сигнала путем удаления всех высокочастотных компонентов сигнала. Конденсатор и резистор образуют фильтр нижних частот для фильтрации несущей частоты. Такое устройство часто используется для демодуляции радиосигналов AM, поскольку огибающая модулированного сигнала эквивалентна сигналу основной полосы.
Аудио
Детектор огибающей иногда называют повторителем огибающей в музыкальной среде. Он по-прежнему используется для обнаружения изменений амплитуды входящего сигнала для создания управляющего сигнала, который напоминает эти изменения. Однако в этом случае входной сигнал состоит из звуковых частот.
Детекторы огибающей часто являются компонентами других схем, таких как компрессор или авто-квакушек или фильтр с отслеживанием огибающей. В этих схемах повторитель огибающей является частью так называемой «боковой цепи », схемы, которая описывает некоторые характеристики входа, в данном случае его объем.
И расширители, и компрессоры используют выходное напряжение огибающей для управления усилением усилителя. Авто-вау использует напряжение для управления частотой среза фильтра. управляемый напряжением фильтр аналогового синтезатора представляет собой аналогичную схему.
Современные повторители конвертов могут быть реализованы:
- непосредственно как электронное оборудование,
- или как программное обеспечение с использованием либо цифрового сигнального процессора (DSP), либо
- на ЦП общего назначения.
См. также
- Аналитический сигнал
- Конверт атаки-затухания-сустейна-высвобождения
Ссылки
Внешние ссылки
- Детектор конверта
- Конверт восстановление конверта
Ликбез по типизации в языках программирования
Эта статья содержит необходимый минимум тех вещей, которые просто необходимо знать о типизации, чтобы не называть динамическую типизацию злом, Lisp — бестиповым языком, а C — языком со строгой типизацией.
В полной версии находится подробное описание всех видов типизации, приправленное примерами кода, ссылками на популярные языки программирования и показательными картинками.
Рекомендую прочитать сначала краткую версию статьи, а затем при наличии желания и полную.
Краткая версия
Языки программирования по типизации принято делить на два больших лагеря — типизированные и нетипизированные (бестиповые). К первому например относятся C, Python, Scala, PHP и Lua, а ко второму — язык ассемблера, Forth и Brainfuck.
Так как «бестиповая типизация» по своей сути — проста как пробка, дальше она ни на какие другие виды не делится. А вот типизированные языки разделяются еще на несколько пересекающихся категорий:
-
Статическая / динамическая типизация. Статическая определяется тем, что конечные типы переменных и функций устанавливаются на этапе компиляции. Т.е. уже компилятор на 100% уверен, какой тип где находится. В динамической типизации все типы выясняются уже во время выполнения программы.
Примеры:
Статическая: C, Java, C#;
Динамическая: Python, JavaScript, Ruby.
Примеры:
Сильная: Java, Python, Haskell, Lisp;
Слабая: C, JavaScript, Visual Basic, PHP.
Тем-не менее не бывает языков со статической и динамической типизаций одновременно. Хотя забегая вперед скажу, что тут я вру — они действительно существуют, но об этом позже.
Подробная версия
Если краткой версии Вам показалось недостаточно, хорошо. Не зря же я писал подробную? Главное, что в краткой версии просто невозможно было уместить всю полезную и интересную информацию, а подробная будет возможно слишком длинной, чтобы каждый смог ее прочесть, не напрягаясь.
Бестиповая типизация
В бестиповых языках программирования — все сущности считаются просто последовательностями бит, различной длины.
Бестиповая типизация обычно присуща низкоуровневым (язык ассемблера, Forth) и эзотерическим (Brainfuck, HQ9, Piet) языкам. Однако и у нее, наряду с недостатками, есть некоторые преимущества.
Преимущества
- Позволяет писать на предельно низком уровне, причем компилятор / интерпретатор не будет мешать какими-либо проверками типов. Вы вольны производить любые операции над любыми видами данных.
- Получаемый код обычно более эффективен.
- Прозрачность инструкций. При знании языка обычно нет сомнений, что из себя представляет тот или иной код.
Недостатки
- Сложность. Часто возникает необходимость в представлении комплексных значений, таких как списки, строки или структуры. С этим могут возникнуть неудобства.
- Отсутствие проверок. Любые бессмысленные действия, например вычитание указателя на массив из символа будут считаться совершенно нормальными, что чревато трудноуловимыми ошибками.
- Низкий уровень абстракции. Работа с любым сложным типом данных ничем не отличается от работы с числами, что конечно будет создавать много трудностей.
Сильная безтиповая типизация?
Да, такое существует. Например в языке ассемблера (для архитектуры х86/х86-64, других не знаю) нельзя ассемблировать программу, если вы попытаетесь загрузить в регистр cx (16 бит) данные из регистра rax (64 бита).
mov cx, eax ; ошибка времени ассемблирования
Так получается, что в ассемлере все-таки есть типизация? Я считаю, что этих проверок недостаточно. А Ваше мнение, конечно, зависит только от Вас.
Статическая и динамическая типизации
Главное, что отличает статическую (static) типизацию от динамической (dynamic) то, что все проверки типов выполняются на этапе компиляции, а не этапе выполнения.
Некоторым людям может показаться, что статическая типизация слишком ограничена (на самом деле так и есть, но от этого давно избавились с помощью некоторых методик). Некоторым же, что динамически типизированные языки — это игра с огнем, но какие же черты их выделяют? Неужели оба вида имеют шансы на существование? Если нет, то почему много как статически, так и динамически типизированных языков?
Преимущества статической типизации
- Проверки типов происходят только один раз — на этапе компиляции. А это значит, что нам не нужно будет постоянно выяснять, не пытаемся ли мы поделить число на строку (и либо выдать ошибку, либо осуществить преобразование).
- Скорость выполнения. Из предыдущего пункта ясно, что статически типизированные языки практически всегда быстрее динамически типизированных.
- При некоторых дополнительных условиях, позволяет обнаруживать потенциальные ошибки уже на этапе компиляции.
- Ускорение разработки при поддержке IDE (отсеивание вариантов, заведомо не подходящих по типу).
Преимущества динамической типизации
- Простота создания универсальных коллекций — куч всего и вся (редко возникает такая необходимость, но когда возникает динамическая типизация выручит).
- Удобство описания обобщенных алгоритмов (например сортировка массива, которая будет работать не только на списке целых чисел, но и на списке вещественных и даже на списке строк).
- Легкость в освоении — языки с динамической типизацией обычно очень хороши для того, чтобы начать программировать.
Обобщенное программирование
Хорошо, самый важный аргумент за динамическую типизацию — удобство описания обобщенных алгоритмов. Давайте представим себе проблему — нам нужна функция поиска по нескольким массивам (или спискам) — по массиву целых чисел, по массиву вещественных и массиву символов.
Как же мы будем ее решать? Решим ее на 3-ех разных языках: одном с динамической типизацией и двух со статической.
Алгоритм поиска я возьму один из простейших — перебор. Функция будет получать искомый элемент, сам массив (или список) и возвращать индекс элемента, или, если элемент не найден — (-1).
Динамическое решение (Python):
def find( required_element, list ): for (index, element) in enumerate(list): if element == required_element: return index return (-1)
Как видите, все просто и никаких проблем с тем, что список может содержать хоть числа, хоть списки, хоть другие массивы нет. Очень хорошо. Давайте пойдем дальше — решим эту-же задачу на Си!
Статическое решение (Си):
unsigned int find_int( int required_element, int array[], unsigned int size ) < for (unsigned int i = 0; i < size; ++i ) if (required_element == array[i]) return i; return (-1); >unsigned int find_float( float required_element, float array[], unsigned int size ) < for (unsigned int i = 0; i < size; ++i ) if (required_element == array[i]) return i; return (-1); >unsigned int find_char( char required_element, char array[], unsigned int size )
Ну, каждая функция в отдельности похожа на версию из Python, но почему их три? Неужели статическое программирование проиграло?
И да, и нет. Есть несколько методик программирования, одну из которых мы сейчас рассмотрим. Она называется обобщенное программирование и язык C++ ее неплохо поддерживает. Давайте посмотрим на новую версию:
Статическое решение (обобщенное программирование, C++):
template unsigned int find( T required_element, std::vector array )
Хорошо! Это выглядит не сильно сложнее чем версия на Python и при этом не пришлось много писать. Вдобавок мы получили реализацию для всех массивов, а не только для 3-ех, необходимых для решения задачи!
Эта версия похоже именно то, что нужно — мы получаем одновременно плюсы статической типизации и некоторые плюсы динамической.
Здорово, что это вообще возможно, но может быть еще лучше. Во-первых обобщенное программирование может быть удобнее и красивее (например в языке Haskell). Во-вторых помимо обобщенного программирования также можно применить полиморфизм (результат будет хуже), перегрузку функций (аналогично) или макросы.
Статика в динамике
Также нужно упомянуть, что многие статические языки позволяют использовать динамическую типизацию, например:
- C# поддерживает псевдо-тип dynamic.
- F# поддерживает синтаксический сахар в виде оператора ?, на базе чего может быть реализована имитация динамической типизации.
- Haskell — динамическая типизация обеспечивается модулем Data.Dynamic.
- Delphi — посредством специального типа Variant.
- Common Lisp — декларации типов.
- Perl — с версии 5.6, довольно ограниченно.
Сильная и слабая типизации
Языки с сильной типизацией не позволяют смешивать сущности разных типов в выражениях и не выполняют никаких автоматических преобразований. Также их называют «языки с строгой типизацией». Английский термин для этого — strong typing.
Слабо типизированные языки, наоборот всячески способствуют, чтобы программист смешивал разные типы в одном выражении, причем компилятор сам приведет все к единому типу. Также их называют «языки с нестрогой типизацией». Английский термин для этого — weak typing.
Слабую типизацию часто путают с динамической, что совершенно неверно. Динамически типизированный язык может быть и слабо и сильно типизирован.
Однако мало, кто придает значение строгости типизации. Часто заявляют, что если язык статически типизирован, то Вы сможете отловить множество потенциальных ошибок при компиляции. Они Вам врут!
Язык при этом должен иметь еще и сильную типизацию. И правда, если компилятор вместо сообщения об ошибке будет просто прибавлять строку к числу, или что еще хуже, вычтет из одного массива другой, какой нам толк, что все «проверки» типов будут на этапе компиляции? Правильно — слабая статическая типизация еще хуже, чем сильная динамическая! (Ну, это мое мнение)
Так что-же у слабой типизации вообще нет плюсов? Возможно так выглядит, однако несмотря на то, что я ярый сторонник сильной типизации, должен согласиться, что у слабой тоже есть преимущества.
Хотите узнать какие?
Преимущества сильной типизации
- Надежность — Вы получите исключение или ошибку компиляции, взамен неправильного поведения.
- Скорость — вместо скрытых преобразований, которые могут быть довольно затратными, с сильной типизацией необходимо писать их явно, что заставляет программиста как минимум знать, что этот участок кода может быть медленным.
- Понимание работы программы — опять-же, вместо неявного приведения типов, программист пишет все сам, а значит примерно понимает, что сравнение строки и числа происходит не само-собой и не по-волшебству.
- Определенность — когда вы пишете преобразования вручную вы точно знаете, что вы преобразуете и во что. Также вы всегда будете понимать, что такие преобразования могут привести к потере точности и к неверным результатам.
Преимущества слабой типизации
- Удобство использования смешанных выражений (например из целых и вещественных чисел).
- Абстрагирование от типизации и сосредоточение на задаче.
- Краткость записи.
Оказывается есть и даже два.
Неявное приведение типов, в однозначных ситуациях и без потерь данных
Ух… Довольно длинный пункт. Давайте я буду дальше сокращать его до «ограниченное неявное преобразование» Так что же значит однозначная ситуация и потери данных?
Однозначная ситуация, это преобразование или операция в которой сущность сразу понятна. Вот например сложение двух чисел — однозначная ситуация. А преобразование числа в массив — нет (возможно создастся массив из одного элемента, возможно массив, с такой длинной, заполненный элементами по-умолчанию, а возможно число преобразуется в строку, а затем в массив символов).
Потеря данных это еще проще. Если мы преобразуем вещественное число 3.5 в целое — мы потеряем часть данных (на самом деле эта операция еще и неоднозначная — как будет производиться округление? В большую сторону? В меньшую? Отбрасывание дробной части?).
Преобразования в неоднозначных ситуациях и преобразования с потерей данных — это очень, очень плохо. Ничего хуже этого в программировании нет.
Если вы мне не верите, изучите язык PL/I или даже просто поищите его спецификацию. В нем есть правила преобразования между ВСЕМИ типами данных! Это просто ад!
Ладно, давайте вспомним про ограниченное неявное преобразование. Есть ли такие языки? Да, например в Pascal Вы можете преобразовать целое число в вещественное, но не наоборот. Также похожие механизмы есть в C#, Groovy и Common Lisp.
Ладно, я говорил, что есть еще способ получить пару плюсов слабой типизации в сильном языке. И да, он есть и называется полиморфизм конструкторов.
Я поясню его на примере замечательного языка Haskell.
Полиморфные конструкторы появились в результате наблюдения, что чаще всего безопасные неявные преобразования нужны при использовании числовых литералов.
Например в выражении pi + 1 , не хочется писать pi + 1.0 или pi + float(1) . Хочется написать просто pi + 1 !
И это сделано в Haskell, благодаря тому, что у литерала 1 нет конкретного типа. Это ни целое, ни вещественное, ни комплексное. Это же просто число!
В итоге при написании простой функции sum x y , перемножающей все числа от x до y (с инкрементом в 1), мы получаем сразу несколько версий — sum для целых, sum для вещественных, sum для рациональных, sum для комплексных чисел и даже sum для всех тех числовых типов что Вы сами определили.
Конечно спасает этот прием только при использовании смешанных выражений с числовыми литералами, а это лишь верхушка айсберга.
Таким образом можно сказать, что лучшим выходом будет балансирование на грани, между сильной и слабой типизацией. Но пока идеальный баланс не держит ни один язык, поэтому я больше склоняюсь к сильно типизированным языкам (таким как Haskell, Java, C#, Python), а не к слабо типизированным (таким как C, JavaScript, Lua, PHP).
Ладно, пойдем дальше?
Явная и неявная типизации
Язык с явной типизацией предполагает, что программист должен указывать типы всех переменных и функций, которые объявляет. Английский термин для этого — explicit typing.
Язык с неявной типизацией, напротив, предлагает Вам забыть о типах и переложить задачу вывода типов на компилятор или интерпретатор. Английски термин для этого — implicit typing.
По-началу можно решить, что неявная типизация равносильна динамической, а явная — статической, но дальше мы увидим, что это не так.
Есть ли плюсы у каждого вида, и опять же, есть ли их комбинации и есть ли языки с поддержкой обоих методов?
Преимущества явной типизации
- Наличие у каждой функции сигнатуры (например int add(int, int) ) позволяет без проблем определить, что функция делает.
- Программист сразу записывает, какого типа значения могут храниться в конкретной переменной, что снимает необходимость запоминать это.
Преимущества неявной типизации
- Сокращение записи — def add(x, y) явно короче, чем int add( int x, int y) .
- Устойчивость к изменениям. Например если в функции временная переменная была того-же типа, что и входной аргумент, то в явно типизированном языке при изменении типа входного аргумента нужно будет изменить еще и тип временной переменной.
Явная типизация по-выбору
Есть языки, с неявной типизацией по-умолчанию и возможностью указать тип значений при необходимости. Настоящий тип выражения транслятор выведет автоматически. Один из таких языков — Haskell, давайте я приведу простой пример, для наглядности:
-- Без явного указания типа add (x, y) = x + y -- Явное указание типа add :: (Integer, Integer) -> Integer add (x, y) = x + y
Примечание: я намерено использовал некаррированную функцию, а также намерено записал частную сигнатуру вместо более общей add :: (Num a) => a -> a -> a *, т.к. хотел показать идею, без объяснения синтаксиса Haskell’а.
* Спасибо int_index за нахождение ошибки.
Хм. Как мы видим, это очень красиво и коротко. Запись функции занимает всего 18 символов на одной строчке, включая пробелы!
Однако автоматический вывод типов довольно сложная вещь, и даже в таком крутом языке как Haskell, он иногда не справляется. (как пример можно привести ограничение мономорфизма)
Есть ли языки с явной типизацией по-умолчанию и неявной по-необходимости? Кон
ечно.
Неявная типизация по-выбору
В новом стандарте языка C++, названном C++11 (ранее назывался C++0x), было введено ключевое слово auto, благодаря которому можно заставить компилятор вывести тип, исходя из контекста:
Давайте сравним: // Ручное указание типа unsigned int a = 5; unsigned int b = a + 3; // Автоматический вывод типа unsigned int a = 5; auto b = a + 3;
Неплохо. Но запись сократилась не сильно. Давайте посмотрим пример с итераторами (если не понимаете, не бойтесь, главное заметьте, что запись благодаря автоматическому выводу очень сильно сокращается):
// Ручное указание типа std::vector vec = randomVector( 30 ); for ( std::vector::const_iterator it = vec.cbegin(); . ) < . >// Автоматический вывод типа auto vec = randomVector( 30 ); for ( auto it = vec.cbegin(); . )
Ух ты! Вот это сокращение. Ладно, но можно ли сделать что-нибудь в духе Haskell, где тип возвращаемого значения будет зависеть от типов аргументов?
И опять ответ да, благодаря ключевому слову decltype в комбинации с auto:
// Ручное указание типа int divide( int x, int y ) < . >// Автоматический вывод типа auto divide( int x, int y ) -> decltype(x / y)
Может показаться, что эта форма записи не сильно хороша, но в комбинации с обобщенным программированием (templates / generics) неявная типизация или автоматический вывод типов творят чудеса.
Некоторые языки программирования по данной классификации
Я приведу небольшой список из популярных языков и напишу как они подразделяются по каждой категории “типизаций”.
JavaScript - Динамическая | Слабая | Неявная Ruby - Динамическая | Сильная | Неявная Python - Динамическая | Сильная | Неявная Java - Статическая | Сильная | Явная PHP - Динамическая | Слабая | Неявная C - Статическая | Слабая | Явная C++ - Статическая | Слабая | Явная Perl - Динамическая | Слабая | Неявная Objective-C - Статическая | Слабая | Явная C# - Статическая | Сильная | Явная Haskell - Статическая | Сильная | Неявная Common Lisp - Динамическая | Сильная | Неявная D - Статическая | Сильная | Явная Delphi - Статическая | Сильная | Явная
- C# — поддерживает динамическую типизацию, посредством специального псевдо-типа dynamic с версии 4.0. Поддерживает неявную типизацию с помощью dynamic и var.
- С++ — после стандарта C++11 получил поддержку неявной типизации с помощью ключевых слов auto и decltype. Поддерживает динамическую типизацию, при использовании библиотеки Boost (boost::any, boost::variant). Имеет черты как сильной так и слабой типизации.
- Common Lisp — стандарт предусматривает декларации типов, которые некоторые реализации могут использовать также для статической проверки типов.
- D — также поддерживает неявную типизацию.
- Delphi — поддерживает динамическую типизацию посредством специального типа Variant.
Возможно я где-то ошибся, особенно с CL, PHP и Obj-C, если по какому-то языку у Вас другое мнение — напишите в комментариях.
Заключение
Окей. Уже скоро будет светло и я чувствую, что про типизацию больше нечего сказать. Ой как? Тема бездонная? Очень много осталось недосказано? Прошу в комментарии, поделитесь полезной информацией.