- PVSM.RU - https://www.pvsm.ru -
Типы в программировании можно(и нужно) рассматривать как математические множества.
Мысль хоть и очевидная, но из моей головы давно выветрилась.
Именно поэтому я и решил написать эту статью: чтобы напомнить о ней самому себе и тем, кто о ней тоже забыл или даже не знал.
Сначала вспомним главное определение:
Множество — это коллекция элементов, обладающих общим свойством, которые рассматриваются как единое целое. Элементы множества могут быть любыми: числами, объектами, символами и т.д.
1. Множество целых чисел: {1, 2, 3, 4}
2. Множество гласных букв русского алфавита: {А, Е, И, О, У, Ы, Э, Ю, Я}
В программировании мы часто думаем о типах данных как о чём-то, что отличается от математических множеств. Но если посмотреть на типы данных по-другому, это может расширить наше понимание того, как они устроены и связаны друг с другом.
Давайте разберемся чуть подробнее.
Примеры буду приводить на языке C#, однако их можно воспринимать как псевдо-код
Рассмотрим целочисленные типы.
Мы можем представлять их как множества чисел с определёнными диапазонами:
x ∈ ℤ следует понимать как "x принадлежит множеству целых чисел"
sbyte: {x ∈ ℤ | -128 ≤ x ≤ 127}
short (Int16): {x ∈ ℤ | -32,768 ≤ x ≤ 32,767}
int (Int32): {x ∈ ℤ | -2,147,483,648 ≤ x ≤ 2,147,483,647}
long (Int64): {x ∈ ℤ | -9,223,372,036,854,775,808 ≤ x ≤ 9,223,372,036,854,775,807}
С такой точки зрения, каждый целочисленный тип является подмножеством следующего более крупного типа.
Здесь тип short является подмножеством типа int, который, в свою очередь, является подмножеством типа long.
Понимание типов как множеств помогает лучше понять, почему некоторые приведения типов безопасны, а другие нет. Например:
// Безопасное приведение от short к int: каждый элемент множества "short"
// также является допустимым элементом для множества "int".
short s = 1000;
int i = s;
Однако обратное не всегда верно:
// Небезопасное приведение: требует явного приведения типов и может привести
// к потере данных, поскольку не каждый элемент множества "int"
// является элементом множества "short".
int i = 40000;
short s = (short)i;
Тип bool в C# можно рассматривать как множество с ровно двумя элементами:
bool: {true, false}
Перечисления в C# — отличные примеры конечных множеств. Например:
enum DaysOfWeek
{
Monday, Tuesday, Wednesday, Thursday, Friday, Saturday, Sunday
}
Перечисление DaysOfWeek можно рассматривать как множество: {Monday, Tuesday, Wednesday, Thursday, Friday, Saturday, Sunday}.
Давайте посмотрим, как можно воспринимать и более сложные типы как множества.
Перед нам иерархия типов транспорта:
class Vehicle
{
public string Make { get; set; }
public string Model { get; set; }
}
class Car : Vehicle
{
public int NumberOfDoors { get; set; }
}
class AudiCar : Car
{
public string AudiSpecificFeature { get; set; }
}
Посмотрим на эти типы как на множества:
Vehicle: Множество всех возможных транспортных средств с маркой и моделью.
Car: Подмножество Vehicle включающее все транспортные средства с дверьми.
AudiCar: Подмножество Car включающее все автомобили с особенностями, характерными для Audi.
A ⊂ B следует понимать как "множество A является подмножеством B".
В терминах множеств это будет выглядеть так:
Vehicle = {x | x имеет марку и модель}
Car = {x ∈ Vehicle | x имеет двери}
AudiCar = {x ∈ Car | x имеет особенности, характерные для Audi}
Можно заключить, что AudiCar ⊂ Car ⊂ Vehicle.
Рассмотрим иерархию электронных устройств:
class ElectronicDevice
{
public string PowerOn() {}
}
class Smartphone : ElectronicDevice
{
public string MakeCall() {}
public string SendText() {}
}
class DualCameraSmartphone : Smartphone
{
public string TakePhoto() { }
}
Мы также можем рассматривать эти типы как множества:
ElectronicDevice: Множество всех возможных устройств, которые могут включаться.
Smartphone: Подмножество ElectronicDevice, которое включает все устройства, способные совершать звонки и отправлять сообщения.
DualCameraSmartphone: Подмножество Smartphone, включающее все смартфоны с возможностью делать качественные фотографии с использованием двойной камеры.
В терминах множеств:
ElectronicDevice = {x | x может быть включен}
Smartphone = {x ∈ ElectronicDevice| x может звонить и отправлять сообщения}
DualCameraSmartphone = {x ∈ Smartphone | x может делать фото}
Соответственно, DualCameraSmartphone ⊂ Smartphone ⊂ ElectronicDevice.
Интерфейсы также могут быть рассмотрены с точки зрения множеств.
Например:
interface IComparable
{
int CompareTo(object obj);
}
interface IEquatable<T>
{
bool Equals(T other);
}
Мы можем представить интерфейс IComparable как множество всех объектов, которые имеют метод CompareTo(object obj), а интерфейс IEquatable<T> — как множество всех объектов, которые имеют метод Equals(T other).
Класс, реализующий несколько интерфейсов, можно рассматривать как принадлежащий пересечению этих множеств:
class CompareableInt : IComparable, IEquatable<int>
{
public int Value { get; set; }
public int CompareTo(object obj) {}
public bool Equals(int other) {}
}
A ∩ B следует понимать как "пересечение множеств A и B".
В терминах множеств, CompareableInt принадлежит к IComparable ∩ IEquatable<int>.
Принцип подстановки Барбары Лисков (буква L из SOLID) является фундаментальным принципом объектно-ориентированного программирования, который тесно связан с нашим представлением о типах как о множествах.
Принцип гласит, что объекты в программе должны быть заменяемы экземплярами их подтипов без изменения корректности программы.
С точки зрения множеств, это означает, что каждый элемент множества A должен вести себя так, как ожидается от элементов более широкого множества B, если A ⊂ B.
Рассмотрим знаменитый пример нарушения этого принципа:
class Rectangle
{
public virtual int Width { get; set; }
public virtual int Height { get; set; }
public int Area() => Width * Height; }
}
class Square : Rectangle
{
public override int Width
{ set { base.Width = base.Height = value; } }
public override int Height
{ set { base.Width = base.Height = value; } }
}
Изюминка:
Вот здесь наш Debug.Assert() будет вести себя по разному в зависимости от того, объект какого типа на самом деле был передан в метод - Rectangle или Square.
void IncreaseWidth(Rectangle rectangle)
{
int originalHeight = rectangle.Height;
rectangle.Width += 1;
Debug.Assert(rectangle.Height == originalHeight); // Для квадрата это будет неверно.
}
Чтобы соблюдать LSP, нам нужно гарантировать, что все операции, которые верны для элементов множества Rectangle, также верны для элементов его подмножества Square.
В данном примере кода эта гарантия не соблюдается, поэтому принцип нарушен.
Сказать хотел, что если вы, как и я, погрязли в перекладывании DTO, настройке инфраструктуры, трекинге в Jira/Asana/Whatever, бесконечных созвонах/переписках и забыли, что программирование это, вообще-то, красиво, - попробуйте посмотреть на обыденные вещи(типы, наследование, интерфейсы и т.д.) с другой, непривычной точки зрения.
На самом деле, есть целая область, которая в том числе покрывает и то, о чем мы сегодня говорили.
Она называется - "Теория Типов".
Поэтому, если вас хоть немного заинтересовало то, чем я поделился, рекомендую продолжить изучение и нырнуть чуть глубже.
Начать можно, например, отсюда https://habr.com/ru/articles/758542/ [1]
Автор: akilayd
Источник [2]
Сайт-источник PVSM.RU: https://www.pvsm.ru
Путь до страницы источника: https://www.pvsm.ru/c-2/398371
Ссылки в тексте:
[1] https://habr.com/ru/articles/758542/: https://habr.com/ru/articles/758542/
[2] Источник: https://habr.com/ru/articles/847958/?utm_source=habrahabr&utm_medium=rss&utm_campaign=847958
Нажмите здесь для печати.