Дисклеймер
Статья не предполагает какой-то принципиально новый взгляд на вещи, кроме как с точки зрения изучения этого материала с «абсолютного нуля».
Материал основан на записях примерно 7-летней давности, когда мой путь в изучении ООП без IT-образования только начинался. В те времена основным языком был MATLAB, много позже я перешел на C#.
Изложение принципов ООП, которое я находил, с примерами в виде каких-то яблок, груш, унаследованных от класса «фрукты» и кучей терминологии (наследование, полиморфизм, инкапсуляция и т.п.), — воспринималось как китайская грамота.
Напротив, теперь же я почему-то воспринимаю подобный материал нормально, а изложение из своей же статьи временами кажется заморочным и длинным.
Но мои старые заметки и сохранившийся ужасный код на голодисках в пипбое говорят о том, что «классическое» изложение не выполняло в те времена свои функции, и было совершенно неудачным. Возможно, в этом что-то есть.
Насколько это соответствует действительности и вашим собственным предпочтениям, — решайте сами…
Предпослылки к ООП
Код стеной
Когда я только начинал писать на MATLAB’e, то только так писать и умел. Я знал про функции и про то, что программу можно делить на части.
Затык был в том, что все примеры были отстой. Я открыл чей-то курсовик, увидел там мелкие бодяжные функции по 2-3 строчки, в сумме все это НЕ работало (не хватало чего-то), и заработало только тогда, когда я пересобрал эту дрянь в «стену».
Потом я еще несколько раз писал какие-то мелкие программки, и всякий раз недоумевал, зачем там что-то делить. Уже потом пришло понимание: код «стеной» — это нормальное состояние программы объемом примерно 1.5 страницы А4. Никаких функций и, боже упаси, ООП там НЕ нужно.
Вот так примерно выглядит матлабовский скрипт (взято из интернета).
Fs = 1000; % Sampling frequency
T = 1/Fs; % Sample time
L = 1000; % Length of signal
t = (0:L-1)*T; % Time vector
% Sum of a 50 Hz sinusoid and a 120 Hz sinusoid
%x = 0.7*sin(2*pi*50*t) + sin(2*pi*120*t);
%y = x + 2*randn(size(t)); % Sinusoids plus noise
y=1+sin(100*pi*t);
plot(Fs*t(1:50),y(1:50))
title('Signal Corrupted with Zero-Mean Random Noise')
xlabel('time (milliseconds)')
figure
NFFT = 2^nextpow2(L); % Next power of 2 from length of y
Y = fft(y,NFFT)/L;
f = Fs/2*linspace(0,1,NFFT/2+1);
% Plot single-sided amplitude spectrum.
plot(f,2*abs(Y(1:NFFT/2+1)))
title('Single-Sided Amplitude Spectrum of y(t)')
xlabel('Frequency (Hz)')
ylabel('|Y(f)|')
Деление кода на функции
О том, зачем код все-таки делят на куски, я догадался, когда его объем начал становиться совершенно невообразимым (сейчас нашел в архиве говнокод – 650 строк стеной). И тогда я вспомнил про функции. Я знал, что они позволяют разделить код на мелкие блоки, которые легче отладить и переиспользовать.
Но фишка в другом – почему-то все обучающие материалы молчат о том, СКОЛЬКО у функции переменных…
Курс математики говорил о том, что функция — это y=f(x)
Это называется «функция одной переменной». Например, y=x2 это целая ПАРАБОЛА!
Задача по математике: построить ПАРАБОЛУ по точкам. В тетрадном листе, в клеточку.
А еще бывают функции двух переменных. z=f(x,y). И для нее — о боже — можно построить ТРЕХМЕРНЫЙ график. Но мы его строить не будем, т.к. на следующем уроке будет контрольная работа. На ней мы будем строить ПАРАБОЛУ.
А Сидоров не аттестован
А потом еще один товарищ, учащийся в ВУЗе по специальности «прикладная математика», рассказывает про функции трех переменных. Для такой функции, говорит он, надо построить ЧЕТЫРЕХМЕРНЫЙ график. Но четвертое измерение – это время. И мы можем только видеть проекции четырехмерного мира на трехмерное пространство.
И далее он торопливо говорит про четырехмерный куб-тессеракт…
А если функция имеет четыре и более переменных…. Теория суперструн. Многообразие Калаби-Яу. Смертным. Не дано. Понять…
Короче говоря, это все не то. В программировании нормальное состояние функции – это double vaginal double anal. Она принимает 100 переменных и возвращает столько же, и это нормально. Ненормально другое – перечислять их через ЗАПЯТУЮ.
Про то, что можно писать как-то иначе, я понял, когда наваял ВОТ ЭТО
function work = SelectFun(ProtName,length_line,num_length,angleN_1,angleN_2,num_angleN,angleF_1,angleF_2,num_angleF, res_max, num_res,varargin)
global angleF angleN model_initialized
Куча переменных через ЗАПЯТУЮ. А вызывающий код имеет совсем другие названия этих параметров, что-то типа SelectFun(a,b,c,d….) Поэтому нужно запоминать, на каком месте какая переменная стоит. И делать их расстановку через ЗАПЯТУЮ. А если код модернизируется, и количество переменных меняется, то надо их снова расставлять через ЗАПЯТУЮ.
А зачем в этом убожестве были глобальные (расстрелять!) переменные?
Бинго! Чтобы не расставлять переменные при каждой модернизации кода через ЗАПЯТУЮ.
Но ЗАПЯТАЯ все равно преследовала меня, как в кошмарном сне.
И появился varargin. Это значит, что я могу в вызывающем коде дописать еще много аргументов через ЗАПЯТУЮ…
И тогда я подумал о массивах. Учебные примеры взахлеб рассказывали о том, что массив может быть таким:
Х=
[1 2 3
4 5 6
7 8 9]
И понимаете, Х(2,3)=6, а Х(3,3)=9, и мы… мы можем организовать на таких массивах перемножение матрицами! На прошлом уроке мы проходили ПАРАБОЛЫ, а теперь МАТРИЦЫ….
И ни в одной строчке этих охренительных учебников нет короткого и ясного: массивы нужно для того, чтобы сделать функцию 100 переменных и не упасть от их перечисления через ЗАПЯТУЮ.
В общем, мне пришла в голову идея запихать все в одну большую двумерную таблицу. Вначале все шло хорошо:
angles =
[angleN, angleN_1, angleN_2, num_angleN
angleF, angleF_1, angleF_2, num_angleF]
function work= SelectFun(ProtName, length_line, num_length, angles , res_max, num_res, varargin)
Но хотелось большего. И начало получаться что-то вроде такого:
data=
[angleN, angleN_1, angleN_2, num_angleN
angleF, angleF_1, angleF_2, num_angleF
length_line, num_length, 0, 0
res_max,num_res, 0,0]
function work= SelectFun(ProtName,data,varargin)
И все вроде замечательно, но… НУЛИ! Они появились оттого, что я хотел раскидать по разным строкам разнородные данные, и количество данных разного типа было разным… А как функция должна эти нули обрабатывать? А что будет, если я захочу модернизировать код? Я же должен буду переписать обработчик этих поганых нулей внутри функции! Ведь какая-то из переменных может реально быть равной нулю…
I never asked for this…
В общем, так я узнал о СТРУКТУРАХ.
Структуры
Это то, с чего надо было начать изложение про способы упаковки данных. Массивы «таблицей», видимо, исторически возникли первыми, и про них пишут тоже – в начале. На практике можно встретить полно программ, где массивы «таблицей» либо одномерные, либо их нет совсем.
Структура – это «файло-папочная» упаковка данных, примерно на жестком диске компьютера.
Диск D:\
Х (переменная-папка – «объект» или «структура»)
— a.txt (переменная-файл с данными – «поле объекта», англ. field. Хранится число 5)
— b.txt (хранится число 10)
— с.txt
Y (переменная-подпапка – «объект»)
— d.txt (хранится число 2)
— e.txt
Чтобы было понятней, запишем, как мы бы видели путь к файлу d.txt в проводнике Windows
D:\X\Y\d.txt
После этого мы открываем файл и пишем туда число «2».
Теперь – как это будет выглядеть в программном коде. Там нет нужды ссылаться на «корневой локальный диск», поэтому D:\ там просто отсутствует, также у нас не будет расширения файла. Что же касается остального, то вместо слэша \ в программировании обычно используется точка.
Получается вот так:
X.Y.d=2
%Остальные переменные задаем аналогично
X.a=5
X.b=10
Теперь что-то посчитаем
X.c=X.a+X.b %т.е. Х.с=5+10=15
X.Y.e=X.c*X.Y.d %т.е. X.Y.e=15*2=30
В матлабе структуры (struct) можно нафигачить прямо на месте, не отходя от кассы, т.е. код выше является исполняемым, его можно вбить в консоль и все будет сразу работать. Структура сразу появится, и туда будут добавляться сразу все «переменные-файлы» и «переменные-подпапки». Про С#, к сожалению, так сказать нельзя, структура (struct) там задается геморройней.
Структура это более крутой родственник МАССИВА ТАБЛИЦЕЙ, где вместо индексов — файло-папочная система. Структура = «переменная-папка», в которой лежат «переменные-файлы» и другие «переменные-папки» (т.е. как бы подпапки).
Все знакомо, все ровно так же как на компе, папки, в них файлы, только в файлах не фотки, а цифры (хотя и можно и фотки).
Это более продвинутая версия хранения данных для передачи в ФУНКЦИЮ по сравнению с идеей сделать МАССИВ ТАБЛИЦЕЙ, в особенности двумерной, и, задави меня тессеракт, трех- и более- мерной.
МАССИВ ТАБЛИЦЕЙ юзабелен в двух случаях:В реальности МАССИВ ТАБЛИЦЕЙ обычно используется только как одномерная строка однородных данных. Все остальное в нормальных программах делается по «файло-папочной» схеме.
— он маленький (зачем он тогда? Что, нельзя передать в функцию аргументы через запятую?).
— либо по нему можно сделать цикл и автоматизировать поиск/заполнение (это не всегда возможно)
Тогда почему в учебниках по программированию начинают с массивов таблицами?!!!
Короче, «открыв» для себя структуры, я решил, что нашел золотую жилу и срочно переписал все нахрен. Говнокод стал выглядеть как-то вот так:
Data.anglesN=[angleN, angleN_1,angleN_2, num_angleN]; %пусть пока так
Data.anglesF=[angleF, angleF_1, angleF_2, num_angleF]; %пусть пока так
Data.length_line= length_line;
Data.num_length= num_length;
Data.res_max= res_max;
Data.num_res= num_res;
function work= SelectFun(ProtName,Data,varargin)
Да, можно здесь заняться перфекционизомом и сделать кучу вложенных объектов, но задача не в этом. Главное – что теперь внутри функции переменная индексируется не по порядковому номеру (на каком месте в списке аргументов через ЗАПЯТУЮ она стоит), а по имени. И нет никаких тупых нулей. И вызов функции теперь приемлемого вида, ЗАПЯТЫХ всего 2 штуки, можно выдохнуть спокойно.
Классы
Понятие «класс» обрушил на меня тонну терминологии: инкапсуляция, наследование, полиморфизм, статические методы, поля, свойства, ординарные методы, конструктор… #@%!!!..
По неопытности, разобравшись со структурами, я решил, что незачем усложнять сущности без надобности, и подумал – «классы – это типа тех же структур, только посложнее».
В какой-то степени так оно и есть. Точнее это прямо в точности так и есть. Класс, если очень глубоко смотреть – это СТРУКТУРА (идейный потомок массива таблицей), которая создается ПРИ ЗАПУСКЕ программы (вообще бывает вроде бы и не только при запуске). Как и в любом потомке МАССИВА ТАБЛИЦЕЙ, там хранятся данные. К ним можно получить доступ во время работы программы.
Поэтому мой первый класс был примерно таким (пишу пример на C#, в матлабе статические поля нормально не реализуются, только через кривой «хак» c persistent переменными в статической функции).
public class Math{
public static double pi;
public static double e;
public static double CircleLength(double R){ //Т.н. «статический метод»
return 2*Math.pi*R; //вычисляем длину окружности
}
}
Вышеприведенный случай – это как бы «базовое» умение класса – быть тупо массивом (структурой) с данными. Эти данные в него закидываются при запуске программы, и оттуда их можно извлечь, в точности так же, как мы вытаскивали их из структуры выше. Для этого используется ключевое слово static.
Структура ->создается где угодно и хранит данные, которые вводятся в нее когда угодно
Класс -> это такая структура, которая создается при запуске программы. Все поля, отмеченные словом static, просто хранят данные, как в обычной структуре. Статические методы – это просто функции, которые вызываются из класса, как из папки.
double L=Math.CircleLength(10); //L=62,8
Math.pi=4; //трололо
У меня был затык – если поля это переменные, а методы это функции, то как они хранятся в одном месте? Как я понял, функция (метод) в классе – это на самом деле не функция, а указатель на функцию. Т.е. это примерно такая же «переменная», как число пи, в плане работы с ней.
Короче говоря, я вначале классы понял именно в таком объеме и написал еще порцию говнокода, где использовались ТОЛЬКО статические функции. Иначе как папку с функциями я классы вообще не юзал.
Еще этому моменту способствовал тот факт, что именно так в MATLABе классы и делаются — как такая дурацкая папка, название которой начинается с @ (типа @ Math, без пробела), внутри нее взаправдашними файлами с расширением .m лежат функции (методы) и есть заголовочный файл с расширением .m, где объясняется, что функция CircleLength действительно принадлежит классу, а не является просто закинутым туда .m-файлом с не-ООП функцией.
@ Math % папкаДа, там есть более привычный нормальному человеку способ писать класс в одном .m – файле, но поначалу я про это не знал. Статические поля в матлабе только константные, и прописываются 1 раз при запуске программы. Вероятно для того, чтобы сделать защиту от «тралла», который решит присвоить Math.pi=4 (имхо, абсолютно бесполезная и тупая тема, никакой нормальный человек большой проект в матлабе писать не будет, а маленький проект программист отладит и так, вряд ли он совсем идиот).
— Math.m %файл заголовка
— CircleLength.m %файл с функцией
Но вернемся к теме. Кроме статических методов, в классе имеется еще и конструктор. Конструктор – в общем-то это просто функция вида y=f(x) или даже y=f(). Входных аргументов у нее может не быть, выходной обязательно есть, и это всегда новая структура (массив).
Что делает конструктор. Он просто делает структуры. Логически это выглядит примерно так:
Код на C# | Примерный логический эквивалент (псевдокод) |
---|---|
|
|
|
|
Говнокод на матлабе, делающий аналогичные структуры безо всяких классов (где класс присутствует — см. ниже):
function Y=MyClass() %необязательно называть MyClass, можно просто Y=F()
Y.a=5
Y.b=10
end
… где-то в другом месте
Y=MyClass()
И на выходе имеем структуру
Y (переменная-папка)Отсюда, собственно, видно, что так называемые поля класса (не статические, без ключевого кода static) — это локальные переменные, объявляемые внутри функции — конструктора. То, что они за каким-то лешим пишутся не в конструкторе, а снаружи, есть СИНТАКСИЧЕСКИЙ САХАР.
— a (переменная-файл, равна 5)
— b (переменная-файл равна 10)
СИНТАКСИЧЕСКИЙ САХАР — такие bullshit-фичи языка программирования, когда код начинает выглядеть, как будто его хотят обфусцировать прямо при написании. Но зато он становится короче и быстрее (якобы) пишется.
Сделав это «открытие», я, писавший в то время только на матлабе, несказанно удивился.
В матлабе, как уже писал выше, эти структуры можно создавать на месте, без всяких конструкторов, просто написав Y.a=5, Y.b=10, точно так же как вы в операционной системе можете делать файлы и папки не отходя от кассы.
А тут — какой-то бодяжный «конструктор», и все поля структуры (в матлабе они называются properties — свойства, хотя, строго говоря, свойства — это более мутная вещь, нежели поля) нужно бюрократически прописывать в заголовочном файле. Зачем? Единственная польза, которую я тогда видел в этой системе — то, что поля структуры определены заранее, и это как бы «самодокументация» — всегда можно посмотреть, что там должно быть, а чего быть не может. Вот примерно такую лажу я тогда писал:
classdef MyClass
properties %Здесь отдельно указываются свойства
a
b
end
methods %здесь методы
function Y=MyClass() % это конструктор.
%Он просто делает структуру (массив) Y с полями a, b
Y.a=5;
Y.b=10;
end
end
methods (Static) %здесь статические методы
function y=f(x) % это конструктор
y=x^2; %ЕСЛИ МНОГО РАЗ ЭТО ЗАПУСТИТЬ, ТО БУДЕТ ПАРАБОЛА !11
end
end
end
Т.е. вы все правильно поняли: методы только статические, конструктор хз для чего (написано в документации — О, классы должны иметь конструктор — ну вот вам конструктор), все остальное я тупо не знал и решил, что познал дзен и ООП.
Но все же собрать функции (статические методы) по классам-папкам мне казалось крутой идеей, т.к. их было много, и я сел писать говнокод.
Бюрократия
И уперся в такую вещь. Есть набор функций какого-то нижнего уровня логики (они статические и распиханы по классам-папкам, сейчас названия классов опустим):
Y1=f1(X1);
Y2=f2(X2);
Y3=f2(X3);
…
Y20=f20(X20);
В маленьких проектах добиться такого засилья функций невозможно, учебные примеры вообще содержат 2-3 функции — типа «смотри, как мы можем строить ПАРАБОЛУ».
А тут — фигова туча функций, и у каждой, мать ее, у каждой есть выходной аргумент, и что вот с ними всеми делать? Засовывать в функции более высокого («руководящего») уровня логики! Обычно их гораздо меньше (условно, 5 шт. вместо 20). Т.е. условно, нужно вот как-то взять эти Y1,Y2, Y3….Y20 и ПЕРЕПАКОВАТЬ их в какие-то Z1,Z2…Z5. Чтобы потом можно было сделать заседание партии и на нем:
A1=g1(Z1);
A2=g2(Z2);
…
A5=g5(Z5);
%Цели ясны, задачи определены. За работу, товарищи!
Но Z1…Z5 не берутся сами по себе. Для их создания нужны ФУНКЦИИ-ПЕРЕПАКОВЩИКИ. Условно они работают как-то так…
function Z1=Repack1(Y1,Y7, Y19)
Z1.a=Y1.a+Y7.b*Y.19.e^2;
Z1.b=Y7.c-Y19.e;
%....еще миллион каких-то тупых действий с содержимым структур Y1, Y7, Y19
%в агонизирующих попытках сделать Z1.
%А еще нам надо сделать перепаковщих для остальных Z2…Z5,
%количеством 4 штуки. Мой моск!
end
А потом может быть еще один «руководящий» уровень…
Короче, я понял, что попал в логистический ад. Я не мог нормально извлекать данные из ФИГОВОЙ ТУЧИ мелких функций y=f(x) без написания еще ФИГОВОЙ ТУЧИ перепаковочно-бюрократических функций, а когда данные передаются еще на уровень выше, нужны еще ПЕРЕПАКОВЩИКИ. Итоговая программа забита бюрократизмом насквозь – перепаковщиков больше, чем «бизнес-кода». Классы-«папки-для-функций» не решают этой проблемы – они всего лишь собирают чиновных перепаковщиков-идиотов по кучкам.
А потом я решил такой говнокод модернизировать, и выяснилось, что без перепиливания всей бюрократической части это невозможно!
Прямо как жизнь в России…
Я понял, что делаю что-то не то, и разобрался в ООП получше. И решение — оно, если так смотреть, идейно было на поверхности.
Идея ООП
Зачем делать кучу функций вида y=f(x), выдающую РАЗНЫЕ выходные аргументы Y1….Y20, когда можно сделать ОДИН аргумент. Что-то вроде:
Y_all=f1(Y_all, X1);
Y_all=f2(Y_all, X2);
….
Y_all=f20(Y_all, X20);
Тогда абсолютно все результаты работы функций будут засованы в одну структуру, в один массив, просто в разные его отсеки. Все. Дальше Y_all можно передавать сразу наверх, на верхний уровень «руководства».
Y_all=DO_MOST_IMPORTANT_SHIT(Y_all, options_how_to_do_this_shit)
Все-все-все функции-ПЕРЕПАКОВЩИКИ-БЮРОКРАТЫ идут в жопу! Все данные собираются в ОДНУ базу Y_all, все функции низкого уровня суют плоды своих трудов по разным отсекам Y_all, «руководство» шмонает по всем отсекам Y_all и делает то, что должно делать. Ничего лишнего, код пишется быстро и работает замечательно…
Вот именно в этом идея ООП и состоит. В учебниках пишут учебные примеры про яблоки и груши, а потом показывают программу в 5 строчек. Там вообще не нужно никакого ООП, в примерах на 5 строчек, т.к. передача данных на «высший уровень руководства» делается без проблем напрямую.
ООП нужно тогда, когда большой проект и есть проблема «бюрократизации»….
Но вернемся к сути. В реальном ООП есть СИНТАКСИЧЕСКИЙ САХАР. Приведенный выше пример с Y_all использовал просто структуры, функции f(,,,) будем считать статическими. ООП – это набор сахарка, когда код начинает выглядеть вот так:
Y_all.f1(X1); % а было Y_all=f1(Y_all, X1),
Y_all.f2(X2);
….
Y_all.f20(X20);
Y_all.DO_MOST_IMPORTANT_SHIT(options_how_to_do_this_shit);
Т.е. мы как бы решили навести мутный синтаксис, в котором можно не писать Y_all 2 раза, а сделать это только 1 раз. Ибо повторение — мать заикания.
Все остальное объяснение «как работает ООП» сводится к объяснению того, как функционирует синтаксический сахар.
Как функционирует синтаксический сахар ООП
Во-первых, эту базу данных Y_all, очевидно, нужно создать до того момента, как она будет идти аргументом в функцию. Для этого нужен конструктор.
Во-вторых, предусмотреть, желательно заранее, какие «отсеки» в ней будут. Пока база данных Y_all маленькая, такая постановка задачи вызывает раздражение. Хочется помечтать о «создаваемых на ходу классах», примерно так же, как в MATLAB можно делать структуры простыми командами Y.a=5, Y.b=10. Но желание фантазировать на эту тему пропадает после отладки здорового проекта.
Далее — вызов метода (функции).
Вот так это примерно эволюционировало
Функция | Комментарий |
---|---|
Y=f(X) | Так было в математике, когда мы строили по точкам ПАРАБОЛУ! |
Х=f(X) | Нас задрали бюрократы, и у нас |
f(X) | Зачем функции возвращать аргумент? Это архаизм времен уроков математики! И бессмысленный расход памяти! Пусть данные передаются по ссылке, тогда функция сама придет к аргументу, поменяет и уйдет. НИЧЕГО=f(X) Не гора идет к Магомету, а Магомет — к горе. |
Х.f() | Просто вытащили аргумент Х «наружу» синтаксическим сахаром. НИЧЕГО=Х.f(НИЧЕГО) |
Теперь – как устроена внутри такая вот функция, принимающая НИЧЕГО и НИЧЕГО (ключевое слово void в C#) возвращающая.
Мне нравится, как это сделано в матлабе (с точки зрения именно понимания): функция, которую мы вызываем как X.f(), внутри пишется как
Пример кода на MATLAB | Пример кода на C# |
---|---|
|
|
Переменную «по умолчанию» надо всегда писать самой первой. Обозвать ее — как угодно (можно Х, можно this, можно fuck, можно shit). Я обычно в матлабе ее называю this, для единообразия. |
Переменную «по умолчанию» писать не надо вообще. При обучении программированию может показаться, что ее нет (и это был для меня затык)! Но она есть! Как тот самый суслик, она есть и скрыта в «ключевом слове this». «Переименовать» this нельзя (хотя это и к лучшему). |
Вот такая функция с «аргументом по умолчанию — this», лежащая в классе, как в папке — есть ординарный метод (ordinary method, хз, как правильно по-русски).
На самом деле пихать вообще все аргументы в единый this — не всегда правильно. Иногда нужны и какие-то еще аргументы (допустим, это ввод данных пользователем):
public void f(int user_input) {
this.c=this.a+this.b + user_input;
}
Иногда надо даже возвращать аргумент (например, об успешности или неуспешности какой-либо операции), а не писать void. Что, впрочем, не меняет статистики: большинство функции в ООП возвращают НИЧЕГО (void) и принимают либо ничего (аргумент по умолчанию не в счет), либо очень мало аргументов.
Напишем итоговый код
На MATLAB
classdef MyClass<handle %наследование от handle нужно для передачи данных по ссылке
properties %Здесь отдельно указываются свойства
a
b
end
methods %здесь методы
function this=MyClass(a, b) % это конструктор. a, b - пользовательский ввод
this.a=a
this.b=b
end
function f(this)
this.c=this.a+this.b
end
end
end
%Снаружи в каком-нибудь скрипте Untitled.m пишем
X=MyClass(5,10);
X.f();
fprintf(‘X.c=%d',X.c) %выведет Х.с=15
Теперь на C#:
public class MyClass {
public int a;
public int b;
public MyClass(int a, int b) { // это конструктор. a, b - задаются (пользователем)
this.a=a;
this.b=b;
}
public void f(this) {
this.c=this.a+this.b
}
}
//Снаружи в каком-нибудь скрипте пишем
MyClass X=new MyClass(5,10);
X.f();
Console.WriteLine(“X.c={0}”,X.c); //выведет Х.с=15
Когда я разобрался с этим, то вроде бы большинство проблем с написанием кода отошло на второй план…
Свойства (properties) vs поля (fields)
Рассмотрим пример.
without properties | with properties |
---|---|
|
|
|
|
комментарий: aргумент Set_a можно назвать как угодно Set_a(int YourVarName) |
комментарий: переменную внутри set{...} нужно называть всегда value |
Вещь это довольно удобная и часто используемая, но это все равно СИНТАКСИЧЕСКИЙ САХАР.
Field является полноценной переменной. Property — это 2 метода класса (get и set), синтаксис вызова которых копирует «вызов переменной».
На самом деле внутри get и set можно творить хрень:
int A {
get{ return 0;}
set{ Console.WriteLine("Ы"); }
}
Поэтому вроде как рекомендуется писать название properties с большой буквы, а fields — с маленькой.
Бывает (например, в интерфейсах нельзя создавать field), что надо сделать по-быстрому property, тогда можно:
int A { get; set;} //будет скрытая переменная, что-то типа _a
//при set и get будет прямое к ней обращение.
public int B { get; private set;} //так разделяется доступ
//(читает кто угодно, заводит значение только метод своего класса)
Наследование, инкапсуляция, полиморфизм
Почему про них раньше не упоминал? Потому, чтоКогда идет процесс освоения навыков писания в ООП-стиле
— на самом деле, при написании кода они востребованы далеко не с такой силой, как о них упоминается при запросе «Ok Google, what is OOP». Я бы даже сказал, что поначалу они практически нахрен не нужны.
— там, где они нужны, о них можно прочитать (только ленивый про это дело не писал).
— большинство классов у вас будут БЕЗ наследования. Вы просто запиливаете в нужном классе ВЕСЬ функционал, и наследовать что-то не особо и нужно.Потом у Вас прирастут руки к плечам, и Вы разберетесь сами, без этой статьи, где так делать НЕ надо, особенно где НЕ надо писать public.
— соответственно, полиморфизм (примочка к наследованию) тоже идет лесом
— «инкапсуляция» сведется к приписыванию везде (ко всем полям, свойствам и методам) public.
Но все-таки
Наследование. Это умный копи-паст
Ущербная реализация «наследования» выглядит так:
О, в моем говнокоде есть класс MyClass, и в нем не хватает еще одного поля SHIT и еще одного метода DO_THE_SHIT()!Но все-таки мы более цивилизованные люди, и знаем, что лучше не копировать текст программы, а сослаться на него.
*Ctrl+C, Ctrl+V
*Делается новый класс MyClass_s_fichami и туда дописываются желаемое
Допустим, мы все равно пишем на каком-то древнем языке программирования или не в курсе о такой вещи, как «наследование». Тогда мы пишем 2 разных класса
|
|
|
|
То, что мы сделали справа — это и есть «наследование». Только в нормальных языках программирования это делается одной командой:
public class MyClassB : MyClassA {
//поле с объектом класса MyClassA не пишется,
//но оно доступно через ключевое слово base
//поле a (точнее, свойство, property a) не пишется,
//но оно доступно в коде напрямую (см. пример в конструкторе ниже)
public int b;
public void F2(int x){ //код новой фичи
this.b=this.a*this.b;
}
public MyClassB(int a, int b){ //конструктор
//конструктор для поля base с объектом класса A
//перед всем кодом функции вызывается втихаря
this.a=a;
this.b=b;
}
}
Работает «снаружи» код ровно так же, как в варианте 2. Т.е. объект как бы становится «матрешкой» — внутри одного объекта сидит тупо другой объект, и есть «каналы связи», дергая за которые, можно обратиться к внутреннему объекту напрямую.
Заголовок спойлера
В матлабе дело обстоит несколько интересней. Когда вы запускаете конструктор «потомка», MyClassB, то «тихушного» вызова конструктора предка MyClassA — не происходит.
Его нужно напрямую создать. С одной стороны это напрягает:
classdef MyClassB<MyClassA
%тут код...
function MyClassB(a, b)
this@MyClassA(a); %если этого не сделать, потомок будет «пустым»
this.b=b;
end
end
Но если потомок вызывается вообще с другими аргументами, типа MyClassB(d), тогда можно сделать внутри преобразование, что-то типа:
classdef MyClassB<MyClassA
%тут код...
function MyClassB(d)
a=d-5;
this@MyClassA(a);
this.b=d+10;
end
end
В C# так сделать напрямую нельзя, и это порождает необходимость писать какие-то «преобразовывающие функции»:
class MyClassB:MyClassA{
//... тут код
static int TransformArgs( int d) {return d-5;}
MyClassB(int d):base(TransformArgs(d)) {this.b=d+10;}
}
или же делать «статические конструкторы» вот так:
class MyClassB:MyClassA {
//... тут код
MyClassB(){} //конструкторы все делаются без аргументов
static MyClassB GetMyClassB(int d) {
var X=new MyClassB(); //конструктор объекта запускается без аргументов
//а потом ему насовывают
Х.a=d-5;
Х.b=d+10;
return X;
}
}
Вроде про наследование, в основном, все.
Естественно, что никто не заставляет обязательно писать у наследника метод «F1» и свойство (property) «a» так, чтобы они обязательно транслировались в вызов метода и поля предка. Трансляция – это просто поведение «наследования» по умолчанию.
Можно (естественно! это же другие методы в другом классе, бро) написать вот так:
public class MyClassB : MyClassA {
public int a{ //обертка для поля предка
get { return 0; }
set { base.a=0; }//у самопального было бы this.fieldA.a=0;
}
public int b;
public void F1(int x){ //делаем якобы обертку для метода «предка»
//к объекту предка - base - не обращаемся
Console.WriteLine(“МХАХАХА”);//Вместо запуска метода предка творим фигню
}
}
Инкапсуляция
… Концептуально это означет, что внутри объекта класса MyClassB в поле base сидит объект класса MyClassA, с возможностью трансляции управляющих команд снаружи. Обо всем этом написано выше и повторять смысла не имеет.
Есть такая тема с разными модификаторами доступа — public, private, protected… О них, что самое интересное, написано везде более-менее нормально, рекомендую просто прочитать об этом.
public — это будет означать, что field, property или method будут видны снаружи и за них можно будет дергать.Если вы НЕ знаете, что делать, пишете public (вредный совет, да).
Потом найдите в себе силы и выкиньте этот public (ну или для наглядности замените на private) везде, где он лишний (сделайте «рефакторинг»). Да, разумеется, очень хорошо быть провидцем,
private — это означает, что field, property или method «файло-папочного» объекта виден только изнутри методов данного класса.НО… Именно класса, не ИНСТАНЦИИ (объекта). Если у вас есть код вида:
class MyClassA{
private int a=10;
public void DO_SOMETHING(MyClassA other_obj) {
//метод DO_SOMETHING способен присунуть
//в любое private поле первого попавшегося объекта класса MyClassA.
this.a=100; //Как в поле своего объекта
other_obj.a=100; //так и чужого
}
}
var X=new MyClassA();
var Y=new MyClassA();
X.DO_SOMETHING(Y); //Будет X.a=100, Y.a=100
Такая штука используется в клонировании (подробнее см. другие источники).
Я пытался при написании кода думать об этой расстановке public и private. При черновых набросках кода на это тратится непозволительно много времени. А потом оказывается, что вообще сам код надо делать принципиально по-другому.
Если код пишется в соло, то нет смысла заморачиваться с private и public раньше времени, есть более важные задачи, например, собственно придумать и написать код…
Единственное место, где более-менее ясно, в каком месте ставить private и public — это те самые пресловутые свойства, которые ссылаются на какое-то поле.
class MyClassA{
//вот тут private
private int a; //"private" в C# по умолчанию можно не писать.
//а вот тут public
public int A {get{...;} set{...;}} //ссылаемся внутри на "а"
}
В остальных местах для расстановки public и private надо реально смотреть, что программа делает, и обучиться этому «заочно», скорее всего, не выйдет.
protected — это означает "public" для всех методов классов-наследников и "private" для всего остального.В общем-то логично, если считать, что классы-наследники появляются как просто «более навороченные версии» предков.
Честно говоря, уже и забыл, где в явном виде я этот protected применял. Обычно либо public, либо private. Большинство классов, которые я писал, не наследовались ни от каких других пользовательских классов, а там, где наследовались, какая-то серьезная потребность в таких вещах возникала редко.
Впечатление такое, что НЕ-public модификаторы востребованы при работе над каким-то большим проектом, который, возможно будет поддерживаться кучей людей… Понимание того, где их применять, появляется только спустя большое время втыкания в километровой длины код. При обучении «заочно» как-то дать это понимание затруднительно.
Полиморфизм
Кода я писал на матлабе, я никак не мог допетрить, зачем вообще нужен полиморфизм и ЧТО ЭТО.
Потом, когда перешел на C#, дошло, что это фича СТРОГО ТИПИЗИРОВАННЫХ ЯЗЫКОВ, и к ООП она имеет весьма слабое отношение. В матлабе можно вообще везде писать, не зная о существовании этого полиморфизма – там нет строгой типизации.
Для простоты пусть классы называются А и В
class A{...}
class B:A{...}
A X=new B();
//Мы объявили x как A, но создали де-факто объект класса B.
//Ну или вот так.
B x_asB=new B();
A x_asA=(A) x_asB;
Это называется приведение типов. В C# можно САМОМУ (если знать как) написать свои кастомные самопальные системы приведения типов, чуть ли ни каких угодно типов к каким угодно другим.
Тут — просто «приведение типов» из коробки. Раз внутри объекта x, принадлежащему к классу B, сидит другой объект класса A, то один из вроде как очевидных способов приведения — замкнуть все связи от внешнего объекта на внутренний.
На самом деле так делать вовсе необязательно, но те, кто придумал «полиморфизм», решили, что наиболее очевидно будет сделать именно так. А остальные варианты пользователь сам напишет.
Простите за (уже не совсем актуальную) «политоту» образца 2008-2012 гг.
сlass Путин {...}
class Медведев : Путин {...}
Медведев медведев = new Meдведев (); //Внутри Медведева скрыт Путин
Путин путин = (Путин) медведев; //А так все становится очевидным
Интерфейс
Надо начать с того, как ЭТО применять.
Допустим, у нас есть списочек, и мы в него что-то хотим положить.
В матлабе наиболее просто сделать это так (называется cell array):
myList={1, ‘2’, ‘fuck’, ‘shit’, MyClassA(), MyClassB(), …. ,Лысый_Черт, Ваша_Бабушка};
Вы не думаете, что это за объект, вы просто берете его и кладете в списочек.
Далее, допустим, вам нужно сделать цикл по списочку и сделать с каждым элементом что-то:
for i=1:length(myList)
item=myList(i);
% здесь мы что-то делаем с item-ом
DoSomeStuff(item);
end
Если функция DoSomeStuff настолько умная, что переваривает все, что ей скармливают, этот код ВЫПОЛНИТСЯ.
Если функция DoSomeStuff (или ее автор) – интеллектом не блещет, то есть вероятность подавиться чем-то: цифрой, строкой, Вашим самопальным классом, Лысым Чертом или – не дай бог — Вашей Бабушкой.
MATLAB покажет красную ругань на английском в консоли и прекратит работу Вашей программы. Таким образом, Ваш код автоматически получает Премию Дарвина.
Однако, на самом деле, это плохо, потому что иногда код бывает очень сложным. Тогда Вы будете свято уверены в том, что сделали все правильно, но на самом деле ошибочная комбинация действий просто ни разу не запускалась во время тестирования.
Именно поэтому (хотя и не только поэтому) на MATLAB – успел убедиться в этом сам (примерно как на КПДВ), на ужасных размеров коде — НЕЗАЧЕМ писать большие проекты.
Теперь переходим к C#. Мы делаем списочек, и… и нас просят сразу указать ТИП объекта. Мы создаем список типа List.
В такой список можно поместить число 1.
В такой список можно поместить число 2 и даже, прости господи, 3.
List<int> lst1=new List<int>().
lst.Add(1);
lst.Add(2);
lst.Add(3);
Но текстовые строки – уже нет. Объекты Вашего самопального класса – строго нет. Я молчу насчет Лысого Черта и Вашей Бабушки, они там не могут оказаться ни при каком варианте.
Можно сделать отдельно списочек строк. Можно – для ваших самопальных классов.
List<MyClassA> lst2=new List<MyClassA>();
lst2.Add(new MyClassA());
На самом деле можно сделать и списки – отдельно — Лысых Чертей, Ваших Бабушек.
Но сложить их в один список не получится. Ваш код получит Премию Дарвина в сочетании с руганью компилятора еще до того, как вы его попробуете запустить. Компилятор предусмотрительно не дает Вам сделать функцию DoSomeStuff(item), которая «подавится» своим аргументом.
В больших проектах это реально удобно.
Но что делать, когда в один списочек сложить все-таки хочется?
На самом деле, это не проблема. Достаточно преобразовать все к типу object. Почти (или даже абсолютно) все можно преобразовать к типу object.
List<object> lst=new List<object>();
lst.Add((object) new MyClassA());
lst.Add((object) new MyClassB());
Проблема начинается тогда, когда мы начинаем делать цикл по списку. Дело в том, что тип object ничего (почти) не умеет делать. Он умеет только БЫТЬ типом object.
— Что вы умеете делать?
— я умею петь и танцевать
— А я — Санчо…
— Что ты умеешь делать, Санчо?
— Я — Санчо.
— Ну ты можешь делать хоть что-то?
— Вы не понимаете. Я могу быть Санчо.
Поэтому пишется интерфейс. Это такой класс, от которого нужно наследоваться. Интерфейс содержит заголовки методов и свойств.
В нашем случае это те методы и свойства, которые обеспечивают НОРМАЛЬНУЮ работу функции DoSomeStuff(item). Сами свойства интерфейс при этом не реализует. Это специально так сделано. На самом деле можно было бы просто унаследоваться от какого-то класса, пригодного для употребления функцией DoSomeStuff(). Но это означает дополнительный код и забывчивого программиста.
Поэтому, если товарищ программист унаследовался от интерфейса, но забыл реализовать нужные свойства и методы класса, компилятор выпишет его коду Премию Дарвина. Таким образом, можно сделать так:
interface ICanDoTheStuff {...};
class MyClassA: ICanDoTheStuff {…}
class MyClassB: ICanDoTheStuff {…}
static void DoSomeStuff(ICanDoTheStuff item) {…}
List<ICanDoTheStuff> lst= new List<ICanDoTheStuff>();
lst.Add(new MyClassA());
lst.Add(new MyClassB());
for (int i=0; i<lst.Count; i++) {
ICanDoTheStuff item=myList[i];
DoSomeStuff(item);
}
Т.е. для чего в конечном счете нужен интерфейс — для того, чтобы сделать типизированный списочек, или какое-то поле в классе, и обойти запрет на добавление (в списочек или поле) туда какой-то левой фигни.
Интерфейс — это «бюрократия». Не везде она есть и не везде она нужна, хотя да, в больших проектах нужна и полезна.
… в общем, как-то так… Извиняюсь за резковатые выражения, мне почему-то кажется, что «сухое» изложение материала было бы неудачным…