Во многих языках программирования есть такое правило: если нужно сразу обработать несколько объектов, то они должны быть одного типа — это позволит выполнять над ними одинаковые действия и получать предсказуемый результат.
Но в некоторых языках бывает так, что объекты или переменные принадлежат к разным типам или классам, но их всё равно можно смешивать и обрабатывать так, будто они одного вида. У программистов это называется утиной типизацией.
Это статья для общего развития — это знать интересно, но необязательно. Если нужны полезные практические знания, почитайте, как сделать нативное Android-приложение из веб-проекта на JavaScript.
Что такое утиная типизация
Утиная типизация — это приём в программировании, который можно описать фразой:
Если что-то выглядит как утка, плавает как утка и крякает как утка, то, скорее всего, это утка.
Сейчас на самом простом примере покажем, как это работает.
В школьном Паскале переменная типа «строка» обозначается как String, а массивы — как Array, например так:
var s : string; // строка
var a : array[1..3] of integer; // массив из 3 чисел
Пока всё в порядке: в строковой переменной мы будем хранить строку, а в массиве — какую-то последовательность чисел:
s := 'Привет!'
a[1] := 10;
a[2] := 20;
a[3] := 30;
Для вывода массива используем цикл for и в скобках указываем номера элементов:
for i := 1 to 3 do
write(a[i]); // выводим очередной элемент массива
Но если нам нужно точно так же посимвольно вывести строку, то мы точно так же можем это сделать в цикле и указать номера символов в скобках:
for i := 1 to Lenght(s) do
write(s[i]); // выводим очередной символ строки
Чтобы поменять значение элементов массива, можно обратиться к ним напрямую, указав в скобках номер элемента:
a[1] := 10;
Но если нам нужно обратиться к конкретному символу в строке, мы можем точно так же указать его индекс в квадратных скобках:
s[1] = 'Ж'
s[2] := 'C'
Получается, что хотя строка и массив — это разные типы данных, но они ведут себя одинаково. А если строка выводится как массив, заполняется как массив и меняется как массив, то с практической точки зрения это и есть массив и с ним можно работать как с массивом. Это и есть утиная типизация, когда нам неважно, что там на самом деле — важно, как оно себя ведёт и как с ним работать.
Сильнее всего утиная типизация проявляется в объектно-ориентированном программировании.
Где применяется и как работает утиная типизация
В ООП есть такое правило: если ты хочешь работать с объектами, ты должен использовать методы, которые прописаны в их классе. Перевод: «Ты можешь сделать с объектом только то, что разрешил автор, который прописал правила для всех подобных объектов».
Допустим, у нас есть обработчик виртуальных мячиков. Его задача — считать, как виртуальные мячики отскакивают от разных поверхностей. Обработчик действует по такому алгоритму:
- К нему попадает несколько объектов класса Ball.
- У этого класса есть методы drop и catch — то есть мячик можно отпустить и поймать.
- Обработчик берёт первый объект, вызывает метод drop (отпускает), потом метод catch (ловит) и смотрит на результат вычислений.
- Если результат совпал с ожидаемым, переходит к следующему объекту, если нет — выводит сообщение.
- Так обработчик перебирает все объекты, которые к нему попали.
Мы хотим, чтобы этот обработчик смог обработать наши объекты, но другого класса — Cube. То есть ронять и замерять не отскок мячика, а отскок кубика. Для этого наши объекты должны выглядеть снаружи для обработчика точно так же, как объекты класса Ball — это значит, они должны иметь методы drop и catch. С точки зрения обработчика, если объектом можно пользоваться как мячиком, это мячик.
Этим часто пользуются разработчики, когда им нужно сделать совместимый интерфейс со старым алгоритмом, но работающий на новой логике. В этом случае они используют те же методы и названия, что у нужных объектов, но внутри у них новые алгоритмы. Благодаря этому микросервисы общаются между собой.
Проще говоря, если что-то отвечает на запрос как почтовый сервер и присылает данные в формате почтового сервера, то с точки зрения микросервиса это почтовый сервер.
Пример утиной типизации
Представим, что в нашем коде есть фрагмент, который узнаёт длину попавшего к нему объекта и дальше что-то с ней делает. В Python за это отвечает функция len()
, например:
len([1,2,3,4,5]) // выдаст число 5
len('Привет, это журнал «Код»!') // выдаст число 25
Но если в качестве параметра в len() попадёт объект нашего нового класса, то компьютер выдаст ошибку — он не знает, как считать длину объектов.
len(my_new_super_class) // выдаст ошибку
Чтобы обойти это ограничение, можно использовать утиную типизацию. Это значит, что если мы научим объект отдавать длину в ответ на запрос len(), то такой объект ничем не будет в этом плане отличаться от массива или строки. Добавим в наш класс метод __len__ — именно он неявно вызывается при любом подсчёте длины в строках или массивах:
class my_new_super_class:
def __len__(self):
return 888
Перевод: «Мы делаем новый класс объектов, который назвали Мой новый супер класс. Мы можем сделать на основании этого класса объект. Если спросить, какова будет длина этого объекта, то мы стабильно получим число 888»:
len(my_new_super_class) // выдаст число 888
Какие языки поддерживают утиную типизацию
В том или ином виде утиная типизация есть во многих современных языках — с её помощью программисты обходят некоторые ограничения. Чаще всего её можно встретить в скриптовых языках, когда заранее часто неизвестно, с чем придётся работать, но понятно, каким способом это нужно сделать.
Если говорить о самых популярных языках, где есть утиная типизация, то вот:
- Python,
- Perl,
- Ruby,
- JavaScript,
- Go,
- Scala,
- Objective-C.
А если покороче?
Пожалуйста: для утиной типизации неважно, какому типу или классу принадлежат объекты. Главное, как они себя ведут и как работают, а что у них внутри на самом деле — это их дело.