18+
Как прожить еще один день

Объем: 158 бумажных стр.

Формат: epub, fb2, pdfRead, mobi

Подробнее

Предисловие

Никто меня не просил. Второй книги, как и первой, никто не ждал. Более того, отзывы о первой книге скорее негативные. Она разошлась безумным тиражом. Может быть, пятьдесят экземпляров. Кто ее действительно читал? Совершенно не впечатлились моей биографией. Не оценили мои шизоидные рассказы. Я не дал миру того, чего он хотел. В этой Книге я тоже не дам. Попробую повторить или даже превзойти успех первой книги. Естественно, все это сарказм. Тогда я выложил все свои рассказы. С тех пор я написал всего два, может, три. Поэтому, если я заставил вас это прочитать, можете быть спокойны. Всего пару раз я погружу вас в пучину шизоидного бреда.

На этот раз я почти не буду утомлять вас своей биографией. К тому же с тех пор, как я сел в кресло, биография почти остановилась. Конечно, я лукавлю. Я построил дом. Переехал в него из другого города. Еще раз женился. Кому-то этого хватает на всю жизнь. По личным ощущениям я сильно замедлился. Перечитывая книгу о своей жизни, понимая, какую дичь я творил последние годы… Какая-то часть меня наслаждается покоем.

Здесь есть иллюстрация

Зарегистрируйтесь или войдите, чтобы увидеть ее и другие изображения

Санаторий-профилакторий у Ильича. Кто понял, тот понял. Эта книга будет о конструировании. О программировании и изобретательстве. Разумеется, немного об искусственном интеллекте. Сначала я начал его использовать, потому что руки уже совсем не торт. Поскольку я довольно четко могу формулировать свои мысли, со скрипом и недоверием я начал им пользоваться. Естественно, только для программирования. Конструирование — настолько творческий процесс, что я даже в голове не могу описать задание. Правильно говорить «Промпт».

Я буду говорить о своих изобретениях. Конечно, ими могут пользоваться все. Правда, в том и дело, что я изобретал это все, чтобы преодолеть свою немощь. Для первой книги я использовал голосовой блокнот, который нашел в интернете. Он мне не сильно нравился. Не имея альтернативы, я написал свой. Эти страницы я пишу уже в своем блокноте. О нем я напишу немного подробнее чуть позднее. Я расскажу о своем экзоскелете. Докажу, что он не «экзоскелет курильщика». Я расскажу о своей мыши на голову для того, чтобы двигать курсор без использования рук. Я расскажу о материалах, которые использую и знаю. Поговорю о технологиях их обработки. Поговорю о сопротивлении материалов. Если первая книга была больше похожа на брошюру, то сейчас графоман внутри меня заставляет писать полноценную книгу. Поэтому заранее прошу прощения.

Немного вводной информации для тех, кому посчастливилось не читать первую книгу. Меня зовут Иван. В результате тяжелого течения аутоиммунного заболевания практически не слушаются ноги. Плохо слушаются руки. Пострадала речь. Весьма относительно нормально работает только голова. Все мои изобретательства, все мои книги служат целью только одного — не свихнуться окончательно. Очень легко потерять себя, находясь на вершине. Как говорят, голова кружится от успеха. Потерять себя, когда ты на самом дне, ничуть не сложнее. Может, даже проще. Возможно, снизу стучат новые трудности. Морально я готов к ним.

Голосовой блокнот

Для меня вещь просто незаменимая. По моему голосу (по телефону не раз меня воспринимали за пьяного) поэтому я и не записываю голосовые сообщения. Мне удобно с распознаванием речи в телефоне. Я нашел два готовых решения. На самом деле их гораздо больше, но мне показалось два более адекватных. Первое решение — это голосовой блокнот российского производства. Решение довольно неплохое. Я даже привык к нему. Написал с ним целую книгу, огромное число постов и статей. В общем, мы с ним дружили.

Пока на одном запрещенном видеохостинге я не увидел рекламу: «Буржуйской разработки. В отличие от нашей. За нее просили деньги». Был бесплатный триал на месяц. Я решил попробовать, и мне понравилось. Работа программистом разучила меня пользоваться пиратским программным обеспечением. Я даже не пробовал искать взломанную версию. Я честно откатал триал. Когда мне пришло уведомление, что пора платить, я полез покупать подписку. Но ведь я русский. Значит, я угроза всему цивилизованному миру. Сны вижу только о том, как я на коляске тараню танк. Коляска моя самая угрожающая, какая только может быть. Танк, естественно, самый демократичный и либеральный. Западный мир распознал угрозу, поэтому подписку купить я не смог.

Я увидел два варианта. Попытаться найти взломанную версию или самому взломать. Второй вариант — написать свое. Чтобы вы верно понимали: при отсутствии голосового блокнота даже поисковый запрос мне приходится набивать экранной клавиатурой. При этом работает только одна рука. Она, кстати, тоже работает довольно плохо. Попросите друга держать вам руку, а сами печатайте на экранной клавиатуре, чтобы понять, что это очень непросто. Это все к тому, что выбор у меня был примерно как из двух стульев. То есть оба варианта плохие. Я решил написать свое.

Здесь есть иллюстрация

Зарегистрируйтесь или войдите, чтобы увидеть ее и другие изображения

Мне понравилось то, что я сделал. У буржуев, конечно, красивее. Но ни черта не стал я гоняться за красотой. Консольное оформление мне нравится гораздо больше. Огромное число людей из моего ближайшего окружения посмотрит на это и не поверит, что это написал я сам. Хотя я могу создавать модные интерфейсы. Но быть собой во всем — это мое слабоумие и отвага. В конце концов, искусственный интеллект может сделать «модно». Так может сделать только живой разработчик. Ну или чуть живой…

Немного о программировании и ООП

Давайте перейдем к реализации. Я писал это все на C#. Я закончил инфоцыганские курсы разработчика C++. Даже есть диплом. Ну, там не учат. Зато это заставило учиться по открытым источникам. Когда ты немного понимаешь C++, достаточно пары книг, чтобы начать понимать C#. У них один общий корень. Как Delphi вырос из Pascal. Очень много разных тонкостей, их можно узнать на практике. ООП в Африке тоже ООП.

Маленькое уточнение. Программирование — это сложно. Со мной будет спорить каждый утюг. Но к реальному бизнес-процессу со знаниями инфоцыганских курсов не пустят. Сейчас все стало гораздо проще. Нейросеть может писать код сама. Ну, задание себе сама она давать не может. Знание сотен технологий, построение архитектуры приложения — это все, пожалуйста, сами. Чтобы все это знать, надо читать много книг. Надо много общаться с теми, кто в это погружен. Надо много писать. Придумывать себе задачи и самому же их решать. Во всяком случае, я так учился. Всякие курсы, как и оплата услуг мошенников, — с моей стороны это просто жест отчаяния. То есть у меня постепенно отказывали ноги. От меня уходила жена. На моей работе попросили меня. Тогда я был немножко расстроен.

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

Пример из реальной жизни. Я работаю программистом. Изначальный проект, над которым работал наш коллектив. Был написан на делфи. Не забывайте, я родился в прошлом тысячелетии. Мы развиваемся и решили переписать его на С++. Мы учили синтаксис и основные конструкции языка. Два месяца. После этого переписывали пятьсот тысяч строк кода. Не сразу, но у нас все заработало.

Со мной можно поспорить. Например, есть питон, и он тоже язык высокого уровня. Только это язык скриптовый. Он не компилируемый. Это примерно как русский и олбанский. Как угодно можно говорить и писать, может быть, и поймут. Это не спор о том, какой язык лучше. Питон хорош для своих целей. Откровенно я с ним не умею. Судя потому, что есть книги Питон для детей. Я еще и тупой.

Вернемся к нашей теме. Что такое инкапсуляция? Каждый объект языка представляет собой капсулу. Наверное, всегда это класс. Каждый класс содержит в себе описание каждого метода. Каждого свойства.

Здесь есть иллюстрация

Зарегистрируйтесь или войдите, чтобы увидеть ее и другие изображения

На картинке машина — Это объект. Я создаю объект. Этот объект обладает всеми свойствами и методами этого класса. Давайте посмотрим, как это реализовано в разных языках.

Инкапсуляция в Delphi — это один из ключевых принципов объектно-ориентированного программирования (ООП), который заключается в объединении данных и методов, работающих с этими данными, в единую сущность (класс) и сокрытии деталей его реализации от внешнего мира. Вместо прямого доступа к полям класса извне, взаимодействие с объектом происходит через специально определённые методы, что позволяет контролировать, как данные могут быть изменены и использованы.

Как инкапсуляция реализуется в Delphi

В Delphi инкапсуляция достигается с помощью модификаторов доступа — ключевых слов, которые управляют видимостью полей и методов класса. Вот основные из них:

Private (закрытый). Поля и методы с этим модификатором доступны только внутри самого класса. Это самый строгий уровень доступа, который полностью ограничивает использование данных объекта извне.

Protected (защищённый). Элементы с этим модификатором доступны не только внутри класса, но и в его подклассах (наследниках). Это полезно, когда нужно предоставить доступ к данным базового класса для его специализированных потомков, но не для всего остального кода.

Public (общедоступный). Элементы с этим модификатором доступны из любого места программы, включая другие модули, классы и объекты. Это интерфейс, через который объект предоставляется внешнему коду.

Published (опубликованный). Похож на public, но его ключевая особенность в том, что элементы, помеченные как published, становятся доступны для механизмов среды разработки (например, для инспектора объектов в Delphi IDE) и для сериализации данных.

Internal (внутренний). Указывает, что члены класса доступны только в пределах одного модуля (unit).

Геттеры и сеттеры

Важным инструментом инкапсуляции являются геттеры (методы для чтения данных) и сеттеры (методы для изменения данных). Они обеспечивают безопасный доступ к скрытым полям класса, позволяя добавлять дополнительную логику при их изменении. Например, сеттер может проверять корректность данных перед их сохранением.

Пример:

type

TPerson = class

private

FName: string;

FAge: Integer;

public

procedure SetName (const AName: string);

function GetName: string;

procedure SetAge (AAge: Integer);

function GetAge: Integer;

end;

procedure TPerson.SetName (const AName: string);

begin

FName:= AName;

end;

function TPerson.GetName: string;

begin

Result:= FName;

end;

// Аналогично для SetAge и GetAge

В этом примере поля FName и FAge скрыты, а доступ к ним осуществляется через публичные методы.

Инкапсуляция в C# — это один из ключевых принципов объектно-ориентированного программирования (ООП). Я бы описал её так: вы «заключаете» данные и код, который с ними работает, в единую оболочку (обычно это класс), а затем контролируете, как извне к этим данным можно получить доступ или их изменить. Главная цель — скрыть внутреннюю реализацию от пользователя, предоставив чёткий и безопасный интерфейс.

Как это работает

В основе инкапсуляции в C# лежат несколько приёмов:

Объединение данных и методов. Данные (поля класса) и методы, которые с ними работают, объединяются в единое целое — класс.

Сокрытие деталей. Внутренние поля класса делают приватными (с помощью модификатора private). Это значит: напрямую обратиться к ним из другого кода (вне класса) нельзя.

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

Пример

Вот как это выглядит в коде:

class BankAccount

{

private double balance; // Приватное поле — скрыто от внешнего кода

// Геттер: позволяет получить текущий баланс

public double GetBalance ()

{

return balance;

}

// Сеттер: позволяет изменить баланс с проверкой

public void Deposit (double amount)

{

if (amount> 0)

{

balance += amount;

}

}

}

В этом примере:

Поле balance объявлено как private — его нельзя изменить напрямую из другого класса.

Доступ к балансу предоставляется только через методы GetBalance () и Deposit ().

Внутри Deposit () есть проверка: деньги можно положить, только если сумма положительная.

Какие модификаторы доступа есть в C#

public — элемент доступен из любого места программы.

private — элемент доступен только внутри того класса, где он объявлен.

protected — элемент доступен внутри класса и его наследников.

internal — элемент доступен только в пределах той же сборки.

Зачем это нужно?

Инкапсуляция даёт несколько преимуществ:

Безопасность данных. Защита от некорректного или несанкционированного изменения.

Упрощение использования. Пользователю класса достаточно знать, какие методы вызывать, а не разбираться в деталях реализации.

Гибкость при изменениях. Если позже вы решите поменять внутреннюю логику (например, добавить логирование при обновлении баланса), код, который использует класс, не придётся менять — изменится только сам класс.

Инкапсуляция в C++ — это механизм, который объединяет данные (поля класса) и методы (функции, работающие с этими данными) в единый модуль (класс), а также ограничивает прямой доступ к внутренним компонентам объекта. Цель — скрыть детали реализации от пользователя, предоставив контролируемый и безопасный интерфейс для работы с объектом.

Как это работает

Для управления доступом в C++ используются спецификаторы доступа:

public: члены класса (поля и методы) доступны из любой части программы.

private: члены класса доступны только внутри самого класса.

protected: члены класса доступны внутри самого класса и из производных (наследниковых) классов.

По умолчанию члены класса являются приватными, поэтому ключевое слово private часто можно опускать.

Пример

#include <iostream>

#include <string>

class Person {

private:

std::string name; // Приватное поле — скрыто от внешнего кода

int age; // Приватное поле

public:

// Публичный метод (интерфейс) для работы с данными

void setName (const std::string& newName) {

if (!newName. empty ()) {// Простая валидация

name = newName;

}

}

int getAge () const {

return age;

}

void printInfo () const {

std::cout <<«Name: " <<name <<», Age: " <<age <<std::endl;

}

};

int main () {

Person person;

person.setName («Иван»); // Через метод — валидация выполняется

// person.age = 30; // Ошибка компиляции: прямое обращение к приватному полю

std::cout <<«Age: " <<person.getAge () <<std::endl;

return 0;

}

В этом примере поле age объявлено как private, и его значение можно изменить только через метод setAge (), где реализована логика проверки.

Три совершенно разных языка. При этом два из них из одной ветки, у них даже синтаксис похожий. У первого ключевое отличие: Begin, End — в начале и конце блока кода. Разумеется, я говорю только о синтаксисе. Есть целая куча тонкостей. Обычно они очень простые для понимания во время практики. Давайте посмотрим на наследование.

Наследование в Delphi — это механизм, который позволяет создавать новый класс (наследник) на основе уже существующего (базового, предка), заимствуя его свойства и функциональность. Это помогает избежать дублирования кода и строить иерархию классов с общей, но специализированной логикой.

Как это работает

Чтобы создать наследник, нужно в объявлении класса после ключевого слова class указать класс-предок в скобках.

Пример:

type

TAncestor = class

// члены класса

end;

TDescendant = class (TAncestor)

// члены класса

end;

Все классы в Delphi по умолчанию являются потомками класса TObject. Если при объявлении наследника класс-предок не указан, подразумевается TObject.

Ключевые особенности

Одиночное наследование. Каждый класс может иметь только одного непосредственного предка, но может порождать множество потомков. Это формирует иерархию, где все классы связаны отношением «является предком/потомком».

Наследование полей, свойств и методов. Класс-наследник автоматически получает все поля, свойства и методы базового класса. При этом в наследнике можно определять новые поля с теми же именами, что и у предков, но это требует осторожности из-за области видимости.

Перекрытие методов (Overriding). Методы базового класса можно переопределить в наследнике с помощью директивы override, чтобы изменить их реализацию. При этом сигнатура метода (имя, параметры, тип возвращаемого значения) должна полностью совпадать.

Ключевое слово inherited. Если в переопределённом методе нужно вызвать реализацию из базового класса, используется inherited. Для процедур это выглядит как inherited ProcedureName; для функций — Result:= inherited FunctionName.

Конструкторы и деструкторы. В конструкторе наследника сначала вызывается конструктор предка, а затем инициализируются поля самого наследника. В деструкторе порядок обратный: сначала уничтожаются поля наследника, а затем вызывается унаследованный деструктор.

Виртуальные и динамические методы

Для реализации полиморфизма в Delphi используются виртуальные (virtual) и динамические (dynamic) методы. Виртуальные методы требуют больше памяти (из-за таблицы виртуальных методов — VMT), но обеспечивают позднее связывание (полиморфизм). Динамические методы используют таблицу динамических методов (DMT) и немного экономнее по памяти.

Важные нюансы:

Нельзя виртуальный метод предка заменить статическим.

При переопределении виртуального метода в наследнике нужно использовать директиву override.

Множественное наследование

Delphi не поддерживает множественное наследование классов. Однако частично эту возможность можно эмулировать с помощью классов-помощников (ClassHelpers).

Для реализации множественного наследования интерфейсов используется другой подход: класс может одновременно реализовывать несколько интерфейсов.

Пример переопределения метода

type

TAncestor = class

procedure BaseMethod;

end;

TDescendant = class (TAncestor)

procedure BaseMethod; override;

end;

procedure TAncestor.BaseMethod;

begin

Writeln («Метод базового класса»);

end;

procedure TDescendant.BaseMethod;

begin

Writeln («Метод наследника перед вызову»);

inherited BaseMethod; // Вызов метода базового класса

Writeln («Метод наследника после вызова»);

end;

В этом примере метод BaseMethod в наследнике TDescendant выполняется перед, во время и после вызова метода базового класса BaseMethod. Ключевое слово inherited позволяет корректно вызвать реализацию предка.

Наследование в C# — это механизм, который позволяет создавать новый класс (производный или наследник) на основе уже существующего (базового или родителя), перенимая его свойства и функциональность. Это один из ключевых принципов объектно-ориентированного программирования, который помогает избежать дублирования кода и создать логичную иерархию классов.

Как это работает

При наследовании производный класс автоматически получает все публичные и защищённые члены базового класса (поля, методы, свойства). При этом производный класс может добавлять собственные члены, а также переопределять (перекрывать) унаследованные методы.

Пример:

using System;

// Базовый класс

public class Animal

{

public string Name {get; set;} // Поле Name

public void Eat () // Метод Eat

{

Console. WriteLine ($» {Name} is eating.»);

}

}

// Производный класс

public class Dog: Animal // Ключевое слово: указывает наследование от класса Animal

{

public void Bark () // Метод Bark, специфичный для собаки

{

Console. WriteLine ($» {Name} says woof!»);

}

}

// Использование

Dog myDog = new Dog ();

myDog.Name = «Rex»; // Доступ к унаследованному полю

myDog. Eat (); // Доступ к унаследованному методу

myDog.Bark (); // Доступ к собственному методу

Синтаксис и спецификаторы доступа

Для указания отношения наследования используется двоеточие и спецификатор доступа после имени производного класса:

// Открытое наследование (по умолчанию)

public class Employee: Person // Employee наследуется от Person

Спецификатор доступа определяет, какие члены базового класса будут доступны в производном:

* **`public`** — члены остаются публичными.

* **`protected`** — публичные члены базового класса становятся защищёнными в производном.

* **`private`** — и публичные, и защищённые члены становятся приватными в производном классе.

Отсутствие спецификатора доступа в объявлении класса эквивалентно `private`.

## Особенности наследования в C#

* **Одиночное наследование классов.** Класс может наследоваться только от одного прямого базового класса.

* **Множественное наследование интерфейсов.** Класс может реализовывать несколько интерфейсов, что позволяет достичь похожей гибкости.

* **Наследование от `object`.** Все классы в C# неявно наследуются от базового класса `Object`, даже если это не указано явно.

* **Абстрактные и sealed классы.** Если базовый класс объявлен как `abstract`, его нельзя инстанциировать напрямую — его могут наследовать только другие классы. Класс, объявленный как `sealed`, не может быть унаследован.

* **Конструкторы и деструкторы.** Конструкторы базового класса вызываются при создании объекта производного класса, а деструкторы — при его уничтожении в обратном порядке. Однако сами конструкторы и деструкторы **не наследуются**.

## Важные нюансы

* **Конструкторы не наследуются.** При создании объекта производного класса вызывается конструктор базового класса, если он доступен. Если базовый класс не имеет подходящего конструктора, компилятор выдаст ошибку.

* **Переопределение (overriding).** Производный класс может переопределить (изменить реализацию) виртуальный или абстрактный метод базового класса. Для этого используется ключевое слово `override`.

* **Композиция vs наследование.** Наследование моделирует отношение «является», тогда как композиция (включение объекта одного класса в другой) моделирует отношение «имеет». Композиция часто предпочтительнее, когда нужно добавить функциональность, не встраивая объект в иерархию наследования.

## Когда использовать наследование

Наследование целесообразно, когда между классами существует отношение «является подтипом чего-то» (например, `Dog` является `Animal`). Оно помогает избежать дублирования кода и создать понятную иерархию. Однако стоит помнить о его ограничениях и иногда рассматривать альтернативные подходы, например, композицию.

Наследование в C++ — это механизм, который позволяет новому классу (производному или подклассу) автоматически получать (наследовать) поля и методы уже существующего класса (базового или суперкласса). Это основа повторного использования кода и организации иерархий классов.

Синтаксис

Для создания отношения наследования после имени производного класса ставится двоеточие, затем идёт спецификатор доступа и имя базового класса:

class Base {/* члены базового класса */};

class Derived: access_specifier Base {

// члены производного класса

};

Где access_specifier — один из спецификаторов доступа: public, protected или private.

Типы наследования

Спецификатор доступа определяет, как будут доступны унаследованные члены:

Спецификатор Доступ в производном классе

public Публичные (public) и защищённые (protected) члены базового класса остаются публичными и защищёнными.

protected И public, и protected члены базового класса становятся protected в производном.

private И public, и protected члены базового класса становятся private в производном.

Важно: При любом типе наследования private-члены базового класса недоступны в производном.

Особенности работы

Приватные члены. Не наследуются, а просто недоступны в производном классе.

Конструкторы и деструкторы. Не наследуются, но вызываются при создании и уничтожении объектов производного класса. Конструкторы вызываются в порядке от базового к производному, деструкторы — в обратном порядке.

Указатель на базовый класс. Производный класс может быть неявно преобразован в указатель на базовый. Это позволяет использовать объекты производного класса там, где ожидается объект базового типа.

Виртуальные функции. Для реализации полиморфизма методы в базовом классе должны быть объявлены как virtual. Ключевое слово override (C++11) позволяет явно указать, что метод переопределяется.

Множественное наследование

C++ поддерживает множественное наследование, когда класс наследуется от нескольких базовых классов. Однако это может привести к проблеме «ромба» (diamond problem): если два класса наследуются от общего базового, а третий — от обоих первых, возникает неоднозначность при обращении к членам общего базового класса.

Для решения этой проблемы используется виртуальное наследование (virtual public), которое гарантирует, что в иерархии будет только один экземпляр базового класса.

Переопределение методов

Производный класс может заменить унаследованный метод, объявив в себе метод с той же сигнатурой. Доступ к оригинальной реализации можно получить через оператор разрешения области видимости (Base::method ()).

Рекомендации по использованию

Предпочитайте public-наследование для моделирования отношения «является» (например, Dog является Animal).

Используйте private-наследование для повторного использования реализации в духе «реализовано через».

Избегайте множественного наследования, если можно смоделировать связь иначе, через композицию.

Будьте осторожны с виртуальными деструкторами в базовом классе при наличии виртуальных функций, чтобы избежать неопределённого поведения при удалении объектов через указатель на базовый класс.

Пример

#include <iostream>

#include <string>

using namespace std;

// Базовый класс

class Animal {

public:

string name;

void eat () {

cout <<name <<" is eating.» <<endl;

}

};

// Производный класс

class Dog: public Animal {

public:

void bark () {

cout <<name <<" says woof!» <<endl;

}

};

int main () {

Dog d;

d.name = «Rex»;

d. eat (); // Используем унаследованный метод

d.bark (); // Вызов собственного метода

return 0;

}

В этом примере класс Dog автоматически получает доступ к полю name и методу eat () из Animal, а также добавляет свой метод bark ().

А теперь давайте посмотрим на реализацию полиморфизма. На мой взгляд, это самое сложное.

Полиморфизм в Delphi — это способность объектов разных классов обрабатывать одно и то же сообщение (вызов метода) по-своему. Проще говоря, это возможность писать обобщённый код, который работает с объектами через их базовый класс, а конкретный тип объекта определяется во время выполнения программы.

Как это работает

В основе полиморфизма в Delphi лежат наследование и виртуальные методы.

Наследование. Классы образуют иерархию. Класс-потомок наследует интерфейс (сигнатуры методов) своего родителя и может переопределить их реализацию.

Виртуальные методы. Чтобы метод мог быть переопределён в потомках, его объявляют в базовом классе с ключевым словом virtual. При вызове такого метода во время выполнения программы будет исполняться та реализация, которая находится в том конкретном классе, чей экземпляр передан в метод.

type

TShape = class (TObject)

public

procedure Draw; virtual; // Виртуальный метод

end;

TCircle = class (TShape)

public

procedure Draw; override; // Переопределение метода

end;

TSquare = class (TShape)

public

procedure Draw; override; // Переопределение метода

end;

//…

Здесь метод Draw объявлен в базовом классе TShape как virtual. В потомках TCircle и TSquare он переопределяется с помощью ключевого слова override. При вызове Draw для объекта типа TShape, но ссылающегося на объект TCircle или TSquare, будет выполнена соответствующая реализация Draw из этого объекта.

Ключевые нюансы

Ключевое слово override. Если в потомке объявить метод с тем же именем, что и виртуальный метод родителя, но без override, то произойдёт сокрытие метода (статическое связывание), а не его переопределение. В этом случае вызов через базовый класс будет работать как обычно, а метод потомка будет недоступен через общий интерфейс.

Динамические методы. В Delphi есть также динамические методы (с директивой dynamic). По сути, они работают аналогично виртуальным, но оптимизированы для экономии памяти, а не скорости. В современных проектах обычно рекомендуют использовать виртуальные методы.

Абстрактные методы. Если в базовом классе объявить метод как virtual и abstract, то любой класс-наследник обязан предоставить свою реализацию этого метода.

Перегрузка методов (overload). Важно не путать полиморфизм с перегрузкой. Перегрузка — это когда в одном классе объявляется несколько методов с одним именем, но разными сигнатурами (количеством или типами параметров). Это разные механизмы: полиморфизм связан с поздним связыванием (выбор реализации на этапе выполнения), а перегрузка — со статическим выбором на этапе компиляции.

Пример из практики

Классический пример — иерархия потоков данных. Базовый класс TStream определяет общий интерфейс для записи данных. Его потомки TFileStream (запись в файл) и TMemoryStream (запись в память) предоставляют свою реализацию этого интерфейса. Код, который работает с потоками (например, процедура SaveToStream), оперирует ссылками на базовый класс TStream. Ему неважно, с каким именно потоком он имеет дело — он просто вызывает метод WriteBuffer, и полиморфизм гарантирует, что будет вызвана правильная, конкретная реализация для переданного объекта.

Преимущества

Гибкость и расширяемость. Добавляя новые типы объектов (новые классы-наследники), не нужно менять уже написанный код, работающий с абстракцией.

Упрощение логики. Код становится более читаемым и понятным, так как отпадает необходимость в длинных цепочках условных операторов (if-else) для выбора поведения.

Я бы сказал так: полиморфизм в C# — это способность объектов разных классов реагировать на один и тот же метод по-своему. Проще говоря: вы вызываете метод у переменной базового типа, но на самом деле внутри работает конкретная реализация из производного класса. Это один из ключевых принципов объектно-ориентированного программирования.

В C# для этого есть несколько механизмов — я подобрала основные.

Переопределение методов (Overriding). Это основа полиморфизма подтипов. В базовом классе объявляется метод с определённой сигнатурой, а в производном классе этот метод переопределяется (то есть создаётся своя реализация). Ключевое слово override (в C#7.2 и новее) помогает компилятору проверить, действительно ли вы переопределяете виртуальный метод, и выдаёт ошибку, если есть опечатка в сигнатуре. Пример:

using System;

class Animal {

public void Speak () {Console. WriteLine («Some sound»);}

}

class Dog: Animal {

public new void Speak () {Console. WriteLine («Bark»);} // Новая реализация

}

class Cat: Animal {

public new void Speak () {Console. WriteLine («Meow»);} // Новая реализация

}

class Program {

static void Main () {

Animal myDog = new Dog ();

Animal myCat = new Cat ();

myDog. Speak (); // Выведет «Bark»

myCat. Speak (); // Выведет «Meow»

}

}

Полиморфизм через интерфейсы. Если у разных классов нет общего базового класса, но они реализуют один и тот же интерфейс, они тоже могут вести себя полиморфно. Например, интерфейс IDrawable с методом Draw (), а классы Circle и Rectangle его реализуют — тогда метод, который принимает IDrawable, работает с объектами этих классов единообразно.

Абстрактные классы и методы. Абстрактный класс содержит абстрактные методы (без реализации), которые должны быть переопределены в наследниках. Это тоже форма полиморфизма: через ссылку на базовый класс мы вызываем реализацию именно того метода, который создан в конкретном наследнике.

Полиморфизм времени компиляции (Ad-hoc). Это не то же самое, что переопределение в иерархии классов. Здесь речь о перегрузке методов — когда у вас несколько методов с одним именем, но разными параметрами (сигнатурами). Компилятор выбирает подходящую версию на этапе компиляции, а не во время выполнения программы. Пример:

int Add (int a, int b) {return a + b;}

double Add (double a, double b) {return a + b;}

Компилятор сам подберёт нужную версию Add в зависимости от типов переданных аргументов.

Параметрический полиморфизм. В C# для этого используются generics (обобщения). Вы пишете код, который работает с разными типами данных, и компилятор генерирует для него конкретную версию под каждый используемый тип.

Какой смысл в этом всём? Полиморфизм делает код гибче и удобнее в поддержке. Он позволяет писать код, который работает с общим интерфейсом, не привязываясь к конкретным реализациям. Это упрощает расширение функциональности — вы можете добавить новый класс, реализующий нужный контракт, и система будет работать с ним, не меняя существующий код, который использует полиморфизм.

Полиморфизм в C++ — это действительно мощная штука: она позволяет писать гибкий код, где один и тот же интерфейс работает с разными типами данных, и под выбранный вариант подводится конкретная реализация. Я бы сказал так: это способность функции или объекта вести себя по-разному в зависимости от того, с объектом какого типа мы имеем дело.

В C++ полиморфизм реализуется несколькими способами, и я бы разделил их на две большие группы по времени разрешения выбора.

1. Полиморфизм времени компиляции (статический)

Здесь решение, какую функцию или шаблон вызвать, принимается на этапе компиляции.

Перегрузка функций. Вы объявляете несколько функций с одним именем, но разным набором параметров (по типу, количеству). Компилятор выбирает подходящую версию на основе типов переданных аргументов.

int add (int a, int b) {return a + b;}

double add (double a, double b) {return a + b;}

```

Шаблоны. Вы пишете обобщённый код (функцию или класс), который работает с разными типами. Компилятор на основе конкретных типов аргументов генерирует для вас специализированные версии.

template <typename T> T max (T a, T b) {return (a> b)? a: b;}

```

2. Полиморфизм времени выполнения (динамический)

Здесь решение, какую реализацию вызвать, принимается во время работы программы. Это как раз то, ради чего в C++ активно используют наследование и виртуальные функции.

Ключевое слово virtual в базовом классе объявляет функцию виртуальной. Это даёт производным классам возможность переопределить её. Когда вы вызываете такую функцию через указатель или ссылку на базовый класс, программа во время выполнения смотрит на фактический тип объекта, на который этот указатель или ссылка указывает, и выбирает соответствующую реализацию.

Как это работает «под капотом»? Для каждого класса с виртуальными функциями компилятор создаёт специальную таблицу (vtable — виртуальная таблица). В каждом объекте такого класса есть скрытый указатель (vptr), который указывает на эту таблицу. При вызове виртуальной функции программа через vptr находит в таблице адрес нужной функции и вызывает её.

Пример:

#include <iostream>

class Animal {

public:

virtual void makeSound () {std::cout <<«Животное издаёт звук» <<std::endl;}

};

class Dog: public Animal {

public:

void makeSound () override {std::cout <<«Собака лает: Гав!» <<std::endl;}

};

class Cat: public Animal {

public:

void makeSound () override {std::cout <<«Кошка мяукает: Мяу!» <<std::endl;}

};

int main () {

Dog dog;

Cat cat;

Animal* animalPtr = &dog;

animalPtr-> makeSound (); // Выведет «Собака лает: Гав!»

animalPtr = &cat;

animalPtr-> makeSound (); // Выведет «Кошка мяукает: Мяу!»

return 0

}

Важное правило: если в базовом классе есть хотя бы одна виртуальная функция, и вы планируете удалять объекты через указатель на базовый класс (особенно в динамической памяти), деструктор этого базового класса обязательно должен быть виртуальным. Иначе при удалении может не сработать destructor производного класса, и возникнут утечки памяти или неопределённое поведение.

Ещё есть чисто виртуальные функции (объявляются с = 0). Класс с такой функцией становится абстрактным — от него нельзя создать объект, но его можно наследовать и реализовывать.

Зачем вообще это нужно?

Гибкость. Код работает с разными типами через единый интерфейс.

Расширяемость. Вы можете добавлять новые производные классы, не меняя уже написанный код, который их использует.

Упрощение. Часто позволяет написать более лаконичный и понятный код.

Небольшой совет

Иногда в коде можно встретить и то, и другое: например, шаблонную функцию для какой-то общей задачи и виртуальную функцию в иерархии для реализации разного поведения. Выбор зависит от задачи: статический полиморфизм часто даёт лучшую производительность, а динамический — большую гибкость.

Очевидные общие ключевые слова. Очевидна связь, которая получается через принципы объектно-ориентированного программирования. Это позволяет наплевать на такой нюанс, как выбор языка программирования. Благодаря нейросетям мы все теперь сеньоры. Однако нас оценивать будут, потому как мы сделали структуру. Если код программы представляет собой простыню, или, что еще хуже, рулон туалетной бумаги, — поддерживать это невозможно. Можно сказать, что дерево и деление на классы — это все нужно нам, тупым людишкам. Мощная нейросеть сама построит простыню. Она будет работать, и этого хватит. Но если даже вы не понимаете, что делает код… Нейросети подвержены галлюцинациям. Код от нее может легко не работать. Придется потратить не один час на поиск ошибки. Если код представляет собой простыню, скорее всего, найти ошибку не получится.

Ну, может, все это уже не актуально? Нейросети пишут не только отличный код, но и строят грамотную архитектуру. Именно про это данная книга. Сейчас я рассмотрел самый простой вариант, когда простенькая программа может решить ваши задачи. Программу напишет нейросеть. Вам надо просто поставить задачу и оценить результат.

Сейчас я приведу код всего своего блокнота. В коде есть комментарий. Мое мнение — этого вполне достаточно. Только что я говорил, насколько это плохо — простыня. Это относительно простая программа. Я посчитал, что ей не надо дерево. К тому же я думал, что про эту программу будет только одна статья. Но я ошибся.

Исходный код голосового блокнота

/*

* =

* VK VoiceTyper — Голосовой ввод текста через VK Cloud Voice API

* =

*

* Функционал:

* — Запись аудио с микрофона по нажатию правой клавиши Ctrl

* — Распознавание речи через VK Cloud Voice (SpeechKit)

* — Автоматическая вставка текста в активное окно

* — Окно консоли всегда поверх других окон

*

* Требования:

* -.NET 8.0 Windows

* — Сервисный токен VK Cloud (получить в настройках проекта)

* — Права администратора (для глобального перехвата клавиш)

*

* Автор: [Ваше имя]

* Версия: 1.0

* =

*/

using System;

using System. Diagnostics;

using System.IO;

using System.Net;

using System.Net.Http;

using System.Net.Http. Headers;

using System.Net.Security;

using System.Runtime.InteropServices;

using System.Security.Authentication;

using System.Threading;

using System.Threading. Tasks;

using System.Windows.Forms;

using NAudio. Wave;

using Newtonsoft.Json.Linq;

namespace VkVoiceTyper

{

/// <summary>

/// Основной класс программы

/// </summary>

class Program

{
// =
// НАСТРОЙКИ ПРИЛОЖЕНИЯ

// =
/// <summary>

/// СЕРВИСНЫЙ ТОКЕН VK CLOUD

/// Получите его в: VK Cloud Console → Ваш проект → Настройки → Сервисные токены

/// Роль токена: VoiceKit Admin или Member с доступом к VoiceKit

/// ВАЖНО: Не публикуйте токен в открытом доступе!

/// </summary>

private const string ServiceToken = «ВАШ_СЕРВИСНЫЙ_ТОКЕН_VC_…»;

/// <summary>

/// Имя временного файла для записи аудио

/// Файл автоматически удаляется после обработки

/// </summary>

private const string OutputFileName = «voice_temp. wav»;

// =
// ГЛОБАЛЬНЫЕ ПЕРЕМЕННЫЕ ДЛЯ ЗАПИСИ АУДИО

// =

/// <summary>

/// Устройство захвата аудио (микрофон)

///? означает nullable — может быть null когда не используется

/// </summary>

private static WaveInEvent? waveIn;

/// <summary>

/// Записывающий WAV файл

/// Создает файл в формате WAV (16kHz, 16bit, Mono)

/// </summary>

private static WaveFileWriter? writer;

/// <summary>

/// Флаг: идет ли запись в данный момент

/// Защищает от повторного запуска записи

/// </summary>

private static bool isRecording = false;

/// <summary>

/// Флаг: идет ли обработка (распознавание) в данный момент

/// Блокирует новую запись пока не завершится текущая обработка

/// </summary>

private static bool isProcessing = false;

// =

// КОНСТАНТЫ ДЛЯ WINDOWS HOOK API (Перехват клавиш)

// =

/// <summary>

/// Тип хука: низкого уровня для клавиатуры

/// Позволяет перехватывать нажатия клавиш глобально (во всех окнах)

/// </summary>

private const int WH_KEYBOARD_LL = 13;

/// <summary>

/// Сообщение Windows: клавиша нажата

/// </summary>

private const int WM_KEYDOWN = 0x0100;

/// <summary>

/// Сообщение Windows: клавиша отпущена

/// </summary>

private const int WM_KEYUP = 0x0101;

/// <summary>

/// Виртуальный код клавиши: ПРАВЫЙ CTRL

/// 0xA3 = 163 в десятичной системе

/// </summary>

private const int VK_RCONTROL = 0xA3;

/// <summary>

/// Виртуальный код клавиши: ESCAPE (выход из программы)

/// </summary>

private const int VK_ESCAPE = 0x1B;

// =

/// <summary>

/// КОНСТАНТЫ ДЛЯ ALWAYS ON TOP (Окно поверх всех)

/// </summary>

// =

/// <summary>

/// Поместить окно поверх всех остальных

/// </summary>

private const int HWND_TOPMOST = -1;

/// <summary>

/// Флаг: не изменять размер окна

/// </summary>

private const int SWP_NOSIZE = 0x0001;

/// <summary>

/// Флаг: не изменять позицию окна

/// </summary>

private const int SWP_NOMOVE = 0x0002;

// =
// ПЕРЕМЕННЫЕ ДЛЯ HOOK API

// =

/// <summary>

/// Делегат для функции обратного вызова хука

/// Вызывается при каждом нажатии клавиши в системе

/// </summary>

private static LowLevelKeyboardProc proc = HookCallback;

/// <summary>

/// Идентификатор установленного хука

/// Нужен для последующего удаления хука

/// </summary>

private static IntPtr hookID = IntPtr. Zero;

// =

/// <summary>

/// HTTP КЛИЕНТ ДЛЯ ЗАПРОСОВ К API

/// </summary>

/// <remarks>

/// Настраивается с правильными параметрами TLS для безопасного соединения

/// Создается один раз и переиспользуется (best practice для HttpClient)

/// </remarks>

// =

private static readonly HttpClient httpClient = CreateSecureHttpClient ();

// =
// ТОЧКА ВХОДА В ПРИЛОЖЕНИЕ

// =

/// <summary>

/// Главная функция программы

/// [STAThread] обязателен для работы Clipboard и Windows Forms

/// </summary>

/// <param name=«args»> Аргументы командной строки (не используются) </param>

[STAThread]

static void Main (string [] args)

{
// — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — // 1. НАСТРОЙКА TLS ПРОТОКОЛОВ

// — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — —

// Принудительно включаем TLS 1.2 и 1.3 для безопасного HTTPS соединения

// Без этого может возникнуть ошибка SSL на старых системах

ServicePointManager.SecurityProtocol = SecurityProtocolType.Tls12 | SecurityProtocolType.Tls13;

// — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — // 2. ДЕЛАЕМ ОКНО КОНСОЛИ ПОВЕРХ ВСЕХ ОКОЛ

// — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — SetConsoleWindowTopMost ();

// — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — // 3. ВЫВОД ПРИВЕТСТВЕННОГО СООБЩЕНИЯ

// — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — —

Console. WriteLine (»🎙️ VK VoiceTyper (Always On Top)»);

Console. WriteLine (»👉 Удерживайте ПРАВЫЙ CTRL для записи»);

Console. WriteLine (»🔚 ESC — выход»);

Console. WriteLine (»==\n»);

// — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — // 4. ПРОВЕРКА СЕРВИСНОГО ТОКЕНА

// — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — —

// Если токен не установлен — показываем ошибку и завершаем программу

if (string.IsNullOrEmpty (ServiceToken) || ServiceToken == «ВАШ_СЕРВИСНЫЙ_ТОКЕН_VC_…»)

{

Console. WriteLine (» Ошибка: Вставьте сервисный токен в код!»);

Console. WriteLine (» Получите его в VK Cloud → Настройки проекта → Сервисные токены»);

Console.ReadKey ();

return;

}

Console. WriteLine (» Токен загружен. Сервис готов.\n»);

// — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — // 5. УСТАНОВКА ГЛОБАЛЬНОГО ХУКА КЛАВИАТУРЫ

// — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — —

// Хук позволяет перехватывать нажатия клавиш даже когда приложение не активно

hookID = SetHook (proc);

// — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — // 6. ЗАПУСК ЦИКЛА ОБРАБОТКИ СООБЩЕНИЙ WINDOWS

// — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — —

// Необходим для работы хука и Windows Forms функций

Application.Run ();

}

// =

/// <summary>

/// СОЗДАНИЕ НАСТРОЕННОГО HTTP КЛИЕНТА

/// </summary>

/// <returns> HttpClient с правильными настройками TLS </returns>

/// <remarks>

/// В. NET 8+ нужно явно указывать версии TLS протоколов

/// Иначе соединение с некоторыми серверами может не установиться

/// </remarks>

// =

private static HttpClient CreateSecureHttpClient ()

{

// Создаем обработчик HTTP запросов с кастомными настройками

var handler = new SocketsHttpHandler

{
// — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — // НАСТРОЙКИ SSL/TLS

// — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — —

SslOptions = new SslClientAuthenticationOptions

{

// Включаем только безопасные протоколы TLS 1.2 и 1.3

// TLS 1.0 и 1.1 устарели и не рекомендуются

EnabledSslProtocols = SslProtocols.Tls12 | SslProtocols.Tls13,

//Callback для проверки сертификата сервера

RemoteCertificateValidationCallback = (sender, certificate, chain, errors) =>

{

#if DEBUG

// В режиме отладки принимаем любые сертификаты (для тестов)

return true;

#else

// В продакшене проверяем валидность сертификата

return errors == SslPolicyErrors.None;

#endif

}}, // — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — // ТАЙМАУТЫ И ЛИМИТЫ

// — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — —

// Таймаут подключения к серверу (10 секунд)

ConnectTimeout = TimeSpan.FromSeconds (10),

// Разрешаем автоматическое перенаправление (301, 302)

AllowAutoRedirect = true,

// Максимум одновременных подключений к одному серверу

MaxConnectionsPerServer = 10

};

// Создаем HttpClient с нашим обработчиком

var client = new HttpClient (handler)

{

// Общий таймаут запроса (15 секунд)

Timeout = TimeSpan.FromSeconds (15)

};

// Добавляем заголовок User-Agent (хорошая практика)

client.DefaultRequestHeaders.UserAgent.ParseAdd («VoiceTyper/1.0»);

return client;

}

// =

/// <summary>

/// ДЕЛАЕТ ОКНО КОНСОЛИ ПОВЕРХ ВСЕХ ОСТАЛЬНЫХ ОКОН

/// </summary>

/// <remarks>

/// Использует Windows API SetWindowPos с флагом HWND_TOPMOST

/// </remarks>

// =

private static void SetConsoleWindowTopMost ()

{

// Получаем handle (идентификатор) окна консоли

IntPtr hWnd = GetConsoleWindow ();

// Если окно найдено — делаем его поверх всех

if (hWnd!= IntPtr. Zero)

{

// SetWindowPos изменяет параметры окна

// Параметры: handle, позиция, X, Y, ширина, высота, флаги

SetWindowPos (hWnd, (IntPtr) HWND_TOPMOST, 0, 0, 0, 0, SWP_NOMOVE | SWP_NOSIZE);

}

}

// =

/// <summary>

/// ОБРАБОТЧИК СОБЫТИЙ КЛАВИАТУРЫ (HOOK CALLBACK)

/// </summary>

/// <param name=«nCode»> Код хука (должен быть> = 0 для обработки) </param>

/// <param name=«wParam»> Тип сообщения (KEYDOWN/KEYUP) </param>

/// <param name=«lParam»> Указатель на структуру KBDLLHOOKSTRUCT с кодом клавиши </param>

/// <returns> Результат передачи следующему хуку в цепочке </returns>

/// <remarks>

/// Вызывается системой при КАЖДОМ нажатии клавиши в Windows

/// Должен выполняться быстро, чтобы не замедлять систему

/// </remarks>

// =

private static IntPtr HookCallback (int nCode, IntPtr wParam, IntPtr lParam)

{

// Проверяем что код валиден (иначе игнорируем)

if (nCode> = 0)

{

// Извлекаем виртуальный код клавиши из lParam

// Marshal.ReadInt32 читает 4 байта по указанному адресу памяти

int vkCode = Marshal.ReadInt32 (lParam);

// — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — —

// ОБРАБОТКА НАЖАТИЯ ПРАВОГО CTRL (НАЧАЛО ЗАПИСИ)

// — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — —

if (wParam == (IntPtr) WM_KEYDOWN && vkCode == VK_RCONTROL)

{

// Запускаем запись только если не идет запись или обработка

if (!isRecording &&!isProcessing)

Task.Run (() => StartRecordingAsync ());

}

// — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — —

// ОБРАБОТКА ОТПУСКАНИЯ ПРАВОГО CTRL (КОНЕЦ ЗАПИСИ)

// — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — —

else if (wParam == (IntPtr) WM_KEYUP && vkCode == VK_RCONTROL)

{

// Останавливаем запись только если она идет

if (isRecording)

Task.Run (() => StopRecordingAndProcessAsync ());

}

// — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — —

// ОБРАБОТКА НАЖАТИЯ ESC (ВЫХОД ИЗ ПРОГРАММЫ)

// — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — —

else if (wParam == (IntPtr) WM_KEYDOWN && vkCode == VK_ESCAPE)

{

// Удаляем хук (освобождаем ресурсы)

UnhookWindowsHookEx (hookID);

// Завершаем цикл сообщений

Application. Exit ();

// Возвращаем 1 чтобы заблокировать дальнейшую обработку ESC

return (IntPtr) 1;

}

}

// Передаем управление следующему хуку в цепочке

// Обязательно для корректной работы системы

return CallNextHookEx (hookID, nCode, wParam, lParam);

}

// =

/// <summary>

/// УСТАНАВЛИВАЕТ ГЛОБАЛЬНЫЙ ХУК КЛАВИАТУРЫ

/// </summary>

/// <param name=«proc»> Функция обратного вызова для обработки нажатий </param>

/// <returns> Идентификатор установленного хука </returns>

// =

private static IntPtr SetHook (LowLevelKeyboardProc proc)

{

// Получаем текущий процесс (для получения handle модуля)

using Process curProcess = Process.GetCurrentProcess ();

// Получаем главный модуль процесса (.exe файл)

using ProcessModule? curModule = curProcess.MainModule;

// Устанавливаем хук

// Параметры: тип хука, callback, handle модуля, ID потока (0 = все потоки)

return SetWindowsHookEx (WH_KEYBOARD_LL, proc,

GetModuleHandle(curModule?.ModuleName), 0);

}

// =

/// <summary>

/// НАЧИНАЕТ ЗАПИСЬ АУДИО С МИКРОФОНА

/// </summary>

/// <remarks>

/// Запускается в отдельном потоке чтобы не блокировать хук

/// Формат: 16kHz, 16-bit, Mono (требование VK Cloud Voice API)

/// </remarks>

// =

private static void StartRecordingAsync ()

{

// Защита от повторного запуска

if (isRecording) return;

isRecording = true;

Console. WriteLine (»🔴 Запись… (отпустите RCtrl)»);

// — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — // НАСТРОЙКА ФОРМАТА АУДИО

// — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — —

// 16000 Гц — частота дискретизации (достаточно для речи)

// 16 бит — глубина звука (стандарт для распознавания)

// 1 канал — моно (стерео не нужно для речи)

var waveFormat = new WaveFormat (16000, 16, 1);

// Создаем устройство захвата с указанным форматом

waveIn = new WaveInEvent {WaveFormat = waveFormat};

// Создаем WAV файл для записи

writer = new WaveFileWriter (OutputFileName, waveIn. WaveFormat);

// — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — // ПОДПИСКА НА СОБЫТИЕ ПОСТУПЛЕНИЯ ДАННЫХ

// — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — —

// Когда микрофон получает звук, вызывается этот обработчик

waveIn. DataAvailable += (s, e) => writer?.Write (e. Buffer, 0, e.BytesRecorded);

// Запускаем запись

waveIn.StartRecording ();

}

// =

/// <summary>

/// ОСТАНАВЛИВАЕТ ЗАПИСЬ И ОТПРАВЛЯЕТ НА РАСПОЗНАВАНИЕ

/// </summary>

/// <remarks>

/// Асинхронный метод — не блокирует основной поток

/// Включает полную обработку: остановка → сохранение → API → вставка

/// </remarks>

// =

private static async Task StopRecordingAndProcessAsync ()

{

// Защита от повторного вызова

if (!isRecording) return;

isRecording = false;

Console. WriteLine (»⏹️ Остановка…");

// — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — // 1. ОСТАНОВКА ЗАПИСИ

// — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — —

waveIn?.StopRecording ();

waveIn?.Dispose ();

writer?.Dispose ();

waveIn = null;

writer = null;

// Ждем завершения записи файла на диск

await Task.Delay (200);

// — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — // 2. ПРОВЕРКА ФАЙЛА

// — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — —

if (!File. Exists (OutputFileName))

{

Console. WriteLine (»⚠️ Файл не создан»);

return;

}

// Проверяем размер файла (меньше 1000 байт = слишком коротко)

long fileSize = new FileInfo (OutputFileName).Length;

if (fileSize <1000)

{

Console. WriteLine ($»⚠️ Слишком короткая запись ({fileSize} байт)»);

return;

}
// — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — // 3. РАСПОЗНАВАНИЕ

// — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — —

isProcessing = true;

Console. WriteLine (»🔄 Распознавание…");

try

{

// Вызываем API распознавания

string? recognizedText = await RecognizeSpeechAsync (ServiceToken, OutputFileName);

// — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — // 4. ОБРАБОТКА РЕЗУЛЬТАТА

// — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — —

if (!string.IsNullOrWhiteSpace (recognizedText))

{

Console. WriteLine ($»✅ Распознано: \» {recognizedText} \«»);

InsertTextInStaThread (recognizedText);

}

else

{

Console. WriteLine (»⚠️ Текст не распознан»);

System.Media.SystemSounds.Beep.Play ();

}}
// — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — // 5. ОБРАБОТКА ОШИБОК

// — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — —

catch (TaskCanceledException)

{

Console. WriteLine (»❌ Таймаут! Сервер не ответил за 15 секунд.»);

System.Media.SystemSounds.Beep.Play ();

}

catch (HttpRequestException httpEx)

{

Console. WriteLine ($»❌ Ошибка HTTP: {httpEx.Message}»);

if (httpEx.InnerException!= null)

Console. WriteLine ($» Детали: {httpEx.InnerException.Message}»);

System.Media.SystemSounds.Beep.Play ();

}

catch (Exception ex)

{

Console. WriteLine ($»❌ Ошибка: {ex.Message}»);

if (ex.InnerException!= null)

Console. WriteLine ($» Детали: {ex.InnerException.Message}»);

System.Media.SystemSounds.Beep.Play ();

}

finally

{

// Сбрасываем флаг обработки

isProcessing = false;

// Удаляем временный файл

if (File. Exists (OutputFileName))

File.Delete (OutputFileName);

}

}

// =

/// <summary>

/// ВСТАВЛЯЕТ ТЕКСТ В АКТИВНОЕ ОКНО

/// </summary>

/// <param name=«text»> Текст для вставки </param>

/// <remarks>

/// Запускается в отдельном STA-потоке (требуется для Clipboard)

/// Копирует текст в буфер обмена + эмулирует Ctrl+V

/// </remarks>

// =

private static void InsertTextInStaThread (string text)

{

// Создаем новый поток для работы с Clipboard

Thread staThread = new Thread (() =>

{
try

{
// — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — // 1. КОПИРОВАНИЕ В БУФЕР ОБМЕНА

// — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — —

Clipboard.SetText (text, TextDataFormat. Text);

Console. WriteLine (»📋 Текст в буфере обмена»);

// Ждем обновления буфера обмена

Thread.Sleep (200);

// — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — // 2. ЭМУЛЯЦИЯ НАЖАТИЯ CTRL+V

// — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — —

bool success = SendCtrlV ();

if (!success)

{

Console. WriteLine (»⚠️ Авто-вставка не сработала. Нажмите Ctrl+V вручную.»);

System.Media.SystemSounds.Asterisk.Play ();

}
else

{

Console. WriteLine (»✅ Текст вставлен автоматически»);

}

}

catch (Exception ex)

{

Console. WriteLine ($»⚠️ Ошибка: {ex.Message}»);

try

{

// Фолбэк: просто копируем в буфер

Clipboard.SetText (text);

Console. WriteLine (»📋 Текст скопирован. Нажмите Ctrl+V вручную.»);

}

catch {}

}

});

// — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — —

// КРИТИЧЕСКИ ВАЖНО: Устанавливаем STA перед запуском потока

// — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — —

// STA (Single Thread Apartment) требуется для работы OLE/COM (Clipboard)

staThread.SetApartmentState(ApartmentState.STA);

staThread.Start ();

staThread.Join (); // Ждем завершения потока

}

// =

/// <summary>

/// ЭМУЛИРУЕТ НАЖАТИЕ CTRL+V ЧЕРЕЗ WINDOWS API

/// </summary>

/// <returns> true если успешно, false если ошибка </returns>

/// <remarks>

/// Использует SendInput вместо SendKeys (более надежно)

/// Отправляет 4 события: Ctrl down, V down, V up, Ctrl up

/// </remarks>

// =

private static bool SendCtrlV ()

{
try

{

// Создаем массив из 4 событий клавиатуры

INPUT [] inputs = new INPUT [4];

// — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — // СОБЫТИЕ 1: НАЖАТИЕ CTRL

// — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — —

inputs [0] = new INPUT

{

type = 1, // INPUT_KEYBOARD

U = new InputUnion

{

ki = new KEYBDINPUT

{

wVk = 0, // 0 = используем скан-код вместо виртуального кода

wScan = (ushort) MapVirtualKey (0x11, 0), // 0x11 = VK_CONTROL

dwFlags = 0, // 0 = нажатие клавиши

time = 0, // 0 = система сама установит время

dwExtraInfo = IntPtr. Zero // Дополнительные данные (не нужны)

}}}; // — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — // СОБЫТИЕ 2: НАЖАТИЕ V

// — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — —

inputs [1] = new INPUT

{

type = 1,

U = new InputUnion

{

ki = new KEYBDINPUT

{

wVk = 0,

wScan = (ushort) MapVirtualKey (0x56, 0), // 0x56 = VK_V

dwFlags = 0,

time = 0,

dwExtraInfo = IntPtr. Zero

}}}; // — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — // СОБЫТИЕ 3: ОТПУСКАНИЕ V

// — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — —

inputs [2] = new INPUT

{

type = 1,

U = new InputUnion

{

ki = new KEYBDINPUT

{

wVk = 0,

wScan = (ushort) MapVirtualKey (0x56, 0),

dwFlags = 0x0002, // KEYUP флаг = отпускание клавиши

time = 0,

dwExtraInfo = IntPtr. Zero

}}}; // — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — // СОБЫТИЕ 4: ОТПУСКАНИЕ CTRL

// — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — —

inputs [3] = new INPUT

{

type = 1,

U = new InputUnion

{

ki = new KEYBDINPUT

{

wVk = 0,

wScan = (ushort) MapVirtualKey (0x11, 0),

dwFlags = 0x0002, // KEYUP флаг

time = 0,

dwExtraInfo = IntPtr. Zero

}

}

};

// Отправляем все 4 события в систему

uint result = SendInput ((uint) inputs. Length, inputs, Marshal.SizeOf <INPUT> ());

// Возвращаем true если все события отправлены успешно

return result == inputs. Length;

}

catch

{

return false;

}

}

// =

/// <summary>

/// ОТПРАВЛЯЕТ АУДИО ФАЙЛ В VK CLOUD VOICE API

/// </summary>

/// <param name=«token»> Сервисный токен авторизации </param>

/// <param name=«filePath»> Путь к WAV файлу </param>

/// <returns> Распознанный текст или null если ошибка </returns>

/// <remarks>

/// Формат запроса: POST с binary body (audio/wave)

/// Ответ: JSON с массивом текстов в result. texts [0].text

/// </remarks>

// =

private static async Task <string?> RecognizeSpeechAsync (string token, string filePath)

{

// Читаем файл в память

byte [] fileBytes = File.ReadAllBytes (filePath);

// Создаем HTTP контент из байтов

var content = new ByteArrayContent (fileBytes);

18+

Книга предназначена
для читателей старше 18 лет

Бесплатный фрагмент закончился.

Купите книгу, чтобы продолжить чтение.