Форум программистов, компьютерный форум, киберфорум
CoderHuligan
Войти
Регистрация
Восстановить пароль
Карта форума Блоги Сообщество Поиск Заказать работу  
Рейтинг: 5.00. Голосов: 1.

Типизация функций - зло или добро?

Запись от CoderHuligan размещена 17.02.2023 в 13:47
Обновил(-а) CoderHuligan 17.02.2023 в 14:03

Это продолжение размышлений из позапрошлого поста данного блога.
На этот раз разговор пойдет о функциях.
Обычные языки программирования (ЯП) имеют не только типизированные наборы данных - структурный тип, но и как ни странно, это распространяется и на функции (процедуры). То есть: каждая отдельная функция представляет собой совершенно отдельный тип. Происходит это от того, что сигнатуры функций могут различаться, как по количеству формальных аргументов, так и по их типу, и по их порядку размещения. А так как это все не поддается никакой систематизации с точки зрения языка, и отдано на откуп программистам, то и получается, что сигнатура функции представляет собой отдельный "функциональный" тип.
Хорошо это или плохо я не знаю, вот и пытаюсь выяснить..
Плохо то, что порядок размещения, тип и количество аргументов накладывает дополнительную нагрузку на мозг программиста, так как все это надо помнить, а если и не помнить, то обращаться к докам, что замедляет процесс кодирования.
К тому же, вызывающая функция обязана знать эту сигнатуру, иначе вызов вызываемой функции становится невозможен. А это означает одно: большую связность кода, что приводит к проблемам переносимости, расширения, изменения и т.д.
Раньше, когда памяти было немного, такой подход был оправдан. Однако сейчас памяти у мас завались: её некуда девать, а производительность от этого не увеличилась, а продолжает падать..
Далее.
По соглашениям функция (к примеру в языке Си или С++) оставляет возвращаемое значение в регистре EAX. Так уж повелось и закрепилось. Однако для передачи аргументов используется стек в памяти. Иначе невозможно обеспечить рекурсивность, когда функция, к примеру, вызывает сама себя. Однако в обычных приложениях рекурсивные алгоритмы используются крайне редко, поэтому парадигма, которая заточена только на то, что кто-то когда-то может использовать рекурсию, становится достаточно накладной.

Допустим у функции есть два или три аргумента. Её вызывает другая функция. Эта другая знает, что вызываемая имеет три аргумента определенных типов. И вот, нам нужно изменить сигнатуру вызываемой: добавить четвертый аргумент. Добавили. При этом нам требуется изменить и код тех функций, которые вызывают данную, так как её сигнатура изменилась.. Это не просто плохо, а очень плохо, и считается дурным тоном. Но от этого никуда не деться. ООП прошу не предлагать.
То есть изменился тип функции, теперь он стал другим, и по цепочке это повлияло на все вызывающие функции.
Это плохо потому, что каждый программный компонент становится зависим от других, а ведь все мечтают о независимости модулей.. Оопщики это понимают, но нашли выход в другой, еще более развесистой лапше.
Как можно было бы выйти из данной ситуации? Вот два примера:
C
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
char s1[20]="haha";
char s2[20]="hoho";
char * concat(char *c1, char *c2)
{
    int m, i;
  m=strlen(c1); i=0;
  while((*(c1+m+i) = *(c2+i)) !='\0')
    i++;
  return c1;
}
int main(void)
{
  printf("%s\n", concat(s1, s2));
  exit(0); 
}
Это пример традиционного подхода. Если сигнатура функции concat изменится, то код в main перестанет работать.
Вот пример другого подхода:
C
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
char s1[20]="haha";
char s2[20]="hoho";
typedef struct T
{
  char *c1;
  char *c2;
}T;
char * concatS(T *a)
{
    int m, i;
  m=strlen(a->c1); i=0;
  while((*(a->c1+m+i) = *(a->c2+i)) !='\0')
    i++;
  return a->c1;
}
 
int main(void)
{
  T d;
  d.c1=s1;
  d.c2=s2;
  printf("%s\n", concatS(&d));
  exit(0); 
}
Теперь, при изменении сигнатуры структурного типа T, например, путем добавления еще одного поля, код в main будет продолжать работать, сколько бы и каких бы функций мы не вызывали.
Что мы видим в последнем примере? Ну, то, что теперь мы имеем функцию с одной точкой входа в неё. Именно с одной! То есть функция возвращает одно значение и принимает одно значение, хотя фактически работает с двумя! Через единственный указатель..
Появляется сразу вопрос: а что мы можем с этого получить? новую парадигму? Может быть, а может быть и нет. Но, то, что это приносит свои плюшки - очевидно.
Если с одним структурным типом работает множество функций, то изменение этого типа, не приносит изменений этих других функций просто потому, что налицо наличие единственного типа сигнатуры функции, за исключением типа единственного аргумента. Но и это обстоятельство можно было бы преодолеть.. А как это уже другой вопрос..
Размещено в Без категории
Показов 8512 Комментарии 97
Всего комментариев 97
Комментарии
  1. Старый комментарий
    Аватар для CoderHuligan
    Кстати, при повторном вызове concatS если первая строка не нуждается в изменении, её не нужно и модифицировать, передавая структуре только один аргумент:
    C
    1
    2
    3
    4
    5
    6
    7
    8
    
    int main(void)
    {
      T d;
      d.c1=s1;
      d.c2=s2;
      printf("%s\n", concatS(&d));
      d.c2=s2;
      printf("%s\n", concatS(&d));
    А если не надо модифицировать и вторую строку, то аргументы трогать вообще не нужно:
    C
    1
    2
    3
    4
    5
    6
    7
    8
    
    int main(void)
    {
      T d;
      d.c1=s1;
      d.c2=s2;
      printf("%s\n", concatS(&d));
      printf("%s\n", concatS(&d));
      printf("%s\n", concatS(&d));
    При обычном использовании, пришлось бы заново передавать по два аргумента в функцию.
    По сути, функция завязанная на структуру имеет свой особый интерфейс с внешним миром, который отделяет этот мир от деталей реализации. Если представить себе гипотетический язык, в котором эта концепция "от данных" могла бы полностью реализоваться, то мы получили бы более быстрый, ясный и поддерживаемый код. В данном случае (язык Си) это невозможно по органическим ограничениям, которые накладывает на разработчика данный язык.
    Запись от CoderHuligan размещена 17.02.2023 в 16:11 CoderHuligan вне форума
  2. Старый комментарий
    Иными словами , пользуясь жаргоном ООП, вы создали DataTransferObject . Если не нравится ООП, назовите Data Transfer Structure


    Но, по сути вы ни чего не изменили. Что значит "вдруг" добавился еще аргумент? Т.е. у вас функция в реальном проекте может легко менять свой контракт? По хорошему, это уже не хорошо. Допустим это объективно необходимо. Добавляем аргумент и это нас заставит внимательно просмотреть все вызовы этой функции и принять решение по поводу нового параметра. Иначе большой риск что то забыть и получить баг, возможно хорошо замаскированный. Допустим в остальных случаях нам не важно значение этого параметра. Так может речь о значении по умолчанию? Ставим значение по умолчанию и вот ни чего не надо переписывать, там где это не важно.

    Теперь рассмотрим ваш вариант со структурой которая может меняться. Это, на самом деле, скрывает проблему, а не решает.
    1. Добавили новое свойство. Применили его при одном вызове, а в других местах, по своей сути используется "по умолчанию"
    2. Убрали свойство.. И вот тут пошла "веселуха".

    Т.е. DTO штука полезная для того чтобы не было 100500 параметров у функции. А для того, чтобы можно было "легко" менять ее контракт.... Штука опасная.

    Для не больших проектов, которые пишешь в одиночку.... Может быть, но очень сомнительно. Для больших и где есть команда - опасная практика. (что правка DTO что правка набора параметров функции). При этом при смене сигнатуры ситуация более управляемая.
    Запись от voral размещена 17.02.2023 в 16:22 voral вне форума
  3. Старый комментарий
    Если эта функция является интерфейсом библиотеки. Т.е. ее могут вызывать сторонние приложения и библиотеки - это вообще плохая истрия.
    Запись от voral размещена 17.02.2023 в 16:23 voral вне форума
  4. Старый комментарий
    Аватар для CoderHuligan
    Цитата:
    Убрали свойство.. И вот тут пошла "веселуха".
    Свойства нельзя убирать. В том же типе Т нельзя убрать одно свойство, так как это невозможно. Добавить можно.
    Цитата:
    Если эта функция является интерфейсом библиотеки. Т.е. ее могут вызывать сторонние приложения и библиотеки - это вообще плохая истрия.
    А вы посмотрите в msdn win api. Там часть функций именно так и реализована. Чтобы воспользоваться функцией надо сперва заполнить поля структуры, а потом передавать её адрес функции api. Правда там и другие параметры могут использоваться. А как по иному работать со структурами? Не передавать же их по значению. То ест: какая там рекурсия и пр. когда сталкиваемся с реальным кодингом. А ведь все эти вызовы реализованы через импортируемые либы. И все структуры уже готовы к принятию информации: они обьявлены в заголовочных файлах. Никаких проблем не вижу.
    Запись от CoderHuligan размещена 17.02.2023 в 18:01 CoderHuligan вне форума
  5. Старый комментарий
    Цитата:
    Сообщение от CoderHuligan Просмотреть комментарий
    Свойства нельзя убирать. В том же типе Т нельзя убрать одно свойство, так как это невозможно. Добавить можно.
    А чем это регламентируется? От этого мы как либо защищены?

    Цитата:
    Сообщение от CoderHuligan Просмотреть комментарий
    А вы посмотрите в msdn win api. Там часть функций именно так и реализована. Чтобы воспользоваться функцией надо сперва заполнить поля структуры, а потом передавать её адрес функции api.
    "плохая история" это я про изменение структур и изменения сигнатуры. В обоих случаях происходит изменгение контракта с внешним миром. Только если изменилась сигнатура (в любую сторону) - мы об этом узнаем на этапе разработки. Если изменилась структура - тут нет ни какой однозначности. (единственное тут зависит от ЯП, если допустимы динамические свойства - то может будет сюрприз и неожиданное поведение, уже в рантайме, при каком то стечении входных данных)

    А так в общем то я и сказал, что ни чего нового вы не открыли. Этим пользуются.


    Цитата:
    Сообщение от CoderHuligan Просмотреть комментарий
    Никаких проблем не вижу.
    Структуры удобны что б не городить бородилы параметров, группировать их по логике и т.п. А для озвученной вами проблемы они мало чего решают. По хорошему, что структуру изменили, что сигнатуру - проверять надо все. А так, чем отличается что вы в добавили свойство, что в параметры - у вас произошло изменение контракта данной функции. И надо обязательно принимать меры, проверять.

    А по ссылке или по значению - тут, на мой взгляд, другие параметры для выбора должны быть. Т.е. если функция внезапно создаст сайдеффект и "отредактирует" входные данные. Это всегда ли ожидаемо... Типа отправляем printStruct(ourStruct) ожидая что она просто выведет в консоль, а там горе программист нафигачил костылей (ну или чего то атм "дебажил" и забыл)
    Запись от voral размещена 17.02.2023 в 18:58 voral вне форума
  6. Старый комментарий
    При этом если мы добавляем параметр, то мы оцениваем важность:
    - параметр обязательный - тут хоть со структурой хоть с параметрами мы должны будем править код, где вызывается функция
    - параметр не обязательный - задаем значение по умолчанию.
    Запись от voral размещена 17.02.2023 в 19:05 voral вне форума
  7. Старый комментарий
    Аватар для XLAT
    Цитата:
    Сообщение от CoderHuligan Просмотреть комментарий
    win api.
    человек который делал винапи:


    что-то в нем мне напоминает:
    Запись от XLAT размещена 17.02.2023 в 22:01 XLAT вне форума
    Обновил(-а) XLAT 17.02.2023 в 22:04
  8. Старый комментарий
    Аватар для CoderHuligan
    Цитата:
    человек который делал винапи:
    Это ложь.
    Запись от CoderHuligan размещена 18.02.2023 в 11:20 CoderHuligan вне форума
  9. Старый комментарий
    Аватар для XLAT
    Цитата:
    Сообщение от CoderHuligan Просмотреть комментарий
    Это ложь.
    да, часто бывает, что правда никому не нравится.

    этот чел был главный в мс - там в мс под его дудку плясали все индусы-погромисты.

    разве винапи не похож на плоское гавнище - набор из неразбираемых функций без всякой иерархии.

    да - не похож - а является этим самим гавнищем!

    вы вот это не видите, а в том же ms это увидели,
    пытались исправлять, например, gdi+.

    ну, и основной апофеоз вдумчивого проектирования с раскаяниями за их говноапи у мс было с выходом FW.

    Цитата:
    Сообщение от CoderHuligan Просмотреть комментарий
    Это ложь.
    щас уже почти середина 21 века.
    выкиньте винапи на помойку и никогда его не трогайте.

    я вам истину говорю.
    Запись от XLAT размещена 19.02.2023 в 09:05 XLAT вне форума
    Обновил(-а) XLAT 19.02.2023 в 09:14
  10. Старый комментарий
    Аватар для CoderHuligan
    Цитата:
    этот чел был главный в мс
    Но не он же конкретно писал код. Это обычный менеджер. NT писали профи. Это потом они все разбежались кто куда и мы получили вин 10 - действительно: унылое г.ще.
    Цитата:
    разве винапи не похож на плоское гавнище - набор из неразбираемых функций без всякой иерархии.
    Типа иерархии классов VCL? Вот это действительно неразбираемое г...о.
    И наоборот: win api довольно понятны для настоящих профи. все остальные, кто не осилил говорят, что это неразбираемо. Ну, может где-то косяки и есть, но где их нет. Конечно апи не верх совершенства, признаю, но под win написано столько полезного софта на этих самых, причем хорошо работающего, что посылать на три буквы свои инструменты - как-то не комильфо. Если вы стойкий ненавистник виндовс и сидите на линукс тогда понятно. Но в линукс дыр гораздо больше, чем в виндовс: истину говорю!
    Запись от CoderHuligan размещена 19.02.2023 в 11:52 CoderHuligan вне форума
  11. Старый комментарий
    Аватар для XLAT
    Цитата:
    Сообщение от CoderHuligan Просмотреть комментарий
    Типа иерархии классов VCL?
    это потому что его делали всё те же индусы с уже испорченными сишкой мозгами.

    поэтому,
    Уважаемые Школьники,
    изучение программирования начинайте с изучения нормальных языков, типа С++, а не Си.
    Запись от XLAT размещена 19.02.2023 в 15:39 XLAT вне форума
  12. Старый комментарий
    Аватар для CoderHuligan
    Лучший способ исковеркать себе мозги это начать с с++
    Запись от CoderHuligan размещена 19.02.2023 в 15:44 CoderHuligan вне форума
    Обновил(-а) CoderHuligan 19.02.2023 в 15:46
  13. Старый комментарий
    Аватар для CoderHuligan
    Цитата:
    Сообщение от XLAT Просмотреть комментарий
    разве винапи не похож на плоское гавнище - набор из неразбираемых функций без всякой иерархии.
    Вы напоминаете вот того самого школьника. Вспомните api, которые предоставляла ms dos. Вспомнили? В регистр AH заносился номер функции. В остальные регистры параметры, если таковые были. потом делался системный вызов/прерывание int21h. Ок? Без всякой системы, что касается параметров. А чем win api отличаются от dos api? Только объемом и большим удобством. Это ведь всего лишь прослойка между вызовами функций драйверов из кольца №0 и пользовательским уровнем. А как иначе? Объяснили бы. А уж поверх win api нагородили кучу библ, которые реально и пользуют все кому не лень. То есть вот на этом "гавнище", - удобрении, выросли прекрасные цветочки высокоуровневых либ, на вроде TK. И что - они плохо работают? Отнюдь нет. Тогда о чем вообще разговор?
    Запись от CoderHuligan размещена 19.02.2023 в 15:55 CoderHuligan вне форума
    Обновил(-а) CoderHuligan 19.02.2023 в 15:57
  14. Старый комментарий
    Аватар для XLAT
    Цитата:
    Сообщение от CoderHuligan Просмотреть комментарий
    ООП прошу не предлагать.
    Цитата:
    Сообщение от CoderHuligan Просмотреть комментарий
    Вы напоминаете вот того самого школьника
    Цитата:
    Сообщение от CoderHuligan Просмотреть комментарий
    вызывающая функция обязана знать эту сигнатуру
    нет - не обязана,
    держите:
    https://rextester.com/IPJJM91230
    C++
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    
    ///----------------------------------------------------------------------------|
    /// ...
    ///---------------------------------------------------------------------------:1
    #include <iostream>
    #include <string>
     
    namespace std
    {   template<typename T>
        T to_wstring(T& s)
        {   return s;
        }
     
        std::wstring to_wstring(char c)
        {   return std::wstring(1,  c);
        }
     
        std::wstring to_wstring(const char* c)
        {   std::string s(c);
            return std::wstring(s.begin(), s.end());
        }
    }
     
    template<typename ... T>
    auto concat(T ... t)
    {   return (std::to_wstring(t) + ...);
    }
     
    int main()
    {   setlocale(0, "");
     
        std::wstring promt{L"Конкатенация в 21 веке: "};
        std::wcout << concat(promt,       "oderHuligan ", 2023,    '\n');
        std::wcout << concat(promt, 'C', L"oderХулиган ", 2023) << '\n' ;
    }
    не благодарите.
    Запись от XLAT размещена 19.02.2023 в 16:28 XLAT вне форума
  15. Старый комментарий
    Аватар для CoderHuligan
    Цитата:
    нет - не обязана,
    Я говорю о механизмах существующих реализаций. Все можно припрятать подальше от глаз, но истина от этого не перестанет быть истиной. Сколько костылей нагородили не имеющих отношения к решению поставленной задачи? Уже не считаем..
    Код:
    proc1
     x=proc2(a, b, c)
    end proc1
    В proc1 явно прописаны параметры proc2. Значит proc1 "знает" часть сигнатуры функции proc2. Это для компилятора важно знать, но не для функции proc1. Компилятор должен вычислить адреса на стеке и очистить стек от параметров на выходе. Для этого он должен знать количество параметров их типы и порядок.
    Но proc1 конечно знать это не обязана. Для неё главное отдать свои переменные, а сколько их будет в сигнатуре функции ей знать не обязательно.. В идеале обращение должно идти через общий для всех интерфейс, например таблицу функций. Обращение идет к интерфейсу таблицы, а уж он делает вызов. Главное не прописывать жестко в самом коде вот эти сигнатуры, которые связывают код в один клубок. В идеале функции вообще знают только имена самих функций и общаются посредством внешних данных. Или через общую шину данных, например стек. Функция кладет на стек три параметра, но она не знает сколько уже положено туда до неё. Она не знает, что следующая берет со стека 4 параметра..
    Поэтому изменение реализаций одних не сильно волнует остальных.
    Запись от CoderHuligan размещена 19.02.2023 в 17:44 CoderHuligan вне форума
    Обновил(-а) CoderHuligan 19.02.2023 в 17:47
  16. Старый комментарий
    Аватар для XLAT
    Цитата:
    Сообщение от CoderHuligan Просмотреть комментарий
    Функция кладет на стек три параметра, но она не знает.
    функция не знает, а вот вам край надо знать.
    учите инкапсуляцию - она сэкономит много ваших сил для по настоящему полезных задач.
    Запись от XLAT размещена 19.02.2023 в 20:03 XLAT вне форума
  17. Старый комментарий
    Цитата:
    Сообщение от CoderHuligan Просмотреть комментарий
    Сколько костылей нагородили не имеющих отношения к решению поставленной задачи? Уже не
    Просто не надо задачу "высасывать из пальца". ВЫ ради "легкой" смены количества параметров функции, предлагаете каждую функцию, снабдить своей структурой. Т.е. теперь вместо "просто вызвать функцию", надо создавать структуру.

    При этом, как я выше сказал, но вы на это не отреагировали ситуацию (которую описали), на самом деле, "не исправили". Точно также если добавился новый параметр и он важный, мы должны поправить код везде, где это необходимо, если не важный - значит он имеет значение по умолчанию. Что так же решается "обычным" способом..

    Между тем в реальности стоит задуматься, а правильно ли мы делаем, что меняем функцию, подозреваю, что в большинстве случаев, правильным решением будет сделать еще одну. Собственно так же задаться вопросом, а часто ли такое желание возникает в принципе?

    И в итоге получаем что ради редких ситуаций вы предлагаете замусорить код заполнением структур.
    Запись от voral размещена 20.02.2023 в 00:09 voral вне форума
  18. Старый комментарий
    При этом, повторюсь, струрктуры в качестве параметров могут быть полезны, но задача у них другая.
    Запись от voral размещена 20.02.2023 в 00:10 voral вне форума
  19. Старый комментарий
    Аватар для CoderHuligan
    Цитата:
    ВЫ ради "легкой" смены количества параметров функции, предлагаете каждую функцию, снабдить своей структурой. Т.е. теперь вместо "просто вызвать функцию", надо создавать структуру.
    Нет, не обязательно структуру. Я не деталях веду речь. Структуру можно создавать и автоматически в гипотетическом ЯП. Смысл не в этом. Смысл найти такие механизмы, синтаксис, чтобы один компонент был слабо связан с другими. Выше мне указали, мол, применяй инкапсуляцию, даже не задумавшись, что предлагают дополнительный механизм связывания для компонентов, от чего надо наоборот уходить.. Инкапсулированные методы становятся зависимыми от своих классов, а должны быть независимыми. Об этом Мейер, писал: самая инкапсулированная функция есть функция вне какого-либо класса. Она инкапсулирована в самой себе.
    Цитата:
    Точно также если добавился новый параметр и он важный, мы должны поправить код везде, где это необходимо, если не важный - значит он имеет значение по умолчанию. Что так же решается "обычным" способом..
    Я об этом и говорю: если функции завязаны друг на друга, то изменение одной влечет изменения всех. а предположим, что код пишется командами, одна из которых ничего не знает о модулях другой, но вынуждена пользоваться общей библиотекой? Или интерфейсом модуля другой команды? Шило в одном месте.
    Запись от CoderHuligan размещена 20.02.2023 в 11:25 CoderHuligan вне форума
  20. Старый комментарий
    А иначе ни как. Вы хотите добиться абсолютной несвязанности? так не бывает. Разве что искусственный интеллект подключать даже в функции вычисления квадрата. Точнее функция в каждом модуле остается одна, и уже сама както вычисляет "че хотел"

    Так что все по прежнему.

    Публичная функция модуля (экспортируемая наружу). Должна иметь четкий контракт. Это хорошо и правильно.
    История о том, что она вдруг поменялась - попахивает не хорошим.

    Т.е. по прежнему с одной стороны: если возникло желание добавить параметр - скорее всего это причина создать другую функцию.

    С другой. ЕСли вам повезло взять в проект функцию, автор которой постоянно что то меняет: то тут самое правильное решение сделать обертку для нее. Т.е. в модуле создаем слой в котором будет "взаимодействие с внешним миром. Там создаем обертки, а вот уже в основном функционале - вызываем только обертку.

    А вообще прежде чем начинать искать выход. Надо понять а есть ли проблема. Часто ли в реальных проектах встречаются ситуации когда есть объективные причины менять сигнатуру публичной функции?

    Конечно могу привести тут же свой пример: в языке PHP есть ряд функций схожих по назначению, но у которых параметры (схожие по назначению) перепутаны местами. И это несколько "раздражает". Ну если вдруг решат поменять - ну это будет единичная ситуаций, которая крайне редка. в данном случае еще добавили поддержку именованных параметров (как в некоторых других ЯП, кстати) - так что может и не случиться никогда такого.

    Цитата:
    Сообщение от CoderHuligan Просмотреть комментарий
    Я об этом и говорю: если функции завязаны друг на друга, то изменение одной влечет изменения всех. а предположим, что код пишется командами, одна из которых ничего не знает о модулях другой, но вынуждена пользоваться общей библиотекой? Или интерфейсом модуля другой команды? Шило в одном месте.
    Типа команда на угад и рандомно берет чужие функции, просто чтоб использовать? Такого быть не может. Как выше и сказал: если берется чужая либа, авторы которой поступают очень плохо, но альтернативы нет. Значит надо писать обертки свои.

    Ради таких нерадивых разработчиков нет необходимости усложнять жизнь большинству.
    Запись от voral размещена 20.02.2023 в 12:31 voral вне форума
 
КиберФорум - форум программистов, компьютерный форум, программирование
Powered by vBulletin
Copyright ©2000 - 2024, CyberForum.ru